diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..5971dd2
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,81 @@
+name: CI
+'on':
+ - push
+jobs:
+ code_checks:
+ runs-on: ubuntu-20.04
+ services:
+ db:
+ image: mysql:5.7.44
+ ports:
+ - 3307:3307
+ env:
+ MYSQL_DATABASE: 'homework_assignment_2024_test'
+ MYSQL_USER: 'rails_user'
+ MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }}
+ MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }}
+ MYSQL_ROOT_HOST: '%'
+ MYSQL_TCP_PORT: 3307
+ # MYSQL_HOST: localhost
+ options: >-
+ --health-cmd "mysqladmin ping"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v1
+ - name: Show databases for root user
+ run: mysql --protocol=tcp -h 127.0.0.1 -P 3307 -u root -p${{ secrets.MYSQL_ROOT_PASSWORD }} -e "SHOW DATABASES"
+ - name: Show databases for rails_user
+ run: mysql --protocol=tcp -h 127.0.0.1 -P 3307 -u rails_user -p${{ secrets.MYSQL_PASSWORD }} -e "SHOW DATABASES"
+ - uses: actions/setup-node@v1
+ with:
+ node-version: 20.x
+ registry-url: 'https://registry.npmjs.org'
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
+ - run: bundle exec rake
+ # - name: Gem cache
+ # id: cache-bundle
+ # uses: actions/cache@v1
+ # with:
+ # path: vendor/bundle
+ # key: 'bundle-${{ hashFiles(''**/Gemfile.lock'') }}'
+ # - name: Bundle install
+ # env:
+ # RAILS_ENV: test
+ # run: |
+ # gem install bundler
+ # bundle install --jobs 4 --retry 3 --path vendor/bundle
+ - name: Install yarn
+ run: npm install -g yarn
+ - name: Get yarn cache dir
+ id: yarn-cache-dir
+ run: 'echo "::set-output name=dir::$(yarn cache dir)"'
+ - name: Yarn cache
+ id: cache-yarn
+ uses: actions/cache@v1
+ with:
+ path: '${{ steps.yarn-cache-dir.outputs.dir }}'
+ key: 'yarn-${{ hashFiles(''**/yarn.lock'') }}'
+ - name: Yarn install
+ run: yarn install
+ - name: 'Setup DB, Run tests'
+ env:
+ RAILS_ENV: test
+ run: bundle exec rails db:migrate
+ - name: Audit
+ env:
+ RAILS_ENV: test
+ run: bundle exec rake audit
+ - name: Rubocop
+ env:
+ RAILS_ENV: test
+ run: bundle exec rubocop
+ - name: RSpec
+ env:
+ RAILS_ENV: test
+ run: bundle exec rspec
diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..c99d2e7
--- /dev/null
+++ b/.rspec
@@ -0,0 +1 @@
+--require spec_helper
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000..5d0a174
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,436 @@
+# .rubocop.yml
+require:
+ - rubocop-rails
+ - rubocop-performance
+ - rubocop-migration
+ - rubocop-rspec_rails
+ - rubocop-factory_bot
+
+inherit_from:
+ - .rubocop_todo.yml
+
+AllCops:
+ NewCops: enable
+ TargetRailsVersion: 7.0
+ TargetRubyVersion: 3.2
+
+Migration/AddCheckConstraint:
+ Enabled: true
+Migration/AddColumnWithDefaultValue:
+ Enabled: true
+Migration/AddForeignKey:
+ Enabled: true
+Migration/AddIndexColumnsCount:
+ Enabled: true
+Migration/AddIndexConcurrently:
+ Enabled: true
+Migration/AddIndexDuplicate:
+ Enabled: true
+Migration/BatchInBatches:
+ Enabled: true
+Migration/BatchInTransaction:
+ Enabled: true
+Migration/BatchWithThrottling:
+ Enabled: true
+Migration/ChangeColumn:
+ Enabled: true
+Migration/ChangeColumnNull:
+ Enabled: true
+Migration/CreateTableForce:
+ Enabled: true
+Migration/Jsonb:
+ Enabled: true
+Migration/RemoveColumn:
+ Enabled: true
+Migration/RenameColumn:
+ Enabled: true
+Migration/RenameTable:
+ Enabled: true
+Migration/ReservedWordMysql:
+ Enabled: true
+
+Metrics/BlockLength:
+ AllowedMethods: ['describe', 'context']
+
+Gemspec/DeprecatedAttributeAssignment: # new in 1.30
+ Enabled: true
+Gemspec/DevelopmentDependencies: # new in 1.44
+ Enabled: true
+Gemspec/RequireMFA: # new in 1.23
+ Enabled: true
+Layout/LineContinuationLeadingSpace: # new in 1.31
+ Enabled: true
+Layout/LineContinuationSpacing: # new in 1.31
+ Enabled: true
+Layout/LineEndStringConcatenationIndentation: # new in 1.18
+ Enabled: true
+Layout/SpaceBeforeBrackets: # new in 1.7
+ Enabled: true
+Lint/AmbiguousAssignment: # new in 1.7
+ Enabled: true
+Lint/AmbiguousOperatorPrecedence: # new in 1.21
+ Enabled: true
+Lint/AmbiguousRange: # new in 1.19
+ Enabled: true
+Lint/ConstantOverwrittenInRescue: # new in 1.31
+ Enabled: true
+Lint/DeprecatedConstants: # new in 1.8
+ Enabled: true
+Lint/DuplicateBranch: # new in 1.3
+ Enabled: true
+Lint/DuplicateMagicComment: # new in 1.37
+ Enabled: true
+Lint/DuplicateMatchPattern: # new in 1.50
+ Enabled: true
+Lint/DuplicateRegexpCharacterClassElement: # new in 1.1
+ Enabled: true
+Lint/EmptyBlock: # new in 1.1
+ Enabled: true
+Lint/EmptyClass: # new in 1.3
+ Enabled: true
+Lint/EmptyInPattern: # new in 1.16
+ Enabled: true
+Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21
+ Enabled: true
+Lint/ItWithoutArgumentsInBlock: # new in 1.59
+ Enabled: true
+Lint/LambdaWithoutLiteralBlock: # new in 1.8
+ Enabled: true
+Lint/LiteralAssignmentInCondition: # new in 1.58
+ Enabled: true
+Lint/MixedCaseRange: # new in 1.53
+ Enabled: true
+Lint/NoReturnInBeginEndBlocks: # new in 1.2
+ Enabled: true
+Lint/NonAtomicFileOperation: # new in 1.31
+ Enabled: true
+Lint/NumberedParameterAssignment: # new in 1.9
+ Enabled: true
+Lint/OrAssignmentToConstant: # new in 1.9
+ Enabled: true
+Lint/RedundantDirGlobSort: # new in 1.8
+ Enabled: true
+Lint/RedundantRegexpQuantifiers: # new in 1.53
+ Enabled: true
+Lint/RefinementImportMethods: # new in 1.27
+ Enabled: true
+Lint/RequireRangeParentheses: # new in 1.32
+ Enabled: true
+Lint/RequireRelativeSelfPath: # new in 1.22
+ Enabled: true
+Lint/SymbolConversion: # new in 1.9
+ Enabled: true
+Lint/ToEnumArguments: # new in 1.1
+ Enabled: true
+Lint/TripleQuotes: # new in 1.9
+ Enabled: true
+Lint/UnexpectedBlockArity: # new in 1.5
+ Enabled: true
+Lint/UnmodifiedReduceAccumulator: # new in 1.1
+ Enabled: true
+Lint/UselessRescue: # new in 1.43
+ Enabled: true
+Lint/UselessRuby2Keywords: # new in 1.23
+ Enabled: true
+Metrics/CollectionLiteralLength: # new in 1.47
+ Enabled: true
+Naming/BlockForwarding: # new in 1.24
+ Enabled: true
+Security/CompoundHash: # new in 1.28
+ Enabled: true
+Security/IoMethods: # new in 1.22
+ Enabled: true
+Style/ArgumentsForwarding: # new in 1.1
+ Enabled: true
+Style/ArrayIntersect: # new in 1.40
+ Enabled: true
+Style/CollectionCompact: # new in 1.2
+ Enabled: true
+Style/ComparableClamp: # new in 1.44
+ Enabled: true
+Style/ConcatArrayLiterals: # new in 1.41
+ Enabled: true
+Style/DataInheritance: # new in 1.49
+ Enabled: true
+Style/DirEmpty: # new in 1.48
+ Enabled: true
+Style/DocumentDynamicEvalDefinition: # new in 1.1
+ Enabled: true
+Style/EmptyHeredoc: # new in 1.32
+ Enabled: true
+Style/EndlessMethod: # new in 1.8
+ Enabled: true
+Style/EnvHome: # new in 1.29
+ Enabled: true
+Style/ExactRegexpMatch: # new in 1.51
+ Enabled: true
+Style/FetchEnvVar: # new in 1.28
+ Enabled: true
+Style/FileEmpty: # new in 1.48
+ Enabled: true
+Style/FileRead: # new in 1.24
+ Enabled: true
+Style/FileWrite: # new in 1.24
+ Enabled: true
+Style/HashConversion: # new in 1.10
+ Enabled: true
+Style/HashExcept: # new in 1.7
+ Enabled: true
+Style/IfWithBooleanLiteralBranches: # new in 1.9
+ Enabled: true
+Style/InPatternThen: # new in 1.16
+ Enabled: true
+Style/MagicCommentFormat: # new in 1.35
+ Enabled: true
+Style/MapCompactWithConditionalBlock: # new in 1.30
+ Enabled: true
+Style/MapIntoArray: # new in 1.63
+ Enabled: true
+Style/MapToHash: # new in 1.24
+ Enabled: true
+Style/MapToSet: # new in 1.42
+ Enabled: true
+Style/MinMaxComparison: # new in 1.42
+ Enabled: true
+Style/MultilineInPatternThen: # new in 1.16
+ Enabled: true
+Style/NegatedIfElseCondition: # new in 1.2
+ Enabled: true
+Style/NestedFileDirname: # new in 1.26
+ Enabled: true
+Style/NilLambda: # new in 1.3
+ Enabled: true
+Style/NumberedParameters: # new in 1.22
+ Enabled: true
+Style/NumberedParametersLimit: # new in 1.22
+ Enabled: true
+Style/ObjectThen: # new in 1.28
+ Enabled: true
+Style/OpenStructUse: # new in 1.23
+ Enabled: true
+Style/OperatorMethodCall: # new in 1.37
+ Enabled: true
+Style/QuotedSymbols: # new in 1.16
+ Enabled: true
+Style/RedundantArgument: # new in 1.4
+ Enabled: true
+Style/RedundantArrayConstructor: # new in 1.52
+ Enabled: true
+Style/RedundantConstantBase: # new in 1.40
+ Enabled: true
+Style/RedundantCurrentDirectoryInPath: # new in 1.53
+ Enabled: true
+Style/RedundantDoubleSplatHashBraces: # new in 1.41
+ Enabled: true
+Style/RedundantEach: # new in 1.38
+ Enabled: true
+Style/RedundantFilterChain: # new in 1.52
+ Enabled: true
+Style/RedundantHeredocDelimiterQuotes: # new in 1.45
+ Enabled: true
+Style/RedundantInitialize: # new in 1.27
+ Enabled: true
+Style/RedundantLineContinuation: # new in 1.49
+ Enabled: true
+Style/RedundantRegexpArgument: # new in 1.53
+ Enabled: true
+Style/RedundantRegexpConstructor: # new in 1.52
+ Enabled: true
+Style/RedundantSelfAssignmentBranch: # new in 1.19
+ Enabled: true
+Style/RedundantStringEscape: # new in 1.37
+ Enabled: true
+Style/ReturnNilInPredicateMethodDefinition: # new in 1.53
+ Enabled: true
+Style/SelectByRegexp: # new in 1.22
+ Enabled: true
+Style/SingleLineDoEndBlock: # new in 1.57
+ Enabled: true
+Style/StringChars: # new in 1.12
+ Enabled: true
+Style/SuperWithArgsParentheses: # new in 1.58
+ Enabled: true
+Style/SwapValues: # new in 1.1
+ Enabled: true
+Style/YAMLFileRead: # new in 1.53
+ Enabled: true
+Rails/ActionControllerFlashBeforeRender: # new in 2.16
+ Enabled: true
+Rails/ActionControllerTestCase: # new in 2.14
+ Enabled: true
+Rails/ActionOrder: # new in 2.17
+ Enabled: true
+Rails/ActiveRecordCallbacksOrder: # new in 2.7
+ Enabled: true
+Rails/ActiveSupportOnLoad: # new in 2.16
+ Enabled: true
+Rails/AddColumnIndex: # new in 2.11
+ Enabled: true
+Rails/AfterCommitOverride: # new in 2.8
+ Enabled: true
+Rails/AttributeDefaultBlockValue: # new in 2.9
+ Enabled: true
+Rails/CompactBlank: # new in 2.13
+ Enabled: true
+Rails/DangerousColumnNames: # new in 2.21
+ Enabled: true
+Rails/DeprecatedActiveModelErrorsMethods: # new in 2.14
+ Enabled: true
+Rails/DotSeparatedKeys: # new in 2.15
+ Enabled: true
+Rails/DuplicateAssociation: # new in 2.14
+ Enabled: true
+Rails/DuplicateScope: # new in 2.14
+ Enabled: true
+Rails/DurationArithmetic: # new in 2.13
+ Enabled: true
+Rails/EagerEvaluationLogMessage: # new in 2.11
+ Enabled: true
+Rails/EnvLocal: # new in 2.22
+ Enabled: true
+Rails/ExpandedDateRange: # new in 2.11
+ Enabled: true
+Rails/FindById: # new in 2.7
+ Enabled: true
+Rails/FreezeTime: # new in 2.16
+ Enabled: true
+Rails/I18nLazyLookup: # new in 2.14
+ Enabled: true
+Rails/I18nLocaleAssignment: # new in 2.11
+ Enabled: true
+Rails/I18nLocaleTexts: # new in 2.14
+ Enabled: true
+Rails/IgnoredColumnsAssignment: # new in 2.17
+ Enabled: true
+Rails/Inquiry: # new in 2.7
+ Enabled: true
+Rails/MailerName: # new in 2.7
+ Enabled: true
+Rails/MatchRoute: # new in 2.7
+ Enabled: true
+Rails/MigrationClassName: # new in 2.14
+ Enabled: true
+Rails/NegateInclude: # new in 2.7
+ Enabled: true
+Rails/Pluck: # new in 2.7
+ Enabled: true
+Rails/PluckInWhere: # new in 2.7
+ Enabled: true
+Rails/RedundantActiveRecordAllMethod: # new in 2.21
+ Enabled: true
+Rails/RedundantPresenceValidationOnBelongsTo: # new in 2.13
+ Enabled: true
+Rails/RedundantTravelBack: # new in 2.12
+ Enabled: true
+Rails/RenderInline: # new in 2.7
+ Enabled: true
+Rails/RenderPlainText: # new in 2.7
+ Enabled: true
+Rails/ResponseParsedBody: # new in 2.18
+ Enabled: true
+Rails/RootJoinChain: # new in 2.13
+ Enabled: true
+Rails/RootPathnameMethods: # new in 2.16
+ Enabled: true
+Rails/RootPublicPath: # new in 2.15
+ Enabled: true
+Rails/SelectMap: # new in 2.21
+ Enabled: true
+Rails/ShortI18n: # new in 2.7
+ Enabled: true
+Rails/SquishedSQLHeredocs: # new in 2.8
+ Enabled: true
+Rails/StripHeredoc: # new in 2.15
+ Enabled: true
+Rails/ThreeStateBooleanColumn: # new in 2.19
+ Enabled: true
+Rails/TimeZoneAssignment: # new in 2.10
+ Enabled: true
+Rails/ToFormattedS: # new in 2.15
+ Enabled: true
+Rails/ToSWithArgument: # new in 2.16
+ Enabled: true
+Rails/TopLevelHashWithIndifferentAccess: # new in 2.16
+ Enabled: true
+Rails/TransactionExitStatement: # new in 2.14
+ Enabled: true
+Rails/UnusedIgnoredColumns: # new in 2.11
+ Enabled: true
+Rails/UnusedRenderContent: # new in 2.21
+ Enabled: true
+Rails/WhereEquals: # new in 2.9
+ Enabled: true
+Rails/WhereExists: # new in 2.7
+ Enabled: true
+Rails/WhereMissing: # new in 2.16
+ Enabled: true
+Rails/WhereNot: # new in 2.8
+ Enabled: true
+Rails/WhereNotWithMultipleConditions: # new in 2.17
+ Enabled: true
+Performance/AncestorsInclude: # new in 1.7
+ Enabled: true
+Performance/BigDecimalWithNumericArgument: # new in 1.7
+ Enabled: true
+Performance/BlockGivenWithExplicitBlock: # new in 1.9
+ Enabled: true
+Performance/CollectionLiteralInLoop: # new in 1.8
+ Enabled: true
+Performance/ConcurrentMonotonicTime: # new in 1.12
+ Enabled: true
+Performance/ConstantRegexp: # new in 1.9
+ Enabled: true
+Performance/MapCompact: # new in 1.11
+ Enabled: true
+Performance/MapMethodChain: # new in 1.19
+ Enabled: true
+Performance/MethodObjectAsBlock: # new in 1.9
+ Enabled: true
+Performance/RedundantEqualityComparisonBlock: # new in 1.10
+ Enabled: true
+Performance/RedundantSortBlock: # new in 1.7
+ Enabled: true
+Performance/RedundantSplitRegexpArgument: # new in 1.10
+ Enabled: true
+Performance/RedundantStringChars: # new in 1.7
+ Enabled: true
+Performance/ReverseFirst: # new in 1.7
+ Enabled: true
+Performance/SortReverse: # new in 1.7
+ Enabled: true
+Performance/Squeeze: # new in 1.7
+ Enabled: true
+Performance/StringIdentifierArgument: # new in 1.13
+ Enabled: true
+Performance/StringInclude: # new in 1.7
+ Enabled: true
+Performance/Sum: # new in 1.8
+ Enabled: true
+RSpecRails/AvoidSetupHook: # new in 2.4
+ Enabled: true
+RSpecRails/HaveHttpStatus: # new in 2.12
+ Enabled: true
+RSpecRails/InferredSpecType: # new in 2.14
+ Enabled: false
+RSpecRails/MinitestAssertions: # new in 2.17
+ Enabled: true
+RSpecRails/NegationBeValid: # new in 2.23
+ Enabled: true
+RSpecRails/TravelAround: # new in 2.19
+ Enabled: true
+FactoryBot/AssociationStyle: # new in 2.23
+ Enabled: true
+FactoryBot/ConsistentParenthesesStyle: # new in 2.14
+ Enabled: true
+FactoryBot/ExcessiveCreateList: # new in 2.25
+ Enabled: true
+FactoryBot/FactoryAssociationWithStrategy: # new in 2.23
+ Enabled: true
+FactoryBot/FactoryNameStyle: # new in 2.16
+ Enabled: true
+FactoryBot/IdSequence: # new in 2.24
+ Enabled: true
+FactoryBot/RedundantFactoryOption: # new in 2.23
+ Enabled: true
+FactoryBot/SyntaxMethods: # new in 2.7
+ Enabled: true
\ No newline at end of file
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
new file mode 100644
index 0000000..c11ffec
--- /dev/null
+++ b/.rubocop_todo.yml
@@ -0,0 +1,2 @@
+Style/Documentation:
+ Enabled: false
\ No newline at end of file
diff --git a/Gemfile b/Gemfile
index 27ff243..8dd8017 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,37 +1,39 @@
-source "https://rubygems.org"
+# frozen_string_literal: true
+
+source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
-ruby "3.2.2"
+ruby '3.2.2'
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
-gem "rails", "~> 7.0.8"
+gem 'rails', '~> 7.0.8'
# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
-gem "sprockets-rails"
+gem 'sprockets-rails'
# Use mysql as the database for Active Record
-gem "mysql2", "~> 0.5"
+gem 'mysql2', '~> 0.5'
# Use the Puma web server [https://github.com/puma/puma]
-gem "puma"
+gem 'puma'
# Bundle and transpile JavaScript [https://github.com/rails/jsbundling-rails]
-gem "jsbundling-rails"
+gem 'jsbundling-rails'
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
-gem "turbo-rails"
+gem 'turbo-rails'
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
-gem "stimulus-rails"
+gem 'stimulus-rails'
# Bundle and process CSS [https://github.com/rails/cssbundling-rails]
-gem "cssbundling-rails"
+gem 'cssbundling-rails'
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
-gem "jbuilder"
+gem 'jbuilder'
# Use Redis adapter to run Action Cable in production
-gem "redis", "~> 4.0"
+gem 'redis', '~> 4.0'
# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"
@@ -40,10 +42,10 @@ gem "redis", "~> 4.0"
# gem "bcrypt", "~> 3.1.7"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
-gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ]
+gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
# Reduces boot times through caching; required in config/boot.rb
-gem "bootsnap", require: false
+gem 'bootsnap', require: false
# Use Sass to process CSS
# gem "sassc-rails"
@@ -51,15 +53,26 @@ gem "bootsnap", require: false
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"
+gem 'interactor', '~> 3.0'
+
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
- gem "debug", platforms: %i[ mri mingw x64_mingw ]
- gem "faker"
+ gem 'bundler-audit'
+ gem 'debug', platforms: %i[mri mingw x64_mingw]
+ gem 'factory_bot_rails'
+ gem 'faker'
+ gem 'rspec-rails', '~> 6.1.0'
+ gem 'rubocop-factory_bot', require: false
+ gem 'rubocop-migration', require: false
+ gem 'rubocop-performance', require: false
+ gem 'rubocop-rails', require: false
+ gem 'rubocop-rspec', require: false
+ gem 'rubocop-rspec_rails', require: false
end
group :development do
# Use console on exceptions pages [https://github.com/rails/web-console]
- gem "web-console"
+ gem 'web-console'
# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
# gem "rack-mini-profiler"
@@ -67,4 +80,3 @@ group :development do
# Speed up commands on slow machines / big apps [https://github.com/rails/spring]
# gem "spring"
end
-
diff --git a/Gemfile.lock b/Gemfile.lock
index c442782..bd0191c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,75 +1,79 @@
GEM
remote: https://rubygems.org/
specs:
- actioncable (7.0.8)
- actionpack (= 7.0.8)
- activesupport (= 7.0.8)
+ actioncable (7.0.8.1)
+ actionpack (= 7.0.8.1)
+ activesupport (= 7.0.8.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (7.0.8)
- actionpack (= 7.0.8)
- activejob (= 7.0.8)
- activerecord (= 7.0.8)
- activestorage (= 7.0.8)
- activesupport (= 7.0.8)
+ actionmailbox (7.0.8.1)
+ actionpack (= 7.0.8.1)
+ activejob (= 7.0.8.1)
+ activerecord (= 7.0.8.1)
+ activestorage (= 7.0.8.1)
+ activesupport (= 7.0.8.1)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
- actionmailer (7.0.8)
- actionpack (= 7.0.8)
- actionview (= 7.0.8)
- activejob (= 7.0.8)
- activesupport (= 7.0.8)
+ actionmailer (7.0.8.1)
+ actionpack (= 7.0.8.1)
+ actionview (= 7.0.8.1)
+ activejob (= 7.0.8.1)
+ activesupport (= 7.0.8.1)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
- actionpack (7.0.8)
- actionview (= 7.0.8)
- activesupport (= 7.0.8)
+ actionpack (7.0.8.1)
+ actionview (= 7.0.8.1)
+ activesupport (= 7.0.8.1)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (7.0.8)
- actionpack (= 7.0.8)
- activerecord (= 7.0.8)
- activestorage (= 7.0.8)
- activesupport (= 7.0.8)
+ actiontext (7.0.8.1)
+ actionpack (= 7.0.8.1)
+ activerecord (= 7.0.8.1)
+ activestorage (= 7.0.8.1)
+ activesupport (= 7.0.8.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
- actionview (7.0.8)
- activesupport (= 7.0.8)
+ actionview (7.0.8.1)
+ activesupport (= 7.0.8.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
- activejob (7.0.8)
- activesupport (= 7.0.8)
+ activejob (7.0.8.1)
+ activesupport (= 7.0.8.1)
globalid (>= 0.3.6)
- activemodel (7.0.8)
- activesupport (= 7.0.8)
- activerecord (7.0.8)
- activemodel (= 7.0.8)
- activesupport (= 7.0.8)
- activestorage (7.0.8)
- actionpack (= 7.0.8)
- activejob (= 7.0.8)
- activerecord (= 7.0.8)
- activesupport (= 7.0.8)
+ activemodel (7.0.8.1)
+ activesupport (= 7.0.8.1)
+ activerecord (7.0.8.1)
+ activemodel (= 7.0.8.1)
+ activesupport (= 7.0.8.1)
+ activestorage (7.0.8.1)
+ actionpack (= 7.0.8.1)
+ activejob (= 7.0.8.1)
+ activerecord (= 7.0.8.1)
+ activesupport (= 7.0.8.1)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
- activesupport (7.0.8)
+ activesupport (7.0.8.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
+ ast (2.4.2)
bindex (0.8.1)
bootsnap (1.17.1)
msgpack (~> 1.2)
builder (3.2.4)
+ bundler-audit (0.9.1)
+ bundler (>= 1.2.0, < 3)
+ thor (~> 1.0)
concurrent-ruby (1.2.3)
crass (1.0.6)
cssbundling-rails (1.4.0)
@@ -78,13 +82,20 @@ GEM
debug (1.9.1)
irb (~> 1.10)
reline (>= 0.3.8)
+ diff-lcs (1.5.1)
erubi (1.12.0)
+ factory_bot (6.4.6)
+ activesupport (>= 5.0.0)
+ factory_bot_rails (6.4.3)
+ factory_bot (~> 6.4)
+ railties (>= 5.0.0)
faker (3.2.3)
i18n (>= 1.8.11, < 2)
globalid (1.2.1)
activesupport (>= 6.1)
- i18n (1.14.1)
+ i18n (1.14.5)
concurrent-ruby (~> 1.0)
+ interactor (3.1.2)
io-console (0.7.2)
irb (1.11.1)
rdoc
@@ -94,6 +105,8 @@ GEM
activesupport (>= 5.0.0)
jsbundling-rails (1.3.0)
railties (>= 6.0.0)
+ json (2.7.2)
+ language_server-protocol (3.17.0.3)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
@@ -102,46 +115,52 @@ GEM
net-imap
net-pop
net-smtp
- marcel (1.0.2)
- method_source (1.0.0)
+ marcel (1.0.4)
+ method_source (1.1.0)
mini_mime (1.1.5)
- minitest (5.21.2)
+ minitest (5.23.0)
msgpack (1.7.2)
mysql2 (0.5.5)
- net-imap (0.4.9.1)
+ net-imap (0.4.11)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
- net-smtp (0.4.0.1)
+ net-smtp (0.5.0)
net-protocol
- nio4r (2.7.0)
- nokogiri (1.16.0-x86_64-darwin)
+ nio4r (2.7.3)
+ nokogiri (1.16.5-x86_64-darwin)
racc (~> 1.4)
+ nokogiri (1.16.5-x86_64-linux)
+ racc (~> 1.4)
+ parallel (1.24.0)
+ parser (3.3.1.0)
+ ast (~> 2.4.1)
+ racc
psych (5.1.2)
stringio
puma (6.4.2)
nio4r (~> 2.0)
racc (1.7.3)
- rack (2.2.8)
+ rack (2.2.9)
rack-test (2.1.0)
rack (>= 1.3)
- rails (7.0.8)
- actioncable (= 7.0.8)
- actionmailbox (= 7.0.8)
- actionmailer (= 7.0.8)
- actionpack (= 7.0.8)
- actiontext (= 7.0.8)
- actionview (= 7.0.8)
- activejob (= 7.0.8)
- activemodel (= 7.0.8)
- activerecord (= 7.0.8)
- activestorage (= 7.0.8)
- activesupport (= 7.0.8)
+ rails (7.0.8.1)
+ actioncable (= 7.0.8.1)
+ actionmailbox (= 7.0.8.1)
+ actionmailer (= 7.0.8.1)
+ actionpack (= 7.0.8.1)
+ actiontext (= 7.0.8.1)
+ actionview (= 7.0.8.1)
+ activejob (= 7.0.8.1)
+ activemodel (= 7.0.8.1)
+ activerecord (= 7.0.8.1)
+ activestorage (= 7.0.8.1)
+ activesupport (= 7.0.8.1)
bundler (>= 1.15.0)
- railties (= 7.0.8)
+ railties (= 7.0.8.1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
@@ -149,19 +168,77 @@ GEM
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
- railties (7.0.8)
- actionpack (= 7.0.8)
- activesupport (= 7.0.8)
+ railties (7.0.8.1)
+ actionpack (= 7.0.8.1)
+ activesupport (= 7.0.8.1)
method_source
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
- rake (13.1.0)
+ rainbow (3.1.1)
+ rake (13.2.1)
rdoc (6.6.2)
psych (>= 4.0.0)
redis (4.8.1)
+ regexp_parser (2.9.1)
reline (0.4.2)
io-console (~> 0.5)
+ rexml (3.2.8)
+ strscan (>= 3.0.9)
+ rspec-core (3.13.0)
+ rspec-support (~> 3.13.0)
+ rspec-expectations (3.13.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.13.0)
+ rspec-mocks (3.13.1)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.13.0)
+ rspec-rails (6.1.2)
+ actionpack (>= 6.1)
+ activesupport (>= 6.1)
+ railties (>= 6.1)
+ rspec-core (~> 3.13)
+ rspec-expectations (~> 3.13)
+ rspec-mocks (~> 3.13)
+ rspec-support (~> 3.13)
+ rspec-support (3.13.1)
+ rubocop (1.63.5)
+ json (~> 2.3)
+ language_server-protocol (>= 3.17.0)
+ parallel (~> 1.10)
+ parser (>= 3.3.0.2)
+ rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 1.8, < 3.0)
+ rexml (>= 3.2.5, < 4.0)
+ rubocop-ast (>= 1.31.1, < 2.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 2.4.0, < 3.0)
+ rubocop-ast (1.31.3)
+ parser (>= 3.3.1.0)
+ rubocop-capybara (2.20.0)
+ rubocop (~> 1.41)
+ rubocop-factory_bot (2.25.1)
+ rubocop (~> 1.41)
+ rubocop-migration (0.5.1)
+ activesupport
+ rubocop (>= 1.34)
+ rubocop-rails (>= 2.24)
+ rubocop-performance (1.21.0)
+ rubocop (>= 1.48.1, < 2.0)
+ rubocop-ast (>= 1.31.1, < 2.0)
+ rubocop-rails (2.24.1)
+ activesupport (>= 4.2.0)
+ rack (>= 1.1)
+ rubocop (>= 1.33.0, < 2.0)
+ rubocop-ast (>= 1.31.1, < 2.0)
+ rubocop-rspec (2.29.2)
+ rubocop (~> 1.40)
+ rubocop-capybara (~> 2.17)
+ rubocop-factory_bot (~> 2.22)
+ rubocop-rspec_rails (~> 2.28)
+ rubocop-rspec_rails (2.28.3)
+ rubocop (~> 1.40)
+ ruby-progressbar (1.13.0)
sprockets (4.2.1)
concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4)
@@ -172,7 +249,8 @@ GEM
stimulus-rails (1.3.3)
railties (>= 6.0.0)
stringio (3.1.0)
- thor (1.3.0)
+ strscan (3.1.0)
+ thor (1.3.1)
timeout (0.4.1)
turbo-rails (1.5.0)
actionpack (>= 6.0.0)
@@ -180,6 +258,7 @@ GEM
railties (>= 6.0.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
+ unicode-display_width (2.5.0)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
@@ -188,22 +267,33 @@ GEM
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
- zeitwerk (2.6.12)
+ zeitwerk (2.6.14)
PLATFORMS
x86_64-darwin-21
+ x86_64-linux
DEPENDENCIES
bootsnap
+ bundler-audit
cssbundling-rails
debug
+ factory_bot_rails
faker
+ interactor (~> 3.0)
jbuilder
jsbundling-rails
mysql2 (~> 0.5)
puma
rails (~> 7.0.8)
redis (~> 4.0)
+ rspec-rails (~> 6.1.0)
+ rubocop-factory_bot
+ rubocop-migration
+ rubocop-performance
+ rubocop-rails
+ rubocop-rspec
+ rubocop-rspec_rails
sprockets-rails
stimulus-rails
turbo-rails
diff --git a/README.md b/README.md
index ab870dc..c8f0735 100644
--- a/README.md
+++ b/README.md
@@ -11,10 +11,10 @@ Your primary task with this homework assignment is to resolve the user stories b
*Estimated time to completion: 3-4 hours*
## User Stories
-- [ ] The frontend is not properly fetching data, i.e. filters aren't being sent, requests are being sent too often, non-successful response codes aren't being handled, etc.
-- [ ] The backend is taking too long to return the expected data, i.e. filters aren't being applied, too much data is being returned, db performance isn't optimized, etc.
-- [ ] The confidence in our current functionality and ability to deploy additional changes is extremely low. Help us increase our confidence in an automated way.
-- [ ] The confidence in our security posture is also very low. Help us increase our confidence that we are running a securely designed application.
+- [x] The frontend is not properly fetching data, i.e. filters aren't being sent, requests are being sent too often, non-successful response codes aren't being handled, etc.
+- [x] The backend is taking too long to return the expected data, i.e. filters aren't being applied, too much data is being returned, db performance isn't optimized, etc.
+- [x] The confidence in our current functionality and ability to deploy additional changes is extremely low. Help us increase our confidence in an automated way.
+- [x] The confidence in our security posture is also very low. Help us increase our confidence that we are running a securely designed application.
## Assumptions
diff --git a/Rakefile b/Rakefile
index 9a5ea73..b771d66 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,6 +1,19 @@
+# frozen_string_literal: true
+
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
-require_relative "config/application"
+require_relative 'config/application'
Rails.application.load_tasks
+
+if Rails.env.development? || Rails.env.test?
+ require 'bundler/audit/task'
+ Bundler::Audit::Task.new
+
+ task audit: :environment do
+ require 'bundler/audit/cli'
+ Bundler::Audit::CLI.start %w[update]
+ Bundler::Audit::CLI.start %w[check]
+ end
+end
diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb
index d672697..9aec230 100644
--- a/app/channels/application_cable/channel.rb
+++ b/app/channels/application_cable/channel.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb
index 0ff5442..8d6c2a1 100644
--- a/app/channels/application_cable/connection.rb
+++ b/app/channels/application_cable/connection.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
diff --git a/app/controllers/api/v1/companies_controller.rb b/app/controllers/api/v1/companies_controller.rb
index 3ddb9c1..bae19ce 100644
--- a/app/controllers/api/v1/companies_controller.rb
+++ b/app/controllers/api/v1/companies_controller.rb
@@ -1,6 +1,24 @@
-class Api::V1::CompaniesController < ApplicationController
- def index
- companies = Company.all.order(created_at: :desc)
- render json: companies.as_json(include: :deals)
+# frozen_string_literal: true
+
+module Api
+ module V1
+ class CompaniesController < ApplicationController
+ def index
+ result = Companies::Finder.call(params: company_params)
+
+ if result.success?
+ companies = result.companies
+ render json: companies.as_json(include: :deals)
+ else
+ render json: { error: result.message }, status: :unprocessable_entity
+ end
+ end
+
+ private
+
+ def company_params
+ params.permit(:name, :industry, :min_employee, :minimum_deal_amount)
+ end
+ end
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 09705d1..7944f9f 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class ApplicationController < ActionController::Base
end
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 95f2992..a0ac1f8 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -1,4 +1,5 @@
+# frozen_string_literal: true
+
class HomeController < ApplicationController
- def index
- end
+ def index; end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index de6be79..15b06f0 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
module ApplicationHelper
end
diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb
index 23de56a..0362095 100644
--- a/app/helpers/home_helper.rb
+++ b/app/helpers/home_helper.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
module HomeHelper
end
diff --git a/app/javascript/components/Home.jsx b/app/javascript/components/Home.jsx
index 21026ba..e7bb906 100644
--- a/app/javascript/components/Home.jsx
+++ b/app/javascript/components/Home.jsx
@@ -1,5 +1,9 @@
import React, { useEffect, useState } from "react";
+// If I had more time, I would add the use of the state manager library here.
+// The author of the article on choosing a state manager
+// https://dev.to/nguyenhongphat0/react-state-management-in-2024-5e7l resonates with me.
+
export default () => {
// List of fetched companies
const [companies, setCompanies] = useState([]);
@@ -10,15 +14,59 @@ export default () => {
const [minEmployee, setMinEmployee] = useState("");
const [minimumDealAmount, setMinimumDealAmount] = useState("");
- // Fetch companies from API
- useEffect(() => {
- const url = "/api/v1/companies";
+ const [filtersChanged, setFiltersChanged] = useState(true);
+ const [error, setError] = useState(null);
+
+ // Update filter state setters to also set filtersChanged to true
+ const handleCompanyNameChange = (e) => {
+ setCompanyName(e.target.value);
+ setFiltersChanged(true);
+ };
+
+ const handleIndustryChange = (e) => {
+ setIndustry(e.target.value);
+ setFiltersChanged(true);
+ };
+
+ const handleMinEmployeeChange = (e) => {
+ setMinEmployee(e.target.value);
+ setFiltersChanged(true);
+ };
+
+ const handleMinimumDealAmountChange = (e) => {
+ setMinimumDealAmount(e.target.value);
+ setFiltersChanged(true);
+ };
+
+ const setCompanyListFromAPI = (url) => {
fetch(url)
.then((res) => {
+ if (!res.ok) {
+ console.error('API response was not ok (status code:', res.status, ')');
+ }
return res.json();
})
- .then((res) => setCompanies(res))
- }, [])
+ .then((res) => {
+ if (res.error) {
+ throw new Error(res.error);
+ } else {
+ setCompanies(res);
+ setError(null);
+ }
+ })
+ .catch((error) => {
+ setError(error.message);
+ });
+ };
+
+ // Fetch companies from API
+ useEffect(() => {
+ if (filtersChanged) {
+ const url = `/api/v1/companies?name=${companyName}&industry=${industry}&min_employee=${minEmployee}&minimum_deal_amount=${minimumDealAmount}`;
+ setCompanyListFromAPI(url);
+ setFiltersChanged(false);
+ };
+ }, [filtersChanged, companyName, industry, minEmployee, minimumDealAmount]);
return (
@@ -26,24 +74,26 @@ export default () => {
Companies
+ {error &&
{error}
}
+
- setCompanyName(e.target.value)} />
+ handleCompanyNameChange(e)} />
- setIndustry(e.target.value)} />
+ handleIndustryChange(e)} />
- setMinEmployee(e.target.value)} />
+ handleMinEmployeeChange(e)} />
- setMinimumDealAmount(e.target.value)} />
+ handleMinimumDealAmountChange(e)} />
diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb
index d394c3d..bef3959 100644
--- a/app/jobs/application_job.rb
+++ b/app/jobs/application_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
index 3c34c81..d84cb6e 100644
--- a/app/mailers/application_mailer.rb
+++ b/app/mailers/application_mailer.rb
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
class ApplicationMailer < ActionMailer::Base
- default from: "from@example.com"
- layout "mailer"
+ default from: 'from@example.com'
+ layout 'mailer'
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index b63caeb..08dc537 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
end
diff --git a/app/models/company.rb b/app/models/company.rb
index a39684f..8117421 100644
--- a/app/models/company.rb
+++ b/app/models/company.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Company < ApplicationRecord
- has_many :deals
+ has_many :deals, dependent: :destroy
end
diff --git a/app/models/deal.rb b/app/models/deal.rb
index 6382fd2..7dad9ce 100644
--- a/app/models/deal.rb
+++ b/app/models/deal.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Deal < ApplicationRecord
belongs_to :company
end
diff --git a/app/services/companies/finder.rb b/app/services/companies/finder.rb
new file mode 100644
index 0000000..948d1a3
--- /dev/null
+++ b/app/services/companies/finder.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+module Companies
+ class Finder
+ include Interactor
+
+ delegate :params, to: :context
+
+ def call
+ companies = Company.all
+ companies = minimum_deal_amount_filter(companies)
+ companies = name_filter(companies)
+ companies = industry_filter(companies)
+ # If I had more time, I would move Industry to a separate model, belongs_to Company
+ # (or with polymorphic relation if Industry can be not only Companies,
+ # but for example Vendors and so on),
+ # make a separate endpoint to get the list of industries on the frontend
+ # and redo the filter to search for industry matches by the ID of the related entity;
+
+ companies = min_employee_filter(companies)
+
+ context.companies = companies
+ end
+
+ private
+
+ def name
+ params[:name]
+ end
+
+ def industry
+ params[:industry]
+ end
+
+ def min_employee
+ params[:min_employee]
+ end
+
+ def minimum_deal_amount
+ params[:minimum_deal_amount]
+ end
+
+ def minimum_deal_amount_filter(companies) # rubocop:disable Metrics/MethodLength
+ if minimum_deal_amount.present?
+ context.fail!(message: 'Minimum deal amount should be an integer') unless numeric?(minimum_deal_amount)
+
+ sql = <<~SQL.squish
+ select
+ companies.*
+ from companies
+ left join deals on deals.company_id=companies.id
+ group by companies.id
+ having COALESCE(sum(deals.amount),0) >= #{minimum_deal_amount}
+ SQL
+ companies.select('*').from("(#{sql}) AS companies")
+ else
+ companies
+ end
+ end
+
+ def name_filter(companies)
+ if name.present?
+ companies.where('LOWER(name) LIKE LOWER(?)', "%#{name}%")
+ else
+ companies
+ end
+ end
+
+ def industry_filter(companies)
+ if industry.present?
+ companies.where('LOWER(industry) LIKE LOWER(?)', "%#{industry}%")
+ else
+ companies
+ end
+ end
+
+ def min_employee_filter(companies)
+ if min_employee.present?
+ context.fail!(message: 'Minimum employee count should be an integer') unless numeric?(min_employee)
+
+ companies.where('employee_count >= ?', min_employee)
+ else
+ companies
+ end
+ end
+
+ # If I had more time, I would add some kind of service-wide library for input validation
+ # like https://github.com/dry-rb/dry-validation
+ def numeric?(string)
+ string.to_i.to_s == string || string.to_f.to_s == string
+ end
+ end
+end
diff --git a/config.ru b/config.ru
index 4a3c09a..6dc8321 100644
--- a/config.ru
+++ b/config.ru
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
# This file is used by Rack-based servers to start the application.
-require_relative "config/environment"
+require_relative 'config/environment'
run Rails.application
Rails.application.load_server
diff --git a/config/application.rb b/config/application.rb
index 5fe8c8f..890fba0 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -1,17 +1,19 @@
-require_relative "boot"
+# frozen_string_literal: true
-require "rails"
+require_relative 'boot'
+
+require 'rails'
# Pick the frameworks you want:
-require "active_model/railtie"
-require "active_job/railtie"
-require "active_record/railtie"
-require "active_storage/engine"
-require "action_controller/railtie"
-require "action_mailer/railtie"
-require "action_mailbox/engine"
-require "action_text/engine"
-require "action_view/railtie"
-require "action_cable/engine"
+require 'active_model/railtie'
+require 'active_job/railtie'
+require 'active_record/railtie'
+require 'active_storage/engine'
+require 'action_controller/railtie'
+require 'action_mailer/railtie'
+require 'action_mailbox/engine'
+require 'action_text/engine'
+require 'action_view/railtie'
+require 'action_cable/engine'
# require "rails/test_unit/railtie"
# Require the gems listed in Gemfile, including any gems
diff --git a/config/boot.rb b/config/boot.rb
index 988a5dd..c04863f 100644
--- a/config/boot.rb
+++ b/config/boot.rb
@@ -1,4 +1,6 @@
-ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+# frozen_string_literal: true
-require "bundler/setup" # Set up gems listed in the Gemfile.
-require "bootsnap/setup" # Speed up boot time by caching expensive operations.
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+
+require 'bundler/setup' # Set up gems listed in the Gemfile.
+require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
diff --git a/config/database.yml b/config/database.yml
index e5abf3f..7059849 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -11,11 +11,14 @@
#
default: &default
adapter: mysql2
+ # If I had more time, I would move the project database to PostgreSQL.
+ # In my opinion postgres has a more developed ecosystem of solutions for query monitoring and optimisation.
encoding: utf8mb4
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
- username: root
- password:
- socket: /tmp/mysql.sock
+ username: rails_user
+ password: password
+ host: 127.0.0.1
+ port: 3307
development:
<<: *default
diff --git a/config/environment.rb b/config/environment.rb
index cac5315..d5abe55 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
# Load the Rails application.
-require_relative "application"
+require_relative 'application'
# Initialize the Rails application.
Rails.application.initialize!
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 8500f45..84a57f4 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -1,4 +1,6 @@
-require "active_support/core_ext/integer/time"
+# frozen_string_literal: true
+
+require 'active_support/core_ext/integer/time'
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
@@ -19,13 +21,13 @@
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
- if Rails.root.join("tmp/caching-dev.txt").exist?
+ if Rails.root.join('tmp/caching-dev.txt').exist?
config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true
config.cache_store = :memory_store
config.public_file_server.headers = {
- "Cache-Control" => "public, max-age=#{2.days.to_i}"
+ 'Cache-Control' => "public, max-age=#{2.days.to_i}"
}
else
config.action_controller.perform_caching = false
diff --git a/config/environments/production.rb b/config/environments/production.rb
index b88be16..48612bd 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -1,4 +1,6 @@
-require "active_support/core_ext/integer/time"
+# frozen_string_literal: true
+
+require 'active_support/core_ext/integer/time'
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
@@ -22,7 +24,7 @@
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
- config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
+ config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
# Compress CSS using a preprocessor.
# config.assets.css_compressor = :sass
@@ -53,7 +55,7 @@
config.log_level = :info
# Prepend all log lines with the following tags.
- config.log_tags = [ :request_id ]
+ config.log_tags = [:request_id]
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
@@ -76,14 +78,14 @@
config.active_support.report_deprecations = false
# Use default logging formatter so that PID and timestamp are not suppressed.
- config.log_formatter = ::Logger::Formatter.new
+ config.log_formatter = Logger::Formatter.new
# Use a different logger for distributed setups.
# require "syslog/logger"
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
- if ENV["RAILS_LOG_TO_STDOUT"].present?
- logger = ActiveSupport::Logger.new(STDOUT)
+ if ENV['RAILS_LOG_TO_STDOUT'].present?
+ logger = ActiveSupport::Logger.new($stdout)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
end
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 6ea4d1e..8f3f63c 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,4 +1,6 @@
-require "active_support/core_ext/integer/time"
+# frozen_string_literal: true
+
+require 'active_support/core_ext/integer/time'
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
@@ -14,12 +16,12 @@
# Eager loading loads your whole application. When running a single test locally,
# this probably isn't necessary. It's a good idea to do in a continuous integration
# system, or in some way before deploying your code.
- config.eager_load = ENV["CI"].present?
+ config.eager_load = ENV['CI'].present?
# Configure public file server for tests with Cache-Control for performance.
config.public_file_server.enabled = true
config.public_file_server.headers = {
- "Cache-Control" => "public, max-age=#{1.hour.to_i}"
+ 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
}
# Show full error reports and disable caching.
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
index 58275b4..a9aa86b 100644
--- a/config/initializers/assets.rb
+++ b/config/initializers/assets.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
-Rails.application.config.assets.version = "1.0"
+Rails.application.config.assets.version = '1.0'
# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
-Rails.application.config.assets.paths << Rails.root.join("node_modules/bootstrap-icons/font")
+Rails.application.config.assets.paths << Rails.root.join('node_modules/bootstrap-icons/font')
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index 54f47cf..53538c1 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Define an application-wide content security policy.
diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb
index adc6568..3df77c5 100644
--- a/config/initializers/filter_parameter_logging.rb
+++ b/config/initializers/filter_parameter_logging.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Configure parameters to be filtered from the log file. Use this to limit dissemination of
# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
# notations and behaviors.
-Rails.application.config.filter_parameters += [
- :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
+Rails.application.config.filter_parameters += %i[
+ passw secret token _key crypt salt certificate otp ssn
]
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index 3860f65..9e049dc 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Add new inflection rules using the following format. Inflections
diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb
index 00f64d7..810aade 100644
--- a/config/initializers/permissions_policy.rb
+++ b/config/initializers/permissions_policy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Define an application-wide HTTP permissions policy. For further
# information see https://developers.google.com/web/updates/2018/06/feature-policy
#
diff --git a/config/puma.rb b/config/puma.rb
index daaf036..1713441 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -1,28 +1,30 @@
+# frozen_string_literal: true
+
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
-max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
-min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
+max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
+min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }
threads min_threads_count, max_threads_count
# Specifies the `worker_timeout` threshold that Puma will use to wait before
# terminating a worker in development environments.
#
-worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
+worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development'
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
-port ENV.fetch("PORT") { 3000 }
+port ENV.fetch('PORT', 3000)
# Specifies the `environment` that Puma will run in.
#
-environment ENV.fetch("RAILS_ENV") { "development" }
+environment ENV.fetch('RAILS_ENV', 'development')
# Specifies the `pidfile` that Puma will use.
-pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
+pidfile ENV.fetch('PIDFILE', 'tmp/pids/server.pid')
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together
diff --git a/config/routes.rb b/config/routes.rb
index b9d2b6d..3efc291 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
Rails.application.routes.draw do
namespace :api, constraints: { format: :json } do
namespace :v1 do
diff --git a/db/migrate/20240122175845_create_companies.rb b/db/migrate/20240122175845_create_companies.rb
index 0762085..3fb2d16 100644
--- a/db/migrate/20240122175845_create_companies.rb
+++ b/db/migrate/20240122175845_create_companies.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
class CreateCompanies < ActiveRecord::Migration[7.0]
def change
create_table :companies do |t|
- t.string :name
+ t.string :name # rubocop:disable Migration/ReservedWordMysql
t.integer :employee_count
t.string :industry
diff --git a/db/migrate/20240122203751_create_deals.rb b/db/migrate/20240122203751_create_deals.rb
index e0dee62..822e2c1 100644
--- a/db/migrate/20240122203751_create_deals.rb
+++ b/db/migrate/20240122203751_create_deals.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
class CreateDeals < ActiveRecord::Migration[7.0]
def change
create_table :deals do |t|
- t.string :name
+ t.string :name # rubocop:disable Migration/ReservedWordMysql
t.integer :amount
- t.string :status
+ t.string :status # rubocop:disable Migration/ReservedWordMysql
t.references :company, null: false, foreign_key: true
t.timestamps
diff --git a/db/seeds.rb b/db/seeds.rb
index 18d8da9..3649fe6 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
#
-10.times do |i|
+10.times do |_i|
Company.create(
name: Faker::Company.name,
industry: Faker::Company.industry,
@@ -10,13 +12,13 @@
)
end
-statuses = ["pending", "won", "lost"]
+statuses = %w[pending won lost]
30.times do |i|
Deal.create(
name: "Deal #{i}",
status: statuses.sample,
amount: rand(10..1000),
- company_id: Company.order("RAND()").first.id
+ company_id: Company.order('RAND()').first.id
)
end
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..6bb0954
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,970 @@
+{
+ "name": "app",
+ "requires": true,
+ "lockfileVersion": 1,
+ "dependencies": {
+ "@esbuild/aix-ppc64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz",
+ "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==",
+ "optional": true
+ },
+ "@esbuild/android-arm": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz",
+ "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==",
+ "optional": true
+ },
+ "@esbuild/android-arm64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz",
+ "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==",
+ "optional": true
+ },
+ "@esbuild/android-x64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz",
+ "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==",
+ "optional": true
+ },
+ "@esbuild/darwin-arm64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz",
+ "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==",
+ "optional": true
+ },
+ "@esbuild/darwin-x64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz",
+ "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==",
+ "optional": true
+ },
+ "@esbuild/freebsd-arm64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz",
+ "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==",
+ "optional": true
+ },
+ "@esbuild/freebsd-x64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz",
+ "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==",
+ "optional": true
+ },
+ "@esbuild/linux-arm": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz",
+ "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==",
+ "optional": true
+ },
+ "@esbuild/linux-arm64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz",
+ "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==",
+ "optional": true
+ },
+ "@esbuild/linux-ia32": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz",
+ "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==",
+ "optional": true
+ },
+ "@esbuild/linux-loong64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz",
+ "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==",
+ "optional": true
+ },
+ "@esbuild/linux-mips64el": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz",
+ "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==",
+ "optional": true
+ },
+ "@esbuild/linux-ppc64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz",
+ "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==",
+ "optional": true
+ },
+ "@esbuild/linux-riscv64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz",
+ "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==",
+ "optional": true
+ },
+ "@esbuild/linux-s390x": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz",
+ "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==",
+ "optional": true
+ },
+ "@esbuild/linux-x64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz",
+ "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==",
+ "optional": true
+ },
+ "@esbuild/netbsd-x64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz",
+ "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==",
+ "optional": true
+ },
+ "@esbuild/openbsd-x64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz",
+ "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==",
+ "optional": true
+ },
+ "@esbuild/sunos-x64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz",
+ "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==",
+ "optional": true
+ },
+ "@esbuild/win32-arm64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz",
+ "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==",
+ "optional": true
+ },
+ "@esbuild/win32-ia32": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz",
+ "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==",
+ "optional": true
+ },
+ "@esbuild/win32-x64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz",
+ "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==",
+ "optional": true
+ },
+ "@hotwired/stimulus": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz",
+ "integrity": "sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A=="
+ },
+ "@hotwired/turbo": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/@hotwired/turbo/-/turbo-7.3.0.tgz",
+ "integrity": "sha512-Dcu+NaSvHLT7EjrDrkEmH4qET2ZJZ5IcCWmNXxNQTBwlnE5tBZfN6WxZ842n5cHV52DH/AKNirbPBtcEXDLW4g=="
+ },
+ "@hotwired/turbo-rails": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/@hotwired/turbo-rails/-/turbo-rails-7.3.0.tgz",
+ "integrity": "sha512-fvhO64vp/a2UVQ3jue9WTc2JisMv9XilIC7ViZmXAREVwiQ2S4UC7Go8f9A1j4Xu7DBI6SbFdqILk5ImqVoqyA==",
+ "requires": {
+ "@hotwired/turbo": "^7.3.0",
+ "@rails/actioncable": "^7.0"
+ }
+ },
+ "@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "requires": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ }
+ },
+ "@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="
+ },
+ "@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "requires": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ }
+ },
+ "@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
+ },
+ "@rails/actioncable": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/@rails/actioncable/-/actioncable-7.1.3.tgz",
+ "integrity": "sha512-ojNvnoZtPN0pYvVFtlO7dyEN9Oml1B6IDM+whGKVak69MMYW99lC2NOWXWeE3bmwEydbP/nn6ERcpfjHVjYQjA=="
+ },
+ "@remix-run/router": {
+ "version": "1.14.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.2.tgz",
+ "integrity": "sha512-ACXpdMM9hmKZww21yEqWwiLws/UPLhNKvimN8RrYSqPSvB3ov7sLvAcfvaxePeLvccTQKGdkDIhLYApZVDFuKg=="
+ },
+ "@sindresorhus/merge-streams": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz",
+ "integrity": "sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw=="
+ },
+ "abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+ },
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "autoprefixer": {
+ "version": "10.4.17",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz",
+ "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==",
+ "requires": {
+ "browserslist": "^4.22.2",
+ "caniuse-lite": "^1.0.30001578",
+ "fraction.js": "^4.3.7",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.0.0",
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="
+ },
+ "bootstrap": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.2.tgz",
+ "integrity": "sha512-D32nmNWiQHo94BKHLmOrdjlL05q1c8oxbtBphQFb9Z5to6eGRDCm0QgeaZ4zFBHzfg2++rqa2JkqCcxDy0sH0g=="
+ },
+ "bootstrap-icons": {
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz",
+ "integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww=="
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "browserslist": {
+ "version": "4.22.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
+ "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==",
+ "requires": {
+ "caniuse-lite": "^1.0.30001565",
+ "electron-to-chromium": "^1.4.601",
+ "node-releases": "^2.0.14",
+ "update-browserslist-db": "^1.0.13"
+ }
+ },
+ "caniuse-lite": {
+ "version": "1.0.30001579",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz",
+ "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA=="
+ },
+ "chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "requires": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "fsevents": "~2.3.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ }
+ },
+ "cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+ },
+ "debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "dependency-graph": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz",
+ "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg=="
+ },
+ "electron-to-chromium": {
+ "version": "1.4.769",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.769.tgz",
+ "integrity": "sha512-bZu7p623NEA2rHTc9K1vykl57ektSPQYFFqQir8BOYf6EKOB+yIsbFB9Kpm7Cgt6tsLr9sRkqfqSZUw7LP1XxQ=="
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "esbuild": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz",
+ "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==",
+ "requires": {
+ "@esbuild/aix-ppc64": "0.19.11",
+ "@esbuild/android-arm": "0.19.11",
+ "@esbuild/android-arm64": "0.19.11",
+ "@esbuild/android-x64": "0.19.11",
+ "@esbuild/darwin-arm64": "0.19.11",
+ "@esbuild/darwin-x64": "0.19.11",
+ "@esbuild/freebsd-arm64": "0.19.11",
+ "@esbuild/freebsd-x64": "0.19.11",
+ "@esbuild/linux-arm": "0.19.11",
+ "@esbuild/linux-arm64": "0.19.11",
+ "@esbuild/linux-ia32": "0.19.11",
+ "@esbuild/linux-loong64": "0.19.11",
+ "@esbuild/linux-mips64el": "0.19.11",
+ "@esbuild/linux-ppc64": "0.19.11",
+ "@esbuild/linux-riscv64": "0.19.11",
+ "@esbuild/linux-s390x": "0.19.11",
+ "@esbuild/linux-x64": "0.19.11",
+ "@esbuild/netbsd-x64": "0.19.11",
+ "@esbuild/openbsd-x64": "0.19.11",
+ "@esbuild/sunos-x64": "0.19.11",
+ "@esbuild/win32-arm64": "0.19.11",
+ "@esbuild/win32-ia32": "0.19.11",
+ "@esbuild/win32-x64": "0.19.11"
+ }
+ },
+ "escalade": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
+ "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA=="
+ },
+ "fast-glob": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+ "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+ "requires": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ }
+ },
+ "fastq": {
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+ "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
+ "requires": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "fraction.js": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="
+ },
+ "fs-extra": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
+ "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
+ "requires": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ }
+ },
+ "fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "optional": true
+ },
+ "get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
+ },
+ "get-stdin": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz",
+ "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA=="
+ },
+ "glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "globby": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.0.tgz",
+ "integrity": "sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==",
+ "requires": {
+ "@sindresorhus/merge-streams": "^1.0.0",
+ "fast-glob": "^3.3.2",
+ "ignore": "^5.2.4",
+ "path-type": "^5.0.0",
+ "slash": "^5.1.0",
+ "unicorn-magic": "^0.1.0"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
+ },
+ "ignore": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
+ "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw=="
+ },
+ "ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="
+ },
+ "immutable": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz",
+ "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA=="
+ },
+ "is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
+ },
+ "is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "requires": {
+ "graceful-fs": "^4.1.6",
+ "universalify": "^2.0.0"
+ }
+ },
+ "lilconfig": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
+ "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ=="
+ },
+ "loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ },
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
+ },
+ "micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "requires": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ }
+ },
+ "minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "nanoid": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="
+ },
+ "node-releases": {
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
+ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
+ },
+ "nodemon": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.3.tgz",
+ "integrity": "sha512-7jH/NXbFPxVaMwmBCC2B9F/V6X1VkEdNgx3iu9jji8WxWcvhMWkmhNWhI5077zknOnZnBzba9hZP6bCPJLSReQ==",
+ "requires": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ }
+ },
+ "nopt": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+ "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
+ "requires": {
+ "abbrev": "1"
+ }
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
+ },
+ "normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="
+ },
+ "path-type": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz",
+ "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg=="
+ },
+ "picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+ },
+ "picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
+ },
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="
+ },
+ "postcss": {
+ "version": "8.4.33",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
+ "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
+ "requires": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "postcss-cli": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-11.0.0.tgz",
+ "integrity": "sha512-xMITAI7M0u1yolVcXJ9XTZiO9aO49mcoKQy6pCDFdMh9kGqhzLVpWxeD/32M/QBmkhcGypZFFOLNLmIW4Pg4RA==",
+ "requires": {
+ "chokidar": "^3.3.0",
+ "dependency-graph": "^0.11.0",
+ "fs-extra": "^11.0.0",
+ "get-stdin": "^9.0.0",
+ "globby": "^14.0.0",
+ "picocolors": "^1.0.0",
+ "postcss-load-config": "^5.0.0",
+ "postcss-reporter": "^7.0.0",
+ "pretty-hrtime": "^1.0.3",
+ "read-cache": "^1.0.0",
+ "slash": "^5.0.0",
+ "yargs": "^17.0.0"
+ }
+ },
+ "postcss-load-config": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-5.0.2.tgz",
+ "integrity": "sha512-Q8QR3FYbqOKa0bnC1UQ2bFq9/ulHX5Bi34muzitMr8aDtUelO5xKeJEYC/5smE0jNE9zdB/NBnOwXKexELbRlw==",
+ "requires": {
+ "lilconfig": "^3.0.0",
+ "yaml": "^2.3.4"
+ }
+ },
+ "postcss-reporter": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.1.0.tgz",
+ "integrity": "sha512-/eoEylGWyy6/DOiMP5lmFRdmDKThqgn7D6hP2dXKJI/0rJSO1ADFNngZfDzxL0YAxFvws+Rtpuji1YIHj4mySA==",
+ "requires": {
+ "picocolors": "^1.0.0",
+ "thenby": "^1.3.4"
+ }
+ },
+ "postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
+ },
+ "pretty-hrtime": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz",
+ "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A=="
+ },
+ "pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="
+ },
+ "queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
+ },
+ "react": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "requires": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "react-dom": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
+ "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.0"
+ }
+ },
+ "react-router": {
+ "version": "6.21.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.3.tgz",
+ "integrity": "sha512-a0H638ZXULv1OdkmiK6s6itNhoy33ywxmUFT/xtSoVyf9VnC7n7+VT4LjVzdIHSaF5TIh9ylUgxMXksHTgGrKg==",
+ "requires": {
+ "@remix-run/router": "1.14.2"
+ }
+ },
+ "react-router-dom": {
+ "version": "6.21.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.21.3.tgz",
+ "integrity": "sha512-kNzubk7n4YHSrErzjLK72j0B5i969GsuCGazRl3G6j1zqZBLjuSlYBdVdkDOgzGdPIffUOc9nmgiadTEVoq91g==",
+ "requires": {
+ "@remix-run/router": "1.14.2",
+ "react-router": "6.21.3"
+ }
+ },
+ "read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "requires": {
+ "pify": "^2.3.0"
+ }
+ },
+ "readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
+ },
+ "reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="
+ },
+ "run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "requires": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "sass": {
+ "version": "1.70.0",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz",
+ "integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==",
+ "requires": {
+ "chokidar": ">=3.0.0 <4.0.0",
+ "immutable": "^4.0.0",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ }
+ },
+ "scheduler": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
+ "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+ "requires": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "requires": {
+ "semver": "^7.5.3"
+ }
+ },
+ "slash": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
+ "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="
+ },
+ "source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "thenby": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz",
+ "integrity": "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ=="
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "touch": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
+ "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
+ "requires": {
+ "nopt": "~1.0.10"
+ }
+ },
+ "undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
+ },
+ "unicorn-magic": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
+ "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="
+ },
+ "universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="
+ },
+ "update-browserslist-db": {
+ "version": "1.0.16",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz",
+ "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==",
+ "requires": {
+ "escalade": "^3.1.2",
+ "picocolors": "^1.0.1"
+ },
+ "dependencies": {
+ "escalade": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
+ "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA=="
+ },
+ "picocolors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
+ "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
+ }
+ }
+ },
+ "wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "yaml": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
+ "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA=="
+ },
+ "yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "requires": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ }
+ },
+ "yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="
+ }
+ }
+}
diff --git a/spec/factories/companies.rb b/spec/factories/companies.rb
new file mode 100644
index 0000000..15682d4
--- /dev/null
+++ b/spec/factories/companies.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :company do
+ name { Faker::Company.name }
+ employee_count { rand(10..1000) }
+ industry { Faker::Company.industry }
+ end
+end
diff --git a/spec/factories/deals.rb b/spec/factories/deals.rb
new file mode 100644
index 0000000..4a7fe65
--- /dev/null
+++ b/spec/factories/deals.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :deal do
+ sequence(:name) { |n| "Deal #{n}" }
+ amount { rand(10..1000) }
+ status { %w[pending won lost].sample }
+ company
+ end
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
new file mode 100644
index 0000000..f2e7996
--- /dev/null
+++ b/spec/rails_helper.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+# This file is copied to spec/ when you run 'rails generate rspec:install'
+require 'spec_helper'
+ENV['RAILS_ENV'] ||= 'test'
+require_relative '../config/environment'
+# Prevent database truncation if the environment is production
+abort('The Rails environment is running in production mode!') if Rails.env.production?
+require 'rspec/rails'
+
+Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
+
+begin
+ ActiveRecord::Migration.maintain_test_schema!
+rescue ActiveRecord::PendingMigrationError => e
+ abort e.to_s.strip
+end
+RSpec.configure do |config|
+ # config.include Requests::JsonHelpers, type: :controller
+ config.include Requests::JsonHelpers, type: :request
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
+ config.fixture_path = Rails.root.join('spec/fixtures')
+
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
+ # examples within a transaction, remove the following line or assign false
+ # instead of true.
+ config.use_transactional_fixtures = true
+
+ # You can uncomment this line to turn off ActiveRecord support entirely.
+ # config.use_active_record = false
+
+ # RSpec Rails can automatically mix in different behaviours to your tests
+ # based on their file location, for example enabling you to call `get` and
+ # `post` in specs under `spec/controllers`.
+ #
+ # You can disable this behaviour by removing the line below, and instead
+ # explicitly tag your specs with their type, e.g.:
+ #
+ # RSpec.describe UsersController, type: :controller do
+ # # ...
+ # end
+ #
+ # The different available types are documented in the features, such as in
+ # https://rspec.info/features/6-0/rspec-rails
+ config.infer_spec_type_from_file_location!
+
+ # Filter lines from Rails gems in backtraces.
+ config.filter_rails_from_backtrace!
+ # arbitrary gems may also be filtered via:
+ # config.filter_gems_from_backtrace("gem name")
+end
diff --git a/spec/requests/filter_companies_spec.rb b/spec/requests/filter_companies_spec.rb
new file mode 100644
index 0000000..c7e57cf
--- /dev/null
+++ b/spec/requests/filter_companies_spec.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'FilterCompanies', type: :request do
+ describe 'GET /api/v1/companies' do
+ subject(:companies_filter_request) { get api_v1_companies_path, params: }
+
+ context 'when there are no companies' do
+ let(:params) {} # rubocop:disable Lint/EmptyBlock
+
+ it 'works!' do
+ subject
+ expect(response).to have_http_status(:ok)
+ end
+ end
+
+ context 'when there are companies' do
+ let!(:company1) do
+ create(:company, name: 'Company1', industry: 'Cosmetics', employee_count: 1000)
+ end
+ let!(:company2) do
+ create(:company, name: 'Company2', industry: 'Test', employee_count: 2000)
+ end
+
+ context 'when filtering by name' do
+ let(:params) { { name: 'pany1' } }
+
+ it 'works!' do
+ subject
+ expect(response).to have_http_status(:ok)
+ names = parsed_json.pluck(:name)
+ expect(names).to include('Company1')
+ expect(names).not_to include('Company2')
+ end
+ end
+
+ context 'when filtering by name and industry' do
+ let(:params) { { name: 'Compa', industry: 'test' } }
+
+ it 'works!' do
+ subject
+ expect(response).to have_http_status(:ok)
+ names = parsed_json.pluck(:name)
+
+ expect(names).to include('Company2')
+ expect(names).not_to include('Company1')
+ end
+ end
+
+ context 'when filtering by min_employee' do
+ let(:params) { { min_employee: 1500 } }
+
+ it 'works!' do
+ subject
+ expect(response).to have_http_status(:ok)
+ names = parsed_json.pluck(:name)
+ expect(names).to include('Company2')
+ expect(names).not_to include('Company1')
+ end
+ end
+
+ context 'when filtering by minimum_deal_amount' do
+ let!(:deals1) { create_list(:deal, 5, company: company1, amount: 100) }
+ let!(:deals2) { create_list(:deal, 4, company: company2, amount: 300) }
+ let(:params) { { minimum_deal_amount: 1000 } }
+
+ it 'works!' do
+ subject
+ expect(response).to have_http_status(:ok)
+ names = parsed_json.pluck(:name)
+ expect(names).to include('Company2')
+ expect(names).not_to include('Company1')
+ end
+ end
+
+ context 'when filtering by name, industry, min_employee, and minimum_deal_amount' do
+ let!(:deals1) { create_list(:deal, 5, company: company1, amount: 100) }
+ let!(:deals2) { create_list(:deal, 4, company: company2, amount: 300) }
+ let(:params) { { name: 'Compa', industry: 'test', min_employee: 1500, minimum_deal_amount: 1000 } }
+
+ it 'works!' do
+ subject
+ expect(response).to have_http_status(:ok)
+ names = parsed_json.pluck(:name)
+ expect(names).to include('Company2')
+ expect(names).not_to include('Company1')
+ end
+ end
+ end
+
+ context 'when filtering by invalid minimum_deal_amount' do
+ let(:params) { { minimum_deal_amount: 'invalid' } }
+
+ it 'works!' do
+ subject
+ expect(response).to have_http_status(:unprocessable_entity)
+ expect(parsed_json).to have_key(:error)
+ expect(parsed_json[:error]).to eq('Minimum deal amount should be an integer')
+ end
+ end
+
+ context 'when filtering by invalid min_employee' do
+ let(:params) { { min_employee: 'invalid' } }
+
+ it 'works!' do
+ subject
+ expect(response).to have_http_status(:unprocessable_entity)
+ expect(parsed_json).to have_key(:error)
+ expect(parsed_json[:error]).to eq('Minimum employee count should be an integer')
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..409c64b
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+# This file was generated by the `rails generate rspec:install` command. Conventionally, all
+# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
+# The generated `.rspec` file contains `--require spec_helper` which will cause
+# this file to always be loaded, without a need to explicitly require it in any
+# files.
+#
+# Given that it is always loaded, you are encouraged to keep this file as
+# light-weight as possible. Requiring heavyweight dependencies from this file
+# will add to the boot time of your test suite on EVERY test run, even for an
+# individual file that may not need all of that loaded. Instead, consider making
+# a separate helper file that requires the additional dependencies and performs
+# the additional setup, and require it from the spec files that actually need
+# it.
+#
+# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+RSpec.configure do |config|
+ # rspec-expectations config goes here. You can use an alternate
+ # assertion/expectation library such as wrong or the stdlib/minitest
+ # assertions if you prefer.
+ config.expect_with :rspec do |expectations|
+ # This option will default to `true` in RSpec 4. It makes the `description`
+ # and `failure_message` of custom matchers include text for helper methods
+ # defined using `chain`, e.g.:
+ # be_bigger_than(2).and_smaller_than(4).description
+ # # => "be bigger than 2 and smaller than 4"
+ # ...rather than:
+ # # => "be bigger than 2"
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ # rspec-mocks config goes here. You can use an alternate test double
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
+ config.mock_with :rspec do |mocks|
+ # Prevents you from mocking or stubbing a method that does not exist on
+ # a real object. This is generally recommended, and will default to
+ # `true` in RSpec 4.
+ mocks.verify_partial_doubles = true
+ end
+
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
+ # have no way to turn it off -- the option exists only for backwards
+ # compatibility in RSpec 3). It causes shared context metadata to be
+ # inherited by the metadata hash of host groups and examples, rather than
+ # triggering implicit auto-inclusion in groups with matching metadata.
+ config.shared_context_metadata_behavior = :apply_to_host_groups
+
+ # The settings below are suggested to provide a good initial experience
+ # with RSpec, but feel free to customize to your heart's content.
+ # # This allows you to limit a spec run to individual examples or groups
+ # # you care about by tagging them with `:focus` metadata. When nothing
+ # # is tagged with `:focus`, all examples get run. RSpec also provides
+ # # aliases for `it`, `describe`, and `context` that include `:focus`
+ # # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
+ # config.filter_run_when_matching :focus
+ #
+ # # Allows RSpec to persist some state between runs in order to support
+ # # the `--only-failures` and `--next-failure` CLI options. We recommend
+ # # you configure your source control system to ignore this file.
+ # config.example_status_persistence_file_path = "spec/examples.txt"
+ #
+ # # Limits the available syntax to the non-monkey patched syntax that is
+ # # recommended. For more details, see:
+ # # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
+ # config.disable_monkey_patching!
+ #
+ # # Many RSpec users commonly either run the entire suite or an individual
+ # # file, and it's useful to allow more verbose output when running an
+ # # individual spec file.
+ # if config.files_to_run.one?
+ # # Use the documentation formatter for detailed output,
+ # # unless a formatter has already been configured
+ # # (e.g. via a command-line flag).
+ # config.default_formatter = "doc"
+ # end
+ #
+ # # Print the 10 slowest examples and example groups at the
+ # # end of the spec run, to help surface which specs are running
+ # # particularly slow.
+ # config.profile_examples = 10
+ #
+ # # Run specs in random order to surface order dependencies. If you find an
+ # # order dependency and want to debug it, you can fix the order by providing
+ # # the seed, which is printed after each run.
+ # # --seed 1234
+ # config.order = :random
+ #
+ # # Seed global randomization in this process using the `--seed` CLI option.
+ # # Setting this allows you to use `--seed` to deterministically reproduce
+ # # test failures related to randomization by passing the same `--seed` value
+ # # as the one that triggered the failure.
+ # Kernel.srand config.seed
+end
diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb
new file mode 100644
index 0000000..2e7665c
--- /dev/null
+++ b/spec/support/factory_bot.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ config.include FactoryBot::Syntax::Methods
+end
diff --git a/spec/support/json_hepler.rb b/spec/support/json_hepler.rb
new file mode 100644
index 0000000..1873ce7
--- /dev/null
+++ b/spec/support/json_hepler.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Requests
+ module JsonHelpers
+ def parsed_json
+ JSON.parse(response.body, symbolize_names: true)
+ end
+ end
+end