├── log └── .keep ├── storage └── .keep ├── tmp └── .keep ├── vendor └── .keep ├── .ruby-version ├── app ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── auth_token.rb │ └── user.rb ├── views │ └── layouts │ │ ├── mailer.text.erb │ │ └── mailer.html.erb ├── graphql │ ├── types │ │ ├── base_enum.rb │ │ ├── base_union.rb │ │ ├── base_scalar.rb │ │ ├── base_argument.rb │ │ ├── base_interface.rb │ │ ├── query_type.rb │ │ ├── base_input_object.rb │ │ ├── base_field.rb │ │ ├── subscription_type.rb │ │ ├── custom_types │ │ │ └── user_type.rb │ │ ├── base_object.rb │ │ └── mutation_type.rb │ ├── queries │ │ ├── base_query.rb │ │ └── user.rb │ ├── mutations │ │ ├── base_mutation.rb │ │ └── user_mutations │ │ │ ├── delete_user_mutation.rb │ │ │ ├── update_user_mutation.rb │ │ │ ├── create_user_mutation.rb │ │ │ └── sign_in_user_mutation.rb │ ├── query_analyzers │ │ ├── concerns │ │ │ └── introspectable.rb │ │ ├── query_depth_analyzer.rb │ │ └── query_complexity_analyzer.rb │ ├── loaders │ │ ├── record_loader.rb │ │ └── association_loader.rb │ └── rails_api_boilerplate_schema.rb ├── assets │ └── config │ │ └── manifest.js ├── controllers │ ├── application_controller.rb │ ├── graphql_controller.rb │ └── concerns │ │ ├── user_retriever.rb │ │ └── graphql_request_resolver.rb ├── channels │ ├── application_cable │ │ ├── channel.rb │ │ └── connection.rb │ └── graphql_channel.rb ├── mailers │ └── application_mailer.rb ├── jobs │ └── application_job.rb └── services │ └── application_service.rb ├── .rspec ├── .env.test ├── spec ├── support │ ├── simple_cov.rb │ ├── webmock.rb │ ├── factory_bot.rb │ ├── shoulda_matchers.rb │ ├── request_helpers.rb │ ├── graphql_utils.rb │ └── matchers │ │ └── render_status_code.rb ├── factories │ └── users.rb ├── models │ └── user_spec.rb ├── rails_helper.rb ├── spec_helper.rb ├── requests │ └── users │ │ ├── show_spec.rb │ │ ├── update_spec.rb │ │ ├── create_spec.rb │ │ └── sign_in_spec.rb └── rubocop │ └── cop │ └── migration │ └── add_index_spec.rb ├── rubocop ├── rubocop.rb ├── rubocop_rails.yml ├── migration_helpers.rb └── cop │ └── migration │ └── add_index.rb ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE ├── workflows │ ├── deploy.yml │ ├── deploy_to_dev.yml │ ├── deploy_to_prod.yml │ ├── deploy_to_staging.yml │ └── test_and_liters.yml └── dependabot.yml ├── public └── robots.txt ├── config ├── spring.rb ├── initializers │ ├── graphql_config.rb │ ├── graphiql.rb │ ├── mime_types.rb │ ├── filter_parameter_logging.rb │ ├── application_controller_renderer.rb │ ├── backtrace_silencers.rb │ ├── sentry.rb │ ├── cors.rb │ ├── wrap_parameters.rb │ ├── inflections.rb │ └── strong_migrations.rb ├── environment.rb ├── boot.rb ├── credentials │ ├── test.yml.enc │ └── development.yml.enc ├── routes.rb ├── cable.yml ├── database.yml ├── credentials.yml.enc ├── storage.yml ├── application.rb ├── locales │ └── en.yml ├── puma.rb └── environments │ ├── test.rb │ ├── development.rb │ └── production.rb ├── Procfile ├── config.ru ├── Rakefile ├── bin ├── rake ├── rails ├── spring ├── setup └── bundle ├── release_tasks.sh ├── .env.template ├── db ├── seeds.rb ├── migrate │ └── 20190703155941_create_users.rb └── schema.rb ├── lib └── tasks │ ├── linters.rake │ └── auto_annotate_models.rake ├── .gitignore ├── .rubocop_todo.yml ├── .rubocop.yml ├── LICENSE ├── Gemfile ├── .reek.yml ├── PAGINATION.md ├── README.md └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.7.2 2 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require rails_helper 2 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | JWT_ENCODING_KEY=dummy_secret_key 2 | -------------------------------------------------------------------------------- /spec/support/simple_cov.rb: -------------------------------------------------------------------------------- 1 | SimpleCov.start 2 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /rubocop/rubocop.rb: -------------------------------------------------------------------------------- 1 | require_relative 'cop/migration/add_index' 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @aperezcristina @elestu @mconiglio @diebas @Stefano-loop @fdecono 2 | -------------------------------------------------------------------------------- /app/graphql/types/base_enum.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class BaseEnum < GraphQL::Schema::Enum 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/graphql/types/base_union.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class BaseUnion < GraphQL::Schema::Union 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/support/webmock.rb: -------------------------------------------------------------------------------- 1 | require 'webmock/rspec' 2 | 3 | WebMock.disable_net_connect!(allow_localhost: true) 4 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link graphiql/rails/application.css 2 | //= link graphiql/rails/application.js 3 | -------------------------------------------------------------------------------- /app/graphql/queries/base_query.rb: -------------------------------------------------------------------------------- 1 | module Queries 2 | class BaseQuery < GraphQL::Schema::Resolver; end 3 | end 4 | -------------------------------------------------------------------------------- /app/graphql/types/base_scalar.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class BaseScalar < GraphQL::Schema::Scalar 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/support/factory_bot.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include FactoryBot::Syntax::Methods 3 | end 4 | -------------------------------------------------------------------------------- /app/graphql/types/base_argument.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class BaseArgument < GraphQL::Schema::Argument 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::API 2 | include UserRetriever 3 | end 4 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | Spring.watch( 2 | '.ruby-version', 3 | '.rbenv-vars', 4 | 'tmp/restart.txt', 5 | 'tmp/caching-dev.txt' 6 | ) 7 | -------------------------------------------------------------------------------- /app/graphql/types/base_interface.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | module BaseInterface 3 | include GraphQL::Schema::Interface 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bin/rails server -p $PORT -e $RAILS_ENV 2 | worker: bundle exec sidekiq -C ./config/sidekiq.yml -v 3 | release: bash ./release_tasks.sh 4 | -------------------------------------------------------------------------------- /app/graphql/types/query_type.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class QueryType < Types::BaseObject 3 | field :user, resolver: Queries::User 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/graphql_config.rb: -------------------------------------------------------------------------------- 1 | module GraphqlConfig 2 | EXPOSE_API_INSIGHTS = ENV.fetch('EXPOSE_API_INSIGHTS', 'false') == 'true' 3 | end 4 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: ENV['DEFAULT_FROM_EMAIL_ADDRESS'] 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /app/graphql/types/base_input_object.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class BaseInputObject < GraphQL::Schema::InputObject 3 | argument_class Types::BaseArgument 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/support/shoulda_matchers.rb: -------------------------------------------------------------------------------- 1 | Shoulda::Matchers.configure do |config| 2 | config.integrate do |with| 3 | with.test_framework :rspec 4 | with.library :rails 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/graphiql.rb: -------------------------------------------------------------------------------- 1 | GraphiQL::Rails.config.headers['Authorization'] = lambda { |_context| 2 | token = ENV['GRAPHIQL_SESSION_TOKEN'] 3 | token.present? ? "Bearer #{token}" : '' 4 | } 5 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /app/graphql/queries/user.rb: -------------------------------------------------------------------------------- 1 | module Queries 2 | class User < Queries::BaseQuery 3 | type Types::CustomTypes::UserType, null: true 4 | 5 | def resolve 6 | context[:current_user] 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/graphql/types/base_field.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class BaseField < GraphQL::Schema::Field 3 | argument_class Types::BaseArgument 4 | 5 | def resolve_field(obj, args, ctx) 6 | resolve(obj, args, ctx) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('spring', __dir__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /config/credentials/test.yml.enc: -------------------------------------------------------------------------------- 1 | luuplRPk4/WRkQyMZvk6ztJ2nQnp4LwzIbSJVJFih5EMwcCnxzHQBcvOkpTZp2hkOOZyauCZ8csUeB6osQeuQImsyCpBA0lmw7RIJkrn6jxNf0iApP4nh+iihRKn45NQLqZ5DRdMwL9TxyTFsr2tnEWmjdLoVNHO8TpoUNhITXZJ2GOfPjGwANzBGoQg70IkQ8iDf9MEOA==--1rCo/bO9W66t4Hyk--y2FjSD2p6Xy6xXoG67SMqg== -------------------------------------------------------------------------------- /config/credentials/development.yml.enc: -------------------------------------------------------------------------------- 1 | xrCSU3AjceTzi25Wl5HHFjsKmlp5ZDGT3AwzexIZHV+rmW+HFkhR0bxMOaJfVYzrNMHCh2QIZSBTVRlPI2A0Ut/Un8qNQE/EmlzYFkxf9zNFdScSB//+F8iQiQqhdBE5DIDZL1HZTuzlLob2awfWP42OxN1yGII4MxKEFhWDPCZ6OFU3b5rDRgvhz4LRL2+mCrbP+eLxmQ==--5R2yIC0JuG5Bg4qS--0Iwt7cJ1PO2ZIsfgUMBbMA== -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | if GraphqlConfig::EXPOSE_API_INSIGHTS 3 | mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: 'graphql#execute' 4 | end 5 | 6 | mount ActionCable.server, at: '/cable' 7 | post '/graphql', to: 'graphql#execute' 8 | end 9 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /rubocop/rubocop_rails.yml: -------------------------------------------------------------------------------- 1 | Rails/LexicallyScopedActionFilter: 2 | Exclude: 3 | - app/controllers/**/** 4 | 5 | Rails/NotNullColumn: 6 | Enabled: false 7 | 8 | Rails/RakeEnvironment: 9 | Exclude: 10 | - lib/tasks/auto_annotate_models.rake 11 | - lib/tasks/linters.rake 12 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: redis 3 | url: redis://localhost:6379/1 4 | 5 | test: 6 | adapter: test 7 | 8 | production: 9 | adapter: redis 10 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 11 | channel_prefix: rails_api_boilerplate_production 12 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('spring', __dir__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/graphql/types/subscription_type.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class SubscriptionType < Types::BaseObject 3 | field( 4 | :user_updated, 5 | Types::CustomTypes::UserType, 6 | null: false, 7 | description: 'A user has been updated' 8 | ) 9 | 10 | def user_updated; end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/graphql/types/custom_types/user_type.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | module CustomTypes 3 | class UserType < Types::BaseObject 4 | field :id, ID, null: false 5 | field :first_name, String, null: true 6 | field :last_name, String, null: true 7 | field :email, String, null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /release_tasks.sh: -------------------------------------------------------------------------------- 1 | echo "Running Release Tasks" 2 | 3 | if [ "$MIGRATIONS_ON_RELEASE" == "true" ]; then 4 | echo "Running Migrations" 5 | bundle exec rails db:migrate 6 | fi 7 | 8 | if [ "$SEED_ON_RELEASE" == "true" ]; then 9 | echo "Seeding DB" 10 | bundle exec rails db:seed 11 | fi 12 | 13 | echo "Done running release_tasks.sh" 14 | -------------------------------------------------------------------------------- /app/controllers/graphql_controller.rb: -------------------------------------------------------------------------------- 1 | class GraphqlController < ApplicationController 2 | include GraphqlRequestResolver 3 | 4 | def execute 5 | result = resolve(query: params, context: context) 6 | 7 | render json: result 8 | end 9 | 10 | private 11 | 12 | def context 13 | { current_user: current_user } 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | MAX_DEPTH=5 2 | MAX_COMPLEXITY=30 3 | JWT_ENCODING_KEY= 4 | 5 | # Local 6 | DB_HOST=localhost 7 | DB_USERNAME= 8 | DB_PASSWORD= 9 | DB_NAME=rails-graphql-api-boilerplate 10 | EXPOSE_API_INSIGHTS=true 11 | DEFAULT_FROM_EMAIL_ADDRESS=no-reply@my-app.com 12 | GRAPHIQL_SESSION_TOKEN= 13 | 14 | # Production 15 | SENTRY_DSN= 16 | EXPOSE_API_INSIGHTS=false 17 | -------------------------------------------------------------------------------- /app/graphql/mutations/base_mutation.rb: -------------------------------------------------------------------------------- 1 | module Mutations 2 | class BaseMutation < GraphQL::Schema::RelayClassicMutation 3 | object_class Types::BaseObject 4 | field_class Types::BaseField 5 | input_object_class Types::BaseInputObject 6 | 7 | def render_error(errors) 8 | raise GraphQL::ExecutionError.new(errors, extensions: { code: :bad_request }) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/services/application_service.rb: -------------------------------------------------------------------------------- 1 | class ApplicationService 2 | attr_reader :result 3 | 4 | class << self 5 | def call(*args, &block) 6 | service = new(*args, &block) 7 | service.call 8 | service 9 | end 10 | end 11 | 12 | def call; end 13 | 14 | def success? 15 | errors.empty? 16 | end 17 | 18 | def errors 19 | @errors ||= [] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/controllers/concerns/user_retriever.rb: -------------------------------------------------------------------------------- 1 | module UserRetriever 2 | extend ActiveSupport::Concern 3 | 4 | def current_user 5 | @current_user ||= AuthToken.verify(token) if token.present? 6 | end 7 | 8 | private 9 | 10 | def token 11 | @token ||= authorization_headers&.split(' ')&.last 12 | end 13 | 14 | def authorization_headers 15 | request.headers['Authorization'] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/graphql/mutations/user_mutations/delete_user_mutation.rb: -------------------------------------------------------------------------------- 1 | module Mutations 2 | module UserMutations 3 | class DeleteUserMutation < Mutations::BaseMutation 4 | field :user, Types::CustomTypes::UserType, null: false 5 | 6 | def resolve 7 | user = User.find(context[:current_user]&.id) 8 | user.destroy! 9 | 10 | { user: user } 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/graphql/types/base_object.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class BaseObject < GraphQL::Schema::Object 3 | def self.connection_type 4 | connection_class = super 5 | 6 | connection_class.define_method(:total_count) do 7 | object.nodes.size 8 | end 9 | 10 | connection_class.send(:field, :total_count, Integer, null: false) 11 | 12 | connection_class 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to 2 | # seed the database with its default values. 3 | # The data can then be loaded with the rails db:seed command 4 | # (or created alongside the database with db:setup). 5 | # 6 | # Examples: 7 | # 8 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 9 | # Character.create(name: 'Luke', movie: movies.first) 10 | -------------------------------------------------------------------------------- /app/graphql/types/mutation_type.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class MutationType < Types::BaseObject 3 | field :create_user, mutation: Mutations::UserMutations::CreateUserMutation 4 | field :delete_user, mutation: Mutations::UserMutations::DeleteUserMutation 5 | field :update_user, mutation: Mutations::UserMutations::UpdateUserMutation 6 | field :sign_in_user, mutation: Mutations::UserMutations::SignInUserMutation 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/models/auth_token.rb: -------------------------------------------------------------------------------- 1 | class AuthToken 2 | KEY = ENV.fetch('JWT_ENCODING_KEY') 3 | ALGORITHM = 'HS256'.freeze 4 | 5 | def self.token(user) 6 | payload = { user_id: user.id } 7 | JWT.encode(payload, KEY, ALGORITHM) 8 | end 9 | 10 | def self.verify(token) 11 | decoded_token = JWT.decode(token, KEY, true, { algorithm: ALGORITHM }) 12 | 13 | User.find_by(id: decoded_token.first['user_id']) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | #### Description: 2 | 3 | Provide a good description of the problem this PR is trying to address. 4 | 5 | --- 6 | 7 | #### :pushpin: Notes: 8 | 9 | * Include TODOS, warnings, things other devs need to be aware of when merging, etc. 10 | 11 | --- 12 | 13 | #### :heavy_check_mark:Tasks: 14 | 15 | * Write the list of things you performed to help the reviewer. 16 | 17 | --- 18 | 19 | @loopstudio/ruby-devs 20 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: akhileshns/heroku-deploy@v3.0.4 14 | with: 15 | heroku_api_key: ${{secrets.HEROKU_API_KEY}} 16 | heroku_app_name: ${{secrets.HEROKU_PROD_APP}} 17 | heroku_email: ${{secrets.HEROKU_EMAIL}} 18 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | encoding: unicode 4 | host: <%= ENV['DB_HOST'] %> 5 | pool: 5 6 | timeout: 5000 7 | username: <%= ENV['DB_USERNAME'] %> 8 | password: <%= ENV['DB_PASSWORD'] %> 9 | database: <%= ENV['DB_NAME'] %> 10 | 11 | development: 12 | <<: *default 13 | database: <%= "#{ENV['DB_NAME']}-development" %> 14 | 15 | test: 16 | <<: *default 17 | database: <%= "#{ENV['DB_NAME']}-test" %> 18 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /rubocop/migration_helpers.rb: -------------------------------------------------------------------------------- 1 | module RuboCop 2 | # Module containing helper methods for writing migration cops. 3 | module MigrationHelpers 4 | # Returns true if the given node originated from the db/migrate directory. 5 | def in_migration?(node) 6 | dirname(node).end_with?('db/migrate') 7 | end 8 | 9 | private 10 | 11 | def dirname(node) 12 | File.dirname(node.location.expression.source_buffer.name) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | WwI/bF5uSezjhEHcSgO3gKZ+xIlTziA19QSGMqI5E2f1J2SRv37bld8HpR2pJ64JjM0GnFpIpWbSX8MTprgGJkdyHs4+3au/QpuDkbTswFQdByiTME3+XAXS97OwNE5NbqeF9s8Fo1FGQ9xFhoKYW0jbowGiDQ+GYjMOrjMQGjGwC6sNg6AKR3bIOAUmcElBpPnuruMh729S0onegWrgBCP8wBZT7Urt1IwYK0bvgxN7nu5ATC5G+Lv1OiV1PtMeZH5rT9Qk+zIC+/r5AvacBpVI2hodateARLKeuvcMAy0FLtFH6deOKcLbL1cwJTgOjQHv9zWcA2YO1kptvTrcPFiRaW81YZhdfhRFWAewcaUUlM32TMZ2SYcU846t7gXW0hlzEIhVP+W3Ey1YUayx8eajFlWCaqCRfiya--krzc1ag7n2IT7MxU--Dc9uUQP/Jr0iclKequD2GA== -------------------------------------------------------------------------------- /db/migrate/20190703155941_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[6.0] 2 | disable_ddl_transaction! 3 | enable_extension 'citext' 4 | 5 | def change 6 | create_table :users do |t| 7 | t.string :first_name 8 | t.string :last_name 9 | t.string :password_digest 10 | t.citext :email, null: false 11 | 12 | t.timestamps 13 | end 14 | 15 | add_index :users, :email, unique: true, algorithm: :concurrently 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /.github/workflows/deploy_to_dev.yml: -------------------------------------------------------------------------------- 1 | # name: Deploy to Dev 2 | 3 | # on: 4 | # push: 5 | # branches: 6 | # - develop 7 | 8 | # jobs: 9 | # build: 10 | # runs-on: ubuntu-latest 11 | # steps: 12 | # - uses: actions/checkout@v2 13 | # - uses: akhileshns/heroku-deploy@v3.0.4 14 | # with: 15 | # heroku_api_key: ${{secrets.HEROKU_API_KEY}} 16 | # heroku_app_name: ${{secrets.HEROKU_DEV_APP}} 17 | # heroku_email: ${{secrets.HEROKU_EMAIL}} 18 | -------------------------------------------------------------------------------- /.github/workflows/deploy_to_prod.yml: -------------------------------------------------------------------------------- 1 | # name: Deploy to Production 2 | 3 | # on: 4 | # push: 5 | # branches: 6 | # - master 7 | 8 | # jobs: 9 | # build: 10 | # runs-on: ubuntu-latest 11 | # steps: 12 | # - uses: actions/checkout@v2 13 | # - uses: akhileshns/heroku-deploy@v3.0.4 14 | # with: 15 | # heroku_api_key: ${{secrets.HEROKU_API_KEY}} 16 | # heroku_app_name: ${{secrets.HEROKU_PROD_APP}} 17 | # heroku_email: ${{secrets.HEROKU_EMAIL}} 18 | -------------------------------------------------------------------------------- /.github/workflows/deploy_to_staging.yml: -------------------------------------------------------------------------------- 1 | # name: Deploy to Staging 2 | 3 | # on: 4 | # pull_request: 5 | # branches: 6 | # - master 7 | 8 | # jobs: 9 | # build: 10 | # runs-on: ubuntu-latest 11 | # steps: 12 | # - uses: actions/checkout@v2 13 | # - uses: akhileshns/heroku-deploy@v3.0.4 14 | # with: 15 | # heroku_api_key: ${{secrets.HEROKU_API_KEY}} 16 | # heroku_app_name: ${{secrets.HEROKU_STAGING_APP}} 17 | # heroku_email: ${{secrets.HEROKU_EMAIL}} 18 | -------------------------------------------------------------------------------- /config/initializers/sentry.rb: -------------------------------------------------------------------------------- 1 | Sentry.init do |config| 2 | config.dsn = ENV['SENTRY_DSN'] 3 | config.breadcrumbs_logger = %i[active_support_logger http_logger] 4 | config.send_default_pii = true 5 | config.sample_rate = 1.0 6 | config.async = ->(event) { Sentry::SendEventJob.perform_later(event) } 7 | 8 | # Param filtering 9 | filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters) 10 | config.before_send = lambda do |event, _hint| 11 | filter.filter(event.to_hash) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/tasks/linters.rake: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | 3 | task :linters do 4 | options = {} 5 | OptionParser.new { |opts| 6 | opts.on('-a') { |autofix| options[:autofix] = autofix } 7 | }.parse! 8 | 9 | files_diff = `git diff --diff-filter=ACMRTUXB --name-only origin/master... | \ 10 | xargs | sed "s/\\n/\\s/"` 11 | 12 | if files_diff.present? 13 | sh "bundle exec rubocop --force-exclusion #{'-a' if options[:autofix]} #{files_diff}" 14 | sh "bundle exec reek --force-exclusion -c .reek.yml #{files_diff}" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Avoid CORS issues when API is called from the frontend app. 4 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. 5 | 6 | # Read more: https://github.com/cyu/rack-cors 7 | 8 | Rails.application.config.middleware.insert_before 0, Rack::Cors do 9 | allow do 10 | origins '*' 11 | 12 | resource '*', 13 | headers: :any, 14 | methods: %i[get post put patch delete options head] 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads Spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == 'spring' } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/graphql/mutations/user_mutations/update_user_mutation.rb: -------------------------------------------------------------------------------- 1 | module Mutations 2 | module UserMutations 3 | class UpdateUserMutation < Mutations::BaseMutation 4 | argument :first_name, String, required: false 5 | argument :last_name, String, required: false 6 | argument :email, String, required: false 7 | argument :password, String, required: false 8 | 9 | field :user, Types::CustomTypes::UserType, null: false 10 | 11 | def resolve(**attributes) 12 | user = User.find(context[:current_user]&.id) 13 | user.update!(attributes) 14 | 15 | { user: user } 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/request_helpers.rb: -------------------------------------------------------------------------------- 1 | module RequestHelpers 2 | def auth_headers(some_user = user) 3 | { 'Authorization' => AuthToken.token(some_user) } 4 | end 5 | 6 | def errors 7 | json[:errors] 8 | end 9 | 10 | def first_error_message 11 | errors.first[:message] 12 | end 13 | 14 | def json 15 | raise 'Response is nil. Are you sure you made a request?' unless response 16 | 17 | JSON.parse(response.body, symbolize_names: true) 18 | end 19 | 20 | def graphql_request(request, variables: {}, headers: nil) 21 | GraphqlUtils.validate!(request.to_s) 22 | 23 | post(graphql_path, params: { query: request, variables: variables }, headers: headers) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/graphql/mutations/user_mutations/create_user_mutation.rb: -------------------------------------------------------------------------------- 1 | module Mutations 2 | module UserMutations 3 | class CreateUserMutation < Mutations::BaseMutation 4 | argument :first_name, String, required: true 5 | argument :last_name, String, required: true 6 | argument :email, String, required: true 7 | argument :password, String, required: true 8 | 9 | field :user, Types::CustomTypes::UserType, null: false 10 | field :token, String, null: false 11 | 12 | def resolve(**attributes) 13 | user = User.create!(attributes) 14 | token = AuthToken.token(user) 15 | 16 | { user: user, token: token } 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "08:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: graphql 11 | versions: 12 | - 1.12.5 13 | - 1.12.7 14 | - dependency-name: rails 15 | versions: 16 | - 6.1.2 17 | - 6.1.3 18 | - dependency-name: rspec-rails 19 | versions: 20 | - 4.1.0 21 | - 5.0.0 22 | - dependency-name: webmock 23 | versions: 24 | - 3.11.3 25 | - 3.12.0 26 | - dependency-name: rubocop 27 | versions: 28 | - 1.11.0 29 | - 1.9.0 30 | - dependency-name: bootsnap 31 | versions: 32 | - 1.6.0 33 | -------------------------------------------------------------------------------- /app/graphql/query_analyzers/concerns/introspectable.rb: -------------------------------------------------------------------------------- 1 | module QueryAnalyzers 2 | module Concerns 3 | module Introspectable 4 | extend ActiveSupport::Concern 5 | 6 | private 7 | 8 | def verify_introspection_availability 9 | introspection_not_available_error unless insights_enabled? 10 | end 11 | 12 | def introspection? 13 | query.operation_name == 'IntrospectionQuery' 14 | end 15 | 16 | def introspection_not_available_error 17 | GraphQL::AnalysisError.new(I18n.t('errors.instrospection_not_available')) 18 | end 19 | 20 | def insights_enabled? 21 | GraphqlConfig::EXPOSE_API_INSIGHTS 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :bigint not null, primary key 6 | # email :citext not null, indexed 7 | # first_name :string 8 | # last_name :string 9 | # password_digest :string 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | # Indexes 14 | # 15 | # index_users_on_email (email) UNIQUE 16 | # 17 | 18 | FactoryBot.define do 19 | factory :user do 20 | email { Faker::Internet.email } 21 | first_name { Faker::Name.first_name } 22 | last_name { Faker::Name.last_name } 23 | password { Faker::Internet.password } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/support/graphql_utils.rb: -------------------------------------------------------------------------------- 1 | module GraphqlUtils 2 | class ParseErrorBetterPrinted < ::GraphQL::ParseError 3 | def initialize(exc) 4 | super(exc.message, exc.line, exc.col, exc.query) 5 | end 6 | 7 | def to_s 8 | "#{super}\n\n#{query}" 9 | end 10 | end 11 | 12 | def self.hash_to_gql(attributes) 13 | attr_gql_lines = attributes.map do |key, value| 14 | new_key = key.to_s.camelize(:lower) 15 | "#{new_key}: #{value.inspect || '""'}" 16 | end 17 | 18 | " { #{attr_gql_lines.join(', ')} } " 19 | end 20 | 21 | def self.validate!(some_string) 22 | GraphQL.parse(some_string) 23 | rescue ::GraphQL::ParseError => e 24 | raise ParseErrorBetterPrinted, e 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/graphql/query_analyzers/query_depth_analyzer.rb: -------------------------------------------------------------------------------- 1 | module QueryAnalyzers 2 | class QueryDepthAnalyzer < GraphQL::Analysis::AST::QueryDepth 3 | include Concerns::Introspectable 4 | 5 | MAX_DEPTH = ENV.fetch('MAX_DEPTH', 5).to_i 6 | 7 | def result 8 | return verify_introspection_availability if introspection? 9 | 10 | @depth = super 11 | max_depth_exceeded_error if depth_exceeded? 12 | end 13 | 14 | private 15 | 16 | def max_depth_exceeded_error 17 | GraphQL::AnalysisError.new( 18 | I18n.t('errors.max_depth_exceeded', current_depth: @depth, max_depth: MAX_DEPTH) 19 | ) 20 | end 21 | 22 | def depth_exceeded? 23 | MAX_DEPTH < @depth 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/graphql/mutations/user_mutations/sign_in_user_mutation.rb: -------------------------------------------------------------------------------- 1 | module Mutations 2 | module UserMutations 3 | class SignInUserMutation < Mutations::BaseMutation 4 | argument :email, String, required: true 5 | argument :password, String, required: true 6 | 7 | field :user, Types::CustomTypes::UserType, null: true 8 | field :token, String, null: true 9 | 10 | def resolve(email:, password:) 11 | user = User.find_by(email: email) 12 | 13 | unless user&.authenticate(password) 14 | raise GraphQL::ExecutionError, I18n.t('errors.invalid_credentials') 15 | end 16 | 17 | token = AuthToken.token(user) 18 | { user: user, token: token } 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :bigint not null, primary key 6 | # email :citext not null, indexed 7 | # first_name :string 8 | # last_name :string 9 | # password_digest :string 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | # Indexes 14 | # 15 | # index_users_on_email (email) UNIQUE 16 | # 17 | 18 | RSpec.describe User, type: :model do 19 | subject(:user) { create(:user) } 20 | 21 | describe 'validations' do 22 | it { is_expected.to validate_presence_of(:email) } 23 | it { is_expected.to validate_uniqueness_of(:email).case_insensitive } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/support/matchers/render_status_code.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :render_error_code do |expected| 2 | match do |_actual| 3 | json[:errors]&.first&.dig(:extensions, :code).to_s == expected.to_s 4 | end 5 | 6 | failure_message do |_actual| 7 | "Expected request to render in the json a status code of #{expected} but #{reason}" \ 8 | "\nThe json obtained was: #{json}" 9 | end 10 | 11 | def reason 12 | errors = json[:errors] 13 | return "it did not contain the key 'errors'." unless errors 14 | 15 | error = errors.first 16 | return "'errors' was empty." unless error 17 | 18 | actual_code = error.dig(:extensions, :code) 19 | return "no 'code' key was present." unless actual_code 20 | 21 | "it was #{actual_code}" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/graphql/loaders/record_loader.rb: -------------------------------------------------------------------------------- 1 | module Loaders 2 | class RecordLoader < GraphQL::Batch::Loader 3 | def initialize(model, column: model.primary_key, where: nil) 4 | super 5 | @model = model 6 | @column = column.to_s 7 | @column_type = model.type_for_attribute(@column) 8 | @where = where 9 | end 10 | 11 | def load(key) 12 | super(@column_type.cast(key)) 13 | end 14 | 15 | def perform(keys) 16 | query(keys).each { |record| fulfill(record.public_send(@column), record) } 17 | keys.each { |key| fulfill(key, nil) unless fulfilled?(key) } 18 | end 19 | 20 | private 21 | 22 | def query(keys) 23 | scope = @model 24 | scope = scope.where(@where) if @where 25 | scope.where(@column => keys) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../config/environment', __dir__) 3 | 4 | abort('The Rails environment is running in production mode!') if Rails.env.production? 5 | 6 | require 'rspec/rails' 7 | require 'spec_helper' 8 | 9 | Dir[Rails.root.join('spec/support/**/*.rb')].sort.each { |file| require file } 10 | 11 | begin 12 | ActiveRecord::Migration.maintain_test_schema! 13 | rescue ActiveRecord::PendingMigrationError => e 14 | puts e.to_s.strip 15 | exit 1 16 | end 17 | 18 | RSpec.configure do |config| 19 | config.include RequestHelpers, type: :request 20 | config.include Rails.application.routes.url_helpers 21 | 22 | config.use_transactional_fixtures = true 23 | config.infer_spec_type_from_file_location! 24 | config.filter_rails_from_backtrace! 25 | end 26 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :bigint not null, primary key 6 | # email :citext not null, indexed 7 | # first_name :string 8 | # last_name :string 9 | # password_digest :string 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | # Indexes 14 | # 15 | # index_users_on_email (email) UNIQUE 16 | # 17 | 18 | class User < ApplicationRecord 19 | has_secure_password 20 | 21 | after_update :notify_subscriber_of_addition 22 | 23 | validates :email, presence: true, uniqueness: true 24 | 25 | private 26 | 27 | def notify_subscriber_of_addition 28 | RailsApiBoilerplateSchema.subscriptions.trigger('user_updated', {}, self) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'active_job' 2 | require 'active_support/testing/time_helpers' 3 | 4 | RSpec.configure do |config| 5 | config.include ActiveJob::TestHelper 6 | config.include ActiveSupport::Testing::TimeHelpers 7 | 8 | config.expect_with :rspec do |expectations| 9 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 10 | expectations.syntax = :expect 11 | end 12 | 13 | config.mock_with :rspec do |mocks| 14 | mocks.verify_partial_doubles = true 15 | end 16 | 17 | config.shared_context_metadata_behavior = :apply_to_host_groups 18 | config.order = :random 19 | 20 | config.before do 21 | ActionMailer::Base.deliveries.clear 22 | ActiveJob::Base.queue_adapter = :test 23 | end 24 | 25 | config.after do 26 | FileUtils.rm_rf(Dir[Rails.root.join('/spec/support/uploads').to_s]) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /config/initializers/strong_migrations.rb: -------------------------------------------------------------------------------- 1 | # Mark existing migrations as safe 2 | StrongMigrations.start_after = 20201119202420 3 | 4 | # Set timeouts for migrations 5 | # If you use PgBouncer in transaction mode, delete these lines and set timeouts on the database user 6 | StrongMigrations.lock_timeout = 10.seconds 7 | StrongMigrations.statement_timeout = 1.hour 8 | 9 | # Analyze tables after indexes are added 10 | # Outdated statistics can sometimes hurt performance 11 | StrongMigrations.auto_analyze = true 12 | 13 | # Set the version of the production database 14 | # so the right checks are run in development 15 | # StrongMigrations.target_version = 10 16 | 17 | # Add custom checks 18 | # StrongMigrations.add_check do |method, args| 19 | # if method == :add_index && args[0].to_s == "users" 20 | # stop! "No more indexes on the users table" 21 | # end 22 | # end 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | /tmp/* 17 | !/log/.keep 18 | !/tmp/.keep 19 | 20 | # Ignore uploaded files in development. 21 | /storage/* 22 | !/storage/.keep 23 | .byebug_history 24 | 25 | # Ignore master key for decrypting credentials and more. 26 | /config/master.key 27 | 28 | /config/credentials/development.key 29 | 30 | /config/credentials/test.key 31 | 32 | .env 33 | coverage 34 | -------------------------------------------------------------------------------- /app/graphql/query_analyzers/query_complexity_analyzer.rb: -------------------------------------------------------------------------------- 1 | module QueryAnalyzers 2 | class QueryComplexityAnalyzer < GraphQL::Analysis::AST::QueryComplexity 3 | include Concerns::Introspectable 4 | 5 | MAX_COMPLEXITY = ENV.fetch('MAX_COMPLEXITY', 30).to_i 6 | 7 | def result 8 | return verify_introspection_availability if introspection? 9 | 10 | @complexity = super 11 | max_complexity_exceeded_error if complexity_exceeded? 12 | end 13 | 14 | private 15 | 16 | def max_complexity_exceeded_error 17 | GraphQL::AnalysisError.new( 18 | I18n.t( 19 | 'errors.max_complexity_exceeded', 20 | current_complexity: @complexity, 21 | max_complexity: MAX_COMPLEXITY 22 | ) 23 | ) 24 | end 25 | 26 | def complexity_exceeded? 27 | MAX_COMPLEXITY < @complexity 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2020-10-16 16:26:38 UTC using RuboCop version 0.93.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 8 10 | # Configuration parameters: Max. 11 | RSpec/ExampleLength: 12 | Exclude: 13 | - 'spec/requests/users/create_spec.rb' 14 | - 'spec/requests/users/show_spec.rb' 15 | - 'spec/requests/users/sign_in_spec.rb' 16 | - 'spec/requests/users/update_spec.rb' 17 | - 'spec/rubocop/cop/migration/add_index_spec.rb' 18 | 19 | # Offense count: 18 20 | # Configuration parameters: AllowSubject. 21 | RSpec/MultipleMemoizedHelpers: 22 | Max: 9 23 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - .rubocop_todo.yml 3 | - ./rubocop/rubocop_rails.yml 4 | 5 | inherit_mode: 6 | merge: 7 | - Exclude 8 | 9 | require: 10 | - rubocop-rails 11 | - rubocop-rspec 12 | - ./rubocop/rubocop 13 | 14 | AllCops: 15 | NewCops: enable 16 | Exclude: 17 | - vendor/**/* 18 | - .circleci/* 19 | - db/schema.rb 20 | - bin/bundle 21 | - rubocop/**/*.rb 22 | 23 | Layout/LineLength: 24 | Max: 100 25 | Exclude: 26 | - config/**/* 27 | 28 | Metrics/AbcSize: 29 | Exclude: 30 | - db/migrate/* 31 | - app/channels/graphql_channel.rb 32 | 33 | Metrics/BlockLength: 34 | Exclude: 35 | - config/**/* 36 | - spec/**/* 37 | - lib/tasks/auto_annotate_models.rake 38 | 39 | Style/BlockDelimiters: 40 | EnforcedStyle: braces_for_chaining 41 | 42 | Style/Documentation: 43 | Enabled: false 44 | 45 | Style/FrozenStringLiteralComment: 46 | Enabled: false 47 | 48 | Style/NumericLiterals: 49 | Exclude: 50 | - config/initializers/strong_migrations.rb 51 | -------------------------------------------------------------------------------- /app/graphql/rails_api_boilerplate_schema.rb: -------------------------------------------------------------------------------- 1 | class RailsApiBoilerplateSchema < GraphQL::Schema 2 | disable_introspection_entry_points unless GraphqlConfig::EXPOSE_API_INSIGHTS 3 | use GraphQL::Subscriptions::ActionCableSubscriptions, redis: Redis.new 4 | use GraphQL::Batch 5 | query_analyzer QueryAnalyzers::QueryComplexityAnalyzer 6 | query_analyzer QueryAnalyzers::QueryDepthAnalyzer 7 | 8 | mutation(Types::MutationType) 9 | query(Types::QueryType) 10 | subscription(Types::SubscriptionType) 11 | 12 | rescue_from(ActiveRecord::RecordInvalid) do |exception| 13 | errors = exception.record.errors.messages.to_json 14 | GraphQL::ExecutionError.new(errors, extensions: { code: :bad_request }) 15 | end 16 | 17 | rescue_from(ActiveRecord::RecordNotFound) do |exception| 18 | GraphQL::ExecutionError.new(exception, extensions: { code: :not_found }) 19 | end 20 | 21 | rescue_from(StandardError) do |exception| 22 | GraphQL::ExecutionError.new(exception, extensions: { code: :internal_server_error }) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Loop Studio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path('..', __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to setup or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at anytime and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?('config/database.yml') 22 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! 'bin/rails db:prepare' 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! 'bin/rails log:clear tmp:clear' 30 | 31 | puts "\n== Restarting application server ==" 32 | system! 'bin/rails restart' 33 | end 34 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /app/controllers/concerns/graphql_request_resolver.rb: -------------------------------------------------------------------------------- 1 | module GraphqlRequestResolver 2 | include ActiveSupport::Concern 3 | 4 | def resolve(query:, context:) 5 | if _json 6 | resolve_multiplex(context) 7 | else 8 | resolve_execute(query, context) 9 | end 10 | end 11 | 12 | private 13 | 14 | def resolve_execute(query, context) 15 | RailsApiBoilerplateSchema.execute( 16 | query[:query], 17 | operation_name: query[:operationName], 18 | variables: hasherize(query[:variables]), 19 | context: context 20 | ) 21 | end 22 | 23 | def resolve_multiplex(context) 24 | input = _json.map do |query| 25 | { 26 | query: query[:query], 27 | operation_name: query[:operationName], 28 | variables: hasherize(query[:variables]), 29 | context: context 30 | } 31 | end 32 | 33 | RailsApiBoilerplateSchema.multiplex(input) 34 | end 35 | 36 | def hasherize(input) 37 | return {} if input.blank? 38 | 39 | case input 40 | when String then hasherize(JSON.parse(input)) 41 | when Hash, ActionController::Parameters then input 42 | else raise ArgumentError, I18n.t('errors.unexpected_param', param: ambiguous_param.to_s) 43 | end 44 | end 45 | 46 | def _json 47 | params[:_json] 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `rails 6 | # db:schema:load`. When creating a new database, `rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2019_07_03_155941) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "citext" 17 | enable_extension "plpgsql" 18 | 19 | create_table "users", force: :cascade do |t| 20 | t.string "first_name" 21 | t.string "last_name" 22 | t.string "password_digest" 23 | t.citext "email", null: false 24 | t.datetime "created_at", precision: 6, null: false 25 | t.datetime "updated_at", precision: 6, null: false 26 | t.index ["email"], name: "index_users_on_email", unique: true 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /.github/workflows/test_and_liters.yml: -------------------------------------------------------------------------------- 1 | name: Tests and Linters 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'master' 7 | - 'develop' 8 | push: 9 | branches: 10 | - 'master' 11 | - 'develop' 12 | - /^release.*/ 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | services: 19 | postgres: 20 | image: postgres:11.5 21 | ports: ["5432:5432"] 22 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - name: Set up Ruby (.ruby-version) 28 | uses: ruby/setup-ruby@v1 29 | with: 30 | bundler-cache: true 31 | 32 | - name: Install PostgreSQL 11 client 33 | run: sudo apt-get -yqq install libpq-dev 34 | 35 | - name: Build App 36 | env: 37 | PGHOST: localhost 38 | PGUSER: postgres 39 | RAILS_ENV: test 40 | run: | 41 | gem install bundler 42 | bundle config path vendor/bundle 43 | bundle install --jobs 4 --retry 3 44 | bin/rails db:setup 45 | 46 | - name: Run Linters 47 | run: | 48 | git fetch origin master:refs/remotes/origin/master 49 | bundle exec rake linters 50 | 51 | - name: Run Tests 52 | env: 53 | PGHOST: localhost 54 | PGUSER: postgres 55 | RAILS_ENV: test 56 | run: bundle exec rspec 57 | -------------------------------------------------------------------------------- /spec/requests/users/show_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe 'Show current user request', type: :request do 4 | let!(:user) { create(:user) } 5 | 6 | let(:request_body) do 7 | <<~GQL 8 | query { 9 | user { 10 | id 11 | firstName 12 | lastName 13 | email 14 | } 15 | } 16 | GQL 17 | end 18 | 19 | context 'when the user is logged in' do 20 | subject(:request) do 21 | graphql_request(request_body, headers: auth_headers) 22 | end 23 | 24 | specify do 25 | request 26 | 27 | expect(response).to have_http_status(:ok) 28 | end 29 | 30 | specify do 31 | request 32 | 33 | expect(errors).to be_nil 34 | end 35 | 36 | it 'returns the current user info' do 37 | request 38 | 39 | expect(json.dig(:data, :user)).to include_json( 40 | id: user.id.to_s, 41 | firstName: user.first_name, 42 | lastName: user.last_name, 43 | email: user.email 44 | ) 45 | end 46 | end 47 | 48 | context 'when the user is not logged in' do 49 | subject(:request) do 50 | graphql_request(request_body) 51 | end 52 | 53 | it 'does not return an error message' do 54 | request 55 | 56 | expect(errors).to be_nil 57 | end 58 | 59 | it 'does not return any data' do 60 | request 61 | 62 | expect(json.dig(:data, :user)).to be_nil 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails' 4 | # Pick the frameworks you want: 5 | require 'active_model/railtie' 6 | require 'active_job/railtie' 7 | require 'active_record/railtie' 8 | require 'active_storage/engine' 9 | require 'action_controller/railtie' 10 | require 'action_mailer/railtie' 11 | require 'action_mailbox/engine' 12 | require 'action_text/engine' 13 | require 'action_view/railtie' 14 | require 'action_cable/engine' 15 | require 'sprockets/railtie' 16 | require 'rails/test_unit/railtie' 17 | 18 | # Require the gems listed in Gemfile, including any gems 19 | # you've limited to :test, :development, or :production. 20 | Bundler.require(*Rails.groups) 21 | 22 | module RailsApiBoilerplate 23 | class Application < Rails::Application 24 | # Initialize configuration defaults for originally generated Rails version. 25 | config.load_defaults 6.0 26 | 27 | # Settings in config/environments/* take precedence over those specified here. 28 | # Application configuration can go into files in config/initializers 29 | # -- all .rb files in that directory are automatically loaded after loading 30 | # the framework and any gems in your application. 31 | 32 | # Only loads a smaller set of middleware suitable for API only apps. 33 | # Middleware like session, flash, cookies can be added back manually. 34 | # Skip views, helpers and assets when generating a new resource. 35 | config.api_only = true 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | errors: 35 | association_error: "%{model} loader can't load association for %{class}" 36 | association_not_found: "No association %{association} on %{model}" 37 | unexpected_param: "Unexpected parameter: %{param}" 38 | invalid_credentials: 'Invalid credentials' 39 | max_complexity_exceeded: "Complexity of %{current_complexity} exceeds max complexity of %{max_complexity}" 40 | max_depth_exceeded: "Depth of %{current_depth} exceeds max depth of %{max_depth}" 41 | instrospection_not_available: "Instrospection not available" 42 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5) 8 | min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 12 | # 13 | port ENV.fetch('PORT', 3000) 14 | 15 | # Specifies the `environment` that Puma will run in. 16 | # 17 | environment ENV.fetch('RAILS_ENV') { 'development' } 18 | 19 | # Specifies the number of `workers` to boot in clustered mode. 20 | # Workers are forked web server processes. If using threads and workers together 21 | # the concurrency of the application would be max `threads` * `workers`. 22 | # Workers do not work on JRuby or Windows (both of which do not support 23 | # processes). 24 | # 25 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 26 | 27 | # Use the `preload_app!` method when specifying a `workers` number. 28 | # This directive tells Puma to first boot the application and load code 29 | # before forking the application. This takes advantage of Copy On Write 30 | # process behavior so workers use less memory. 31 | # 32 | # preload_app! 33 | 34 | # Allow puma to be restarted by `rails restart` command. 35 | plugin :tmp_restart 36 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | ruby '2.7.2' 3 | 4 | gem 'bootsnap', '>= 1.4.2', require: false 5 | gem 'rails', '~> 6.1.5' 6 | 7 | # WebServer 8 | gem 'puma', '~> 5.6' 9 | gem 'rack-cors', '~> 1.1.1' 10 | 11 | # Graphql 12 | gem 'graphql', '~> 2.0.11' 13 | gem 'graphql-batch', '~> 0.5.1' 14 | 15 | # Database 16 | gem 'pg', '~> 1.4.1' 17 | gem 'redis', '~> 4.6.0' 18 | gem 'strong_migrations', '~> 1.2.0' 19 | 20 | # Environment variables 21 | gem 'dotenv-rails', '~> 2.7.6' 22 | 23 | # Authentication 24 | gem 'jwt', '~> 2.4.1' 25 | 26 | # Encryption 27 | gem 'bcrypt', '~> 3.1.18' 28 | 29 | # Monitoring errors 30 | gem 'graphiql-rails', '~> 1.8.0' 31 | gem 'sentry-rails' 32 | gem 'sentry-ruby' 33 | 34 | group :development, :test do 35 | gem 'bullet', '~> 7.0.2' 36 | gem 'byebug', platforms: %i[mri mingw x64_mingw] 37 | gem 'factory_bot_rails', '~> 6.2.0' 38 | gem 'faker', '~> 2.21.0' 39 | gem 'rspec-rails', '~> 5.1.2' 40 | end 41 | 42 | group :development do 43 | gem 'annotate', '~> 3.2.0' 44 | gem 'letter_opener', '~> 1.8.1' 45 | gem 'listen', '>= 3.0.5', '< 3.8' 46 | gem 'reek', '~> 6.1.1', require: false 47 | gem 'rubocop', '~> 1.30.1', require: false 48 | gem 'rubocop-rails', '~> 2.15.0', require: false 49 | gem 'rubocop-rspec', '~> 2.11.1', require: false 50 | gem 'spring', '~> 2.1.1' 51 | gem 'spring-watcher-listen', '~> 2.0.0' 52 | end 53 | 54 | group :test do 55 | gem 'rspec-json_expectations', '~> 2.2.0' 56 | gem 'shoulda-matchers', '~> 5.1.0' 57 | gem 'simplecov', '~> 0.21.2' 58 | gem 'webmock', '~> 3.14.0' 59 | end 60 | 61 | gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] 62 | -------------------------------------------------------------------------------- /app/channels/graphql_channel.rb: -------------------------------------------------------------------------------- 1 | class GraphqlChannel < ApplicationCable::Channel 2 | delegate :subscription?, to: :result 3 | 4 | attr_reader :result 5 | 6 | def subscribed 7 | @subscription_ids = [] 8 | end 9 | 10 | def execute(data) 11 | context = { channel: self } 12 | 13 | set_result(data, context) 14 | 15 | payload = { 16 | result: subscription? ? { data: nil } : result.to_h, 17 | more: subscription? 18 | } 19 | 20 | # Track the subscription here so we can remove it on unsubscribe. 21 | @subscription_ids << context[:subscription_id] if result.context[:subscription_id] 22 | 23 | transmit(payload) 24 | end 25 | 26 | def unsubscribed 27 | @subscription_ids.each do |sid| 28 | RailsApiBoilerplateSchema.subscriptions.delete_subscription(sid) 29 | end 30 | end 31 | 32 | private 33 | 34 | def set_result(data, context) 35 | query, operation_name = data.values_at('query', 'operationName') 36 | variables = ensure_hash(data['variables']) 37 | 38 | @result = RailsApiBoilerplateSchema.execute( 39 | query: query, 40 | context: context, 41 | variables: variables, 42 | operation_name: operation_name 43 | ) 44 | end 45 | 46 | def ensure_hash(ambiguous_param) 47 | case ambiguous_param 48 | when String 49 | ambiguous_param.present? ? ensure_hash(JSON.parse(ambiguous_param)) : {} 50 | when Hash, ActionController::Parameters 51 | ambiguous_param 52 | when nil 53 | {} 54 | else 55 | raise ArgumentError, I18n.t('errors.unexpected_param', param: ambiguous_param.to_s) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /app/graphql/loaders/association_loader.rb: -------------------------------------------------------------------------------- 1 | module Loaders 2 | class AssociationLoader < GraphQL::Batch::Loader 3 | def self.validate(model, association_name) 4 | new(model, association_name) 5 | nil 6 | end 7 | 8 | def initialize(model, association_name) 9 | super 10 | @model = model 11 | @association_name = association_name 12 | validate 13 | end 14 | 15 | def load(record) 16 | unless record.is_a?(@model) 17 | raise TypeError, I18n.t('errors.association_error', model: @model, 18 | class: record.class.model_name.human) 19 | end 20 | return Promise.resolve(read_association(record)) if association_loaded?(record) 21 | 22 | super 23 | end 24 | 25 | # We want to load the associations on all records, even if they have the same id 26 | def cache_key(record) 27 | record.object_id 28 | end 29 | 30 | def perform(records) 31 | preload_association(records) 32 | records.each { |record| fulfill(record, read_association(record)) } 33 | end 34 | 35 | private 36 | 37 | def validate 38 | return if @model.reflect_on_association(@association_name) 39 | 40 | raise ArgumentError, I18n.t( 41 | 'errors.association_not_found', model: @model, association: @association_name 42 | ) 43 | end 44 | 45 | def preload_association(records) 46 | ::ActiveRecord::Associations::Preloader.new.preload(records, @association_name) 47 | end 48 | 49 | def read_association(record) 50 | record.public_send(@association_name) 51 | end 52 | 53 | def association_loaded?(record) 54 | record.association(@association_name).loaded? 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /rubocop/cop/migration/add_index.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../migration_helpers' 2 | 3 | module RuboCop 4 | module Cop 5 | module Migration 6 | # Cop that checks if indexes are added in a concurrent manner. 7 | class AddIndex < RuboCop::Cop::Cop 8 | include MigrationHelpers 9 | 10 | MSG_DDT = 'Prefer using disable_ddl_transaction! and { algorithm: :concurrently }'\ 11 | ' when creating an index'.freeze 12 | MSG_ALG = 'Prefer using { algorithm: :concurrently } when creating an index'.freeze 13 | 14 | def_node_search :disable_ddl_transaction?, '(send $_ :disable_ddl_transaction!)' 15 | 16 | def_node_matcher :indexes?, <<-MATCHER 17 | (send _ {:add_index :drop_index} $...) 18 | MATCHER 19 | 20 | def on_class(node) 21 | return unless in_migration?(node) 22 | 23 | return if ddl_transaction_disabled? 24 | 25 | @disable_ddl_transaction = disable_ddl_transaction?(node) 26 | @offensive_node = node 27 | end 28 | 29 | def on_send(node) 30 | return unless in_migration?(node) 31 | 32 | indexes?(node) do |args| 33 | add_offense(node, message: MSG_ALG) unless concurrently_enabled?(args.last) 34 | add_offense(@offensive_node, message: MSG_DDT) unless ddl_transaction_disabled? 35 | end 36 | end 37 | 38 | private 39 | 40 | def concurrently_enabled?(last_arg) 41 | last_arg.hash_type? && last_arg.each_descendant.any? do |node| 42 | next unless node.sym_type? 43 | 44 | concurrently?(node) 45 | end 46 | end 47 | 48 | def ddl_transaction_disabled? 49 | @disable_ddl_transaction 50 | end 51 | 52 | def concurrently?(node) 53 | node.children.any? { |sym| sym == :concurrently } 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/tasks/auto_annotate_models.rake: -------------------------------------------------------------------------------- 1 | if Rails.env.development? 2 | require 'annotate' 3 | task :set_annotation_options do 4 | # You can override any of these by setting an environment variable of the 5 | # same name. 6 | Annotate.set_defaults( 7 | 'additional_file_patterns' => [], 8 | 'routes' => 'false', 9 | 'models' => 'true', 10 | 'position_in_routes' => 'before', 11 | 'position_in_class' => 'before', 12 | 'position_in_test' => 'before', 13 | 'position_in_fixture' => 'before', 14 | 'position_in_factory' => 'before', 15 | 'position_in_serializer' => 'before', 16 | 'show_foreign_keys' => 'true', 17 | 'show_complete_foreign_keys' => 'false', 18 | 'show_indexes' => 'true', 19 | 'simple_indexes' => 'true', 20 | 'model_dir' => 'app/models', 21 | 'root_dir' => '', 22 | 'include_version' => 'false', 23 | 'require' => '', 24 | 'exclude_tests' => 'false', 25 | 'exclude_fixtures' => 'false', 26 | 'exclude_factories' => 'false', 27 | 'exclude_serializers' => 'false', 28 | 'exclude_scaffolds' => 'true', 29 | 'exclude_controllers' => 'true', 30 | 'exclude_helpers' => 'true', 31 | 'exclude_sti_subclasses' => 'false', 32 | 'ignore_model_sub_dir' => 'false', 33 | 'ignore_columns' => nil, 34 | 'ignore_routes' => nil, 35 | 'ignore_unknown_models' => 'false', 36 | 'hide_limit_column_types' => 'integer,bigint,boolean', 37 | 'hide_default_column_types' => 'json,jsonb,hstore,text', 38 | 'skip_on_db_migrate' => 'false', 39 | 'format_bare' => 'true', 40 | 'format_rdoc' => 'false', 41 | 'format_markdown' => 'false', 42 | 'sort' => 'false', 43 | 'force' => 'false', 44 | 'frozen' => 'false', 45 | 'classified_sort' => 'true', 46 | 'trace' => 'false', 47 | 'wrapper_open' => nil, 48 | 'wrapper_close' => nil, 49 | 'with_comment' => 'true' 50 | ) 51 | end 52 | 53 | Annotate.load_tasks 54 | end 55 | -------------------------------------------------------------------------------- /spec/rubocop/cop/migration/add_index_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | require 'rubocop' 6 | require 'rubocop/rspec/support' 7 | 8 | require_relative '../../../../rubocop/cop/migration/add_index' 9 | 10 | RSpec.describe RuboCop::Cop::Migration::AddIndex do 11 | include CopHelper 12 | include RuboCop::RSpec::ExpectOffense 13 | 14 | let(:cop) { described_class.new } 15 | 16 | before { allow(cop).to receive(:in_migration?).and_return(true) } 17 | 18 | context 'with concurrently and without disable_ddl_transaction!' do 19 | it 'reports an offense' do 20 | expect_offense(<<~RUBY) 21 | class Migration 22 | ^^^^^^^^^^^^^^^ Prefer using disable_ddl_transaction! and { algorithm: :concurrently } when creating an index 23 | def change 24 | add_index :table, :column, algorithm: :concurrently 25 | end 26 | end 27 | RUBY 28 | end 29 | end 30 | 31 | context 'without concurrently and without disable_ddl_transaction!' do 32 | it 'reports an offense' do 33 | expect_offense(<<~RUBY) 34 | class Migration 35 | ^^^^^^^^^^^^^^^ Prefer using disable_ddl_transaction! and { algorithm: :concurrently } when creating an index 36 | def change 37 | add_index :table, :column 38 | ^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using { algorithm: :concurrently } when creating an index 39 | end 40 | end 41 | RUBY 42 | end 43 | end 44 | 45 | context 'with concurrently and with disable_ddl_transaction!' do 46 | it 'does not report an offense' do 47 | expect_no_offenses(<<~RUBY) 48 | class Migration 49 | disable_ddl_transaction! 50 | def change 51 | add_index :table, :column, algorithm: :concurrently 52 | end 53 | end 54 | RUBY 55 | end 56 | end 57 | 58 | context 'without concurrently and with disable_ddl_transaction!' do 59 | it 'reports an offense' do 60 | expect_offense(<<~RUBY) 61 | class Migration 62 | disable_ddl_transaction! 63 | def change 64 | add_index :table, :column 65 | ^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using { algorithm: :concurrently } when creating an index 66 | end 67 | end 68 | RUBY 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # The test environment is used exclusively to run your application's 2 | # test suite. You never need to work with it otherwise. Remember that 3 | # your test database is "scratch space" for the test suite and is wiped 4 | # and recreated between test runs. Don't rely on the data there! 5 | 6 | Rails.application.configure do 7 | # Settings specified here will take precedence over those in config/application.rb. 8 | 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. This avoids loading your whole application 12 | # just for the purpose of running a single test. If you are using a tool that 13 | # preloads Rails for running tests, you may have to set it to true. 14 | config.eager_load = false 15 | 16 | # Configure public file server for tests with Cache-Control for performance. 17 | config.public_file_server.enabled = true 18 | config.public_file_server.headers = { 19 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 20 | } 21 | 22 | # Show full error reports and disable caching. 23 | config.consider_all_requests_local = true 24 | config.action_controller.perform_caching = false 25 | config.cache_store = :null_store 26 | 27 | # Raise exceptions instead of rendering exception templates. 28 | config.action_dispatch.show_exceptions = false 29 | 30 | # Disable request forgery protection in test environment. 31 | config.action_controller.allow_forgery_protection = false 32 | 33 | # Store uploaded files on the local file system in a temporary directory. 34 | config.active_storage.service = :test 35 | 36 | config.action_mailer.perform_caching = false 37 | 38 | # Tell Action Mailer not to deliver emails to the real world. 39 | # The :test delivery method accumulates sent emails in the 40 | # ActionMailer::Base.deliveries array. 41 | config.action_mailer.delivery_method = :test 42 | 43 | # Print deprecation notices to the stderr. 44 | config.active_support.deprecation = :stderr 45 | 46 | # Raises error for missing translations. 47 | config.action_view.raise_on_missing_translations = true 48 | 49 | config.after_initialize do 50 | Bullet.enable = true 51 | Bullet.bullet_logger = false 52 | Bullet.rails_logger = true 53 | Bullet.n_plus_one_query_enable = true 54 | Bullet.unused_eager_loading_enable = true 55 | Bullet.counter_cache_enable = true 56 | Bullet.raise = true 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | # Run rails dev:cache to toggle caching. 17 | if Rails.root.join('tmp/caching-dev.txt').exist? 18 | config.cache_store = :memory_store 19 | config.public_file_server.headers = { 20 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 21 | } 22 | else 23 | config.action_controller.perform_caching = false 24 | 25 | config.cache_store = :null_store 26 | end 27 | 28 | # Store uploaded files on the local file system (see config/storage.yml for options). 29 | config.active_storage.service = :local 30 | 31 | # Mailer 32 | config.action_mailer.delivery_method = :letter_opener 33 | config.action_mailer.default_url_options = { host: 'localhost' } 34 | config.action_mailer.perform_deliveries = true 35 | config.action_mailer.raise_delivery_errors = false 36 | config.action_mailer.perform_caching = false 37 | config.action_mailer.preview_path = Rails.root.join('spec/mailers/previews') 38 | 39 | # Print deprecation notices to the Rails logger. 40 | config.active_support.deprecation = :log 41 | 42 | # Raise an error on page load if there are pending migrations. 43 | config.active_record.migration_error = :page_load 44 | 45 | # Highlight code that triggered database queries in logs. 46 | config.active_record.verbose_query_logs = true 47 | 48 | # Raises error for missing translations. 49 | # config.action_view.raise_on_missing_translations = true 50 | 51 | # Use an evented file watcher to asynchronously detect changes in source code, 52 | # routes, locales, etc. This feature depends on the listen gem. 53 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 54 | 55 | config.after_initialize do 56 | Bullet.enable = true 57 | Bullet.alert = false 58 | Bullet.bullet_logger = true 59 | Bullet.console = true 60 | Bullet.rails_logger = true 61 | Bullet.add_footer = true 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/requests/users/update_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe 'Update user mutation request', type: :request do 4 | subject(:request) do 5 | graphql_request(request_body, variables: request_variables, headers: auth_headers) 6 | end 7 | 8 | let!(:user) { create(:user) } 9 | let(:first_name) { 'Obi Wan' } 10 | let(:last_name) { 'Kenobi' } 11 | let(:email) { 'obikenobi@rebel.com' } 12 | let(:password) { 'abcd1234' } 13 | let(:request_body) do 14 | <<~GQL 15 | mutation UpdateUser( 16 | $firstName: String, 17 | $lastName: String, 18 | $email: String, 19 | $password: String 20 | ) { 21 | updateUser(input: { 22 | firstName: $firstName, 23 | lastName: $lastName, 24 | email: $email, 25 | password: $password 26 | }) { 27 | user { 28 | id 29 | firstName 30 | lastName 31 | email 32 | } 33 | } 34 | } 35 | GQL 36 | end 37 | let(:request_variables) do 38 | { 39 | firstName: first_name, 40 | lastName: last_name, 41 | email: email, 42 | password: password 43 | } 44 | end 45 | let(:response_content) { json.dig(:data, :updateUser) } 46 | 47 | context 'with valid params' do 48 | let(:updated_user) { user.reload } 49 | 50 | specify do 51 | request 52 | 53 | expect(errors).to be_nil 54 | end 55 | 56 | specify do 57 | request 58 | 59 | expect(response).to have_http_status(:ok) 60 | end 61 | 62 | it 'updates current user' do 63 | request 64 | 65 | expect(updated_user.first_name).to eq(first_name) 66 | end 67 | 68 | it 'returns the user data' do 69 | request 70 | 71 | expect(response_content[:user]).to include_json( 72 | id: updated_user.id.to_s, 73 | firstName: updated_user.first_name, 74 | lastName: updated_user.last_name, 75 | email: updated_user.email 76 | ) 77 | end 78 | end 79 | 80 | context 'with invalid params' do 81 | context 'when the email is missing' do 82 | let(:email) { '' } 83 | 84 | it 'returns an error message' do 85 | request 86 | 87 | expect(first_error_message).not_to be_nil 88 | end 89 | 90 | it 'does not update current user' do 91 | expect { 92 | request 93 | }.not_to change(user.reload, :email) 94 | end 95 | end 96 | 97 | context 'when the email is taken' do 98 | before { create(:user, email: email) } 99 | 100 | it 'returns an error message' do 101 | request 102 | 103 | expect(first_error_message).not_to be_nil 104 | end 105 | 106 | it 'does not update current user' do 107 | expect { 108 | request 109 | }.not_to change(user.reload, :email) 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/requests/users/create_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe 'Create user mutation request', type: :request do 4 | subject(:request) { graphql_request(mutation_request, variables: request_variables) } 5 | 6 | let(:first_name) { 'Obi Wan' } 7 | let(:last_name) { 'Kenobi' } 8 | let(:email) { 'obikenobi@rebel.com' } 9 | let(:password) { 'abcd1234' } 10 | 11 | let(:mutation_request) do 12 | <<~GQL 13 | mutation SignUp( 14 | $email: String!, 15 | $password: String!, 16 | $firstName: String!, 17 | $lastName: String! 18 | ) { 19 | createUser(input: { 20 | email: $email, 21 | password: $password, 22 | firstName: $firstName, 23 | lastName: $lastName 24 | }) { 25 | user { 26 | id 27 | firstName 28 | lastName 29 | email 30 | } 31 | token 32 | } 33 | } 34 | GQL 35 | end 36 | 37 | let(:request_variables) do 38 | { 39 | firstName: first_name, 40 | lastName: last_name, 41 | email: email, 42 | password: password 43 | } 44 | end 45 | 46 | let(:response_content) { json.dig(:data, :createUser) } 47 | 48 | context 'with valid params' do 49 | let(:created_user) { User.last } 50 | 51 | specify do 52 | request 53 | 54 | expect(errors).to be_nil 55 | end 56 | 57 | specify do 58 | request 59 | 60 | expect(response).to have_http_status(:ok) 61 | end 62 | 63 | it 'creates a user' do 64 | expect { 65 | request 66 | }.to change(User, :count).by(1) 67 | end 68 | 69 | it 'returns the user data' do 70 | request 71 | 72 | expect(response_content[:user]).to include_json( 73 | id: created_user.id.to_s, 74 | firstName: created_user.first_name, 75 | lastName: created_user.last_name, 76 | email: created_user.email 77 | ) 78 | end 79 | 80 | specify do 81 | request 82 | 83 | token = response_content[:token] 84 | 85 | expect(token).not_to be_nil 86 | end 87 | 88 | it 'sets the authentication headers' do 89 | request 90 | 91 | token = response_content[:token] 92 | 93 | expect(AuthToken.verify(token)).to eq(created_user) 94 | end 95 | end 96 | 97 | context 'with invalid params' do 98 | context 'when the email is missing' do 99 | let(:email) { '' } 100 | 101 | it 'returns an error message' do 102 | request 103 | 104 | expect(first_error_message).not_to be_nil 105 | end 106 | 107 | it 'does not create a user' do 108 | expect { 109 | request 110 | }.not_to change(User, :count) 111 | end 112 | end 113 | 114 | context 'when the password is missing' do 115 | let(:password) { '' } 116 | 117 | it 'returns an error message' do 118 | request 119 | 120 | expect(first_error_message).not_to be_nil 121 | end 122 | 123 | it 'does not create a user' do 124 | expect { 125 | request 126 | }.not_to change(User, :count) 127 | end 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /spec/requests/users/sign_in_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe 'Sign in user mutation request', type: :request do 4 | subject(:request) do 5 | graphql_request(request_body, variables: request_variables) 6 | end 7 | 8 | let!(:user) { create(:user, email: email, password: password) } 9 | 10 | let(:email) { 'user@email.com' } 11 | let(:password) { 'abcd1234' } 12 | 13 | let(:request_body) do 14 | <<~GQL 15 | mutation SignIn($email: String!, $password: String!) { 16 | signInUser(input: {email: $email, password: $password}) { 17 | user { 18 | id 19 | firstName 20 | lastName 21 | email 22 | } 23 | token 24 | } 25 | } 26 | GQL 27 | end 28 | 29 | let(:request_variables) do 30 | { 31 | email: email_param, 32 | password: password_param 33 | } 34 | end 35 | 36 | let(:email_param) { email } 37 | let(:password_param) { password } 38 | 39 | let(:response_content) { json.dig(:data, :signInUser) } 40 | 41 | context 'with valid params' do 42 | specify do 43 | request 44 | 45 | expect(errors).to be_nil 46 | end 47 | 48 | specify do 49 | request 50 | 51 | expect(response).to have_http_status(:ok) 52 | end 53 | 54 | it 'does not create a new user' do 55 | expect { 56 | request 57 | }.not_to change(User, :count) 58 | end 59 | 60 | it 'returns the user data' do 61 | request 62 | 63 | expect(response_content[:user]).to include_json( 64 | id: user.id.to_s, 65 | firstName: user.first_name, 66 | lastName: user.last_name, 67 | email: user.email 68 | ) 69 | end 70 | 71 | specify do 72 | request 73 | 74 | token = response_content[:token] 75 | 76 | expect(token).not_to be_nil 77 | end 78 | 79 | it 'sets the authentication headers' do 80 | request 81 | 82 | token = response_content[:token] 83 | 84 | expect(AuthToken.verify(token)).to eq(user) 85 | end 86 | end 87 | 88 | context 'with invalid params' do 89 | let(:first_error) { errors.first[:message] } 90 | 91 | shared_examples_for 'does not sign in' do 92 | it 'returns an error message' do 93 | request 94 | 95 | expect(first_error).to eq(I18n.t('errors.invalid_credentials')) 96 | end 97 | 98 | it 'does not return the user sign in info' do 99 | request 100 | 101 | expect(response_content).to be_nil 102 | end 103 | end 104 | 105 | context 'when the email is missing' do 106 | let(:email_param) { '' } 107 | 108 | it_behaves_like 'does not sign in' 109 | end 110 | 111 | context 'when the password is missing' do 112 | let(:password_param) { '' } 113 | 114 | it_behaves_like 'does not sign in' 115 | end 116 | 117 | context 'when the email is not registered' do 118 | let(:email_param) { 'not@registered.com' } 119 | 120 | it_behaves_like 'does not sign in' 121 | end 122 | 123 | context 'when the password is invalid for the user' do 124 | let(:password_param) { 'notHisPassword' } 125 | 126 | it_behaves_like 'does not sign in' 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 || ">= 0.a" 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../../Gemfile", __FILE__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_version 64 | @bundler_version ||= begin 65 | env_var_version || cli_arg_version || 66 | lockfile_version || "#{Gem::Requirement.default}.a" 67 | end 68 | end 69 | 70 | def load_bundler! 71 | ENV["BUNDLE_GEMFILE"] ||= gemfile 72 | 73 | # must dup string for RG < 1.8 compatibility 74 | activate_bundler(bundler_version.dup) 75 | end 76 | 77 | def activate_bundler(bundler_version) 78 | if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new("2.0") 79 | bundler_version = "< 2" 80 | end 81 | gem_error = activation_error_handling do 82 | gem "bundler", bundler_version 83 | end 84 | return if gem_error.nil? 85 | require_error = activation_error_handling do 86 | require "bundler/version" 87 | end 88 | return if require_error.nil? && Gem::Requirement.new(bundler_version).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 89 | warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`" 90 | exit 42 91 | end 92 | 93 | def activation_error_handling 94 | yield 95 | nil 96 | rescue StandardError, LoadError => e 97 | e 98 | end 99 | end 100 | 101 | m.load_bundler! 102 | 103 | if m.invoked_as_script? 104 | load Gem.bin_path("bundler", "bundle") 105 | end 106 | -------------------------------------------------------------------------------- /.reek.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | ### Generic smell configuration 4 | 5 | detectors: 6 | Attribute: 7 | enabled: true 8 | BooleanParameter: 9 | enabled: true 10 | ClassVariable: 11 | enabled: true 12 | ControlParameter: 13 | enabled: true 14 | exclude: 15 | - resolve 16 | DataClump: 17 | enabled: true 18 | max_copies: 2 19 | min_clump_size: 2 20 | DuplicateMethodCall: 21 | enabled: true 22 | max_calls: 1 23 | FeatureEnvy: 24 | enabled: true 25 | exclude: 26 | - GraphqlRequestResolver#resolve_execute 27 | - GraphqlRequestResolver#resolve_multiplex 28 | - GraphqlUtils::ParseErrorBetterPrinted#initialize 29 | InstanceVariableAssumption: 30 | enabled: false 31 | IrresponsibleModule: 32 | enabled: false 33 | LongParameterList: 34 | enabled: true 35 | max_params: 3 36 | overrides: 37 | initialize: 38 | max_params: 5 39 | LongYieldList: 40 | enabled: true 41 | max_params: 3 42 | ManualDispatch: 43 | enabled: true 44 | MissingSafeMethod: 45 | enabled: false 46 | ModuleInitialize: 47 | enabled: true 48 | NestedIterators: 49 | enabled: true 50 | ignore_iterators: 51 | - tap 52 | max_allowed_nesting: 1 53 | NilCheck: 54 | enabled: false 55 | RepeatedConditional: 56 | enabled: true 57 | max_ifs: 2 58 | SubclassedFromCoreClass: 59 | enabled: true 60 | TooManyConstants: 61 | enabled: true 62 | max_constants: 5 63 | TooManyInstanceVariables: 64 | enabled: true 65 | max_instance_variables: 4 66 | TooManyMethods: 67 | enabled: true 68 | max_methods: 15 69 | TooManyStatements: 70 | enabled: true 71 | exclude: 72 | - initialize 73 | max_statements: 9 74 | UncommunicativeMethodName: 75 | enabled: true 76 | reject: 77 | - "/^[a-z]$/" 78 | - "/[0-9]$/" 79 | - "/[A-Z]/" 80 | UncommunicativeModuleName: 81 | accept: 82 | - "/V[0-9]/" 83 | enabled: true 84 | reject: 85 | - '/^.$/' 86 | - '/[0-9]$/' 87 | accept: 88 | - "/V[0-9]/" 89 | UncommunicativeParameterName: 90 | enabled: true 91 | reject: 92 | - /^.$/ 93 | - "/[0-9]$/" 94 | - "/[A-Z]/" 95 | - /^_/ 96 | UncommunicativeVariableName: 97 | accept: 98 | - /^_$/ 99 | - e 100 | enabled: true 101 | reject: 102 | - /^.$/ 103 | - "/[0-9]$/" 104 | - "/[A-Z]/" 105 | UnusedParameters: 106 | enabled: true 107 | UnusedPrivateMethod: 108 | enabled: false 109 | UtilityFunction: 110 | enabled: true 111 | exclude: 112 | - Loaders::AssociationLoader#cache_key 113 | - resolve 114 | public_methods_only: false 115 | 116 | ### Directory specific configuration 117 | 118 | directories: 119 | app/graphql/query_analyzers/concerns: 120 | UtilityFunction: 121 | enabled: false 122 | app/jobs: 123 | FeatureEnvy: 124 | enabled: false 125 | UtilityFunction: 126 | enabled: false 127 | app/presenters: 128 | FeatureEnvy: 129 | enabled: false 130 | UtilityFunction: 131 | enabled: false 132 | app/services: 133 | FeatureEnvy: 134 | enabled: false 135 | UtilityFunction: 136 | enabled: false 137 | app/uploaders: 138 | UtilityFunction: 139 | enabled: false 140 | db/migrate: 141 | UtilityFunction: 142 | enabled: false 143 | FeatureEnvy: 144 | enabled: false 145 | TooManyStatements: 146 | enabled: false 147 | UncommunicativeVariableName: 148 | enabled: false 149 | "rubocop": 150 | UtilityFunction: 151 | enabled: false 152 | FeatureEnvy: 153 | enabled: false 154 | -------------------------------------------------------------------------------- /PAGINATION.md: -------------------------------------------------------------------------------- 1 | Pagination 2 | -------- 3 | 4 | For cursor pagination, you need to define a connection for the model you are querying. 5 | 6 | For example if you need to return the collection of all the User records in the app: 7 | ``` 8 | field :users_connection, Types::CustomTypes::UserType.connection_type, null: false 9 | 10 | def users_connection 11 | User.all 12 | end 13 | ``` 14 | With this defined, the query would need to have the following structure: 15 | ``` 16 | query { 17 | usersConnection { 18 | edges { 19 | node { 20 | id 21 | firstName 22 | } 23 | } 24 | } 25 | } 26 | ``` 27 | and the response will have this structure: 28 | ``` 29 | { 30 | "data": { 31 | "userConnection": { 32 | "edges": [ 33 | { 34 | "node": { 35 | "id": "1", 36 | "firstName": "Myesha", 37 | "lastName": "Stoltenberg" 38 | } 39 | }, 40 | { 41 | "node": { 42 | "id": "2", 43 | "firstName": "Oscar", 44 | "lastName": "Kozey" 45 | } 46 | } 47 | ] 48 | } 49 | } 50 | } 51 | ```` 52 | With this set up we can request useful information for pagination 53 | 54 | at the connection level: 55 | ``` 56 | pageInfo { 57 | 58 | startCursor: the cursor of the first returned value 59 | 60 | endCursor: the cursor of the last returned value 61 | 62 | hasPreviousPage: a boolean indicating if there are records before the first cursor 63 | 64 | hasNextPage: a boolean indicating if there are records after the last cursor 65 | } 66 | 67 | totalCount: number of values that the collection contains 68 | 69 | ``` 70 | 71 | in the node level: 72 | ``` 73 | cursor: the cursor of the actual node 74 | ``` 75 | 76 | Adding this to the request would look something like: 77 | ``` 78 | query { 79 | usersConnection{ 80 | pageInfo { 81 | startCursor 82 | endCursor 83 | hasPreviousPage 84 | hasNextPage 85 | } 86 | edges { 87 | cursor 88 | node { 89 | id 90 | firstName 91 | lastName 92 | } 93 | } 94 | totalCount 95 | } 96 | } 97 | ``` 98 | with this response: 99 | ``` 100 | { 101 | "data": { 102 | "usersConnection": { 103 | "pageInfo": { 104 | "startCursor": "MQ", 105 | "endCursor": "Mw", 106 | "hasPreviousPage": false, 107 | "hasNextPage": true 108 | }, 109 | "totalCount": 21, 110 | "edges": [ 111 | { 112 | "node": { 113 | "id": "1", 114 | "firstName": "Myesha", 115 | "lastName": "Stoltenberg" 116 | }, 117 | "cursor": "MQ" 118 | }, 119 | { 120 | "node": { 121 | "id": "2", 122 | ... 123 | ] 124 | } 125 | } 126 | } 127 | ``` 128 | 129 | Finally, the connection could receive some values as parameters to customize de response for only returning the desirable records. 130 | 131 | `first: int` returns the first given number of records 132 | `last: int` returns the last given number of records 133 | `after: String` returns the records after the given cursor 134 | `before: String` returns the records before the given cursor 135 | 136 | For example: `usersConnection(first: 5 after: "Mw"){ ... }` returns the first 5 records after the node with the "Mw" cursor. 137 | 138 | 139 | It's important to take into account that `hasPreviousPage` would always be false unless you are paginating forward (`UserConnection(first: 5)`), and `hasNextPage` would always be false unless paginating backward (`UserConnection(last: 5)`). This is not an implementation error, it's due to how connections are designed. 140 | 141 | If you need the hasPreviosPage value when paginating forward, you could make an additional query requesting the last 0 values before the node of which we want to know if there are values before: 142 | ``` 143 | query { 144 | userConnection(last: 0, before: "MQ") { 145 | pageInfo { 146 | hasPreviousPage 147 | } 148 | } 149 | } 150 | ``` 151 | or the first 0 values after the desired node when we want to know if it `hasNextPage` while paginating backward. 152 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | 16 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 17 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 18 | # config.require_master_key = true 19 | 20 | # Disable serving static files from the `/public` folder by default since 21 | # Apache or NGINX already handles this. 22 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 23 | 24 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 25 | # config.action_controller.asset_host = 'http://assets.example.com' 26 | 27 | # Specifies the header that your server uses for sending files. 28 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 29 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 30 | 31 | # Store uploaded files on the local file system (see config/storage.yml for options). 32 | config.active_storage.service = :local 33 | 34 | # Mount Action Cable outside main process or domain. 35 | # config.action_cable.mount_path = nil 36 | # config.action_cable.url = 'wss://example.com/cable' 37 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 38 | 39 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 40 | # config.force_ssl = true 41 | 42 | # Use the lowest log level to ensure availability of diagnostic information 43 | # when problems arise. 44 | config.log_level = :debug 45 | 46 | # Prepend all log lines with the following tags. 47 | config.log_tags = [:request_id] 48 | 49 | # Use a different cache store in production. 50 | # config.cache_store = :mem_cache_store 51 | 52 | # Use a real queuing backend for Active Job (and separate queues per environment). 53 | # config.active_job.queue_adapter = :resque 54 | # config.active_job.queue_name_prefix = "rails_api_boilerplate_production" 55 | 56 | config.action_mailer.perform_caching = false 57 | 58 | # Ignore bad email addresses and do not raise email delivery errors. 59 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 60 | # config.action_mailer.raise_delivery_errors = false 61 | 62 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 63 | # the I18n.default_locale when a translation cannot be found). 64 | config.i18n.fallbacks = true 65 | 66 | # Send deprecation notices to registered listeners. 67 | config.active_support.deprecation = :notify 68 | 69 | # Use default logging formatter so that PID and timestamp are not suppressed. 70 | config.log_formatter = ::Logger::Formatter.new 71 | 72 | # Use a different logger for distributed setups. 73 | # require 'syslog/logger' 74 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 75 | 76 | if ENV['RAILS_LOG_TO_STDOUT'].present? 77 | logger = ActiveSupport::Logger.new($stdout) 78 | logger.formatter = config.log_formatter 79 | config.logger = ActiveSupport::TaggedLogging.new(logger) 80 | end 81 | 82 | # Do not dump schema after migrations. 83 | config.active_record.dump_schema_after_migration = false 84 | 85 | # Inserts middleware to perform automatic connection switching. 86 | # The `database_selector` hash is used to pass options to the DatabaseSelector 87 | # middleware. The `delay` is used to determine how long to wait after a write 88 | # to send a subsequent read to the primary. 89 | # 90 | # The `database_resolver` class is used by the middleware to determine which 91 | # database is appropriate to use based on the time delay. 92 | # 93 | # The `database_resolver_context` class is used by the middleware to set 94 | # timestamps for the last write to the primary. The resolver uses the context 95 | # class timestamps to determine how long to wait before reading from the 96 | # replica. 97 | # 98 | # By default Rails will store a last write timestamp in the session. The 99 | # DatabaseSelector middleware is designed as such you can define your own 100 | # strategy for connection switching and pass that into the middleware through 101 | # these configuration options. 102 | # config.active_record.database_selector = { delay: 2.seconds } 103 | # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver 104 | # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session 105 | end 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 |
](https://loopstudio.dev)
163 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actioncable (6.1.5.1)
5 | actionpack (= 6.1.5.1)
6 | activesupport (= 6.1.5.1)
7 | nio4r (~> 2.0)
8 | websocket-driver (>= 0.6.1)
9 | actionmailbox (6.1.5.1)
10 | actionpack (= 6.1.5.1)
11 | activejob (= 6.1.5.1)
12 | activerecord (= 6.1.5.1)
13 | activestorage (= 6.1.5.1)
14 | activesupport (= 6.1.5.1)
15 | mail (>= 2.7.1)
16 | actionmailer (6.1.5.1)
17 | actionpack (= 6.1.5.1)
18 | actionview (= 6.1.5.1)
19 | activejob (= 6.1.5.1)
20 | activesupport (= 6.1.5.1)
21 | mail (~> 2.5, >= 2.5.4)
22 | rails-dom-testing (~> 2.0)
23 | actionpack (6.1.5.1)
24 | actionview (= 6.1.5.1)
25 | activesupport (= 6.1.5.1)
26 | rack (~> 2.0, >= 2.0.9)
27 | rack-test (>= 0.6.3)
28 | rails-dom-testing (~> 2.0)
29 | rails-html-sanitizer (~> 1.0, >= 1.2.0)
30 | actiontext (6.1.5.1)
31 | actionpack (= 6.1.5.1)
32 | activerecord (= 6.1.5.1)
33 | activestorage (= 6.1.5.1)
34 | activesupport (= 6.1.5.1)
35 | nokogiri (>= 1.8.5)
36 | actionview (6.1.5.1)
37 | activesupport (= 6.1.5.1)
38 | builder (~> 3.1)
39 | erubi (~> 1.4)
40 | rails-dom-testing (~> 2.0)
41 | rails-html-sanitizer (~> 1.1, >= 1.2.0)
42 | activejob (6.1.5.1)
43 | activesupport (= 6.1.5.1)
44 | globalid (>= 0.3.6)
45 | activemodel (6.1.5.1)
46 | activesupport (= 6.1.5.1)
47 | activerecord (6.1.5.1)
48 | activemodel (= 6.1.5.1)
49 | activesupport (= 6.1.5.1)
50 | activestorage (6.1.5.1)
51 | actionpack (= 6.1.5.1)
52 | activejob (= 6.1.5.1)
53 | activerecord (= 6.1.5.1)
54 | activesupport (= 6.1.5.1)
55 | marcel (~> 1.0)
56 | mini_mime (>= 1.1.0)
57 | activesupport (6.1.5.1)
58 | concurrent-ruby (~> 1.0, >= 1.0.2)
59 | i18n (>= 1.6, < 2)
60 | minitest (>= 5.1)
61 | tzinfo (~> 2.0)
62 | zeitwerk (~> 2.3)
63 | addressable (2.8.0)
64 | public_suffix (>= 2.0.2, < 5.0)
65 | annotate (3.2.0)
66 | activerecord (>= 3.2, < 8.0)
67 | rake (>= 10.4, < 14.0)
68 | ast (2.4.2)
69 | bcrypt (3.1.18)
70 | bootsnap (1.12.0)
71 | msgpack (~> 1.2)
72 | builder (3.2.4)
73 | bullet (7.0.2)
74 | activesupport (>= 3.0.0)
75 | uniform_notifier (~> 1.11)
76 | byebug (11.1.3)
77 | concurrent-ruby (1.1.10)
78 | crack (0.4.5)
79 | rexml
80 | crass (1.0.6)
81 | diff-lcs (1.5.0)
82 | docile (1.4.0)
83 | dotenv (2.7.6)
84 | dotenv-rails (2.7.6)
85 | dotenv (= 2.7.6)
86 | railties (>= 3.2)
87 | erubi (1.10.0)
88 | factory_bot (6.2.1)
89 | activesupport (>= 5.0.0)
90 | factory_bot_rails (6.2.0)
91 | factory_bot (~> 6.2.0)
92 | railties (>= 5.0.0)
93 | faker (2.21.0)
94 | i18n (>= 1.8.11, < 2)
95 | ffi (1.15.5)
96 | globalid (1.0.0)
97 | activesupport (>= 5.0)
98 | graphiql-rails (1.8.0)
99 | railties
100 | sprockets-rails
101 | graphql (2.0.11)
102 | graphql-batch (0.5.1)
103 | graphql (>= 1.10, < 3)
104 | promise.rb (~> 0.7.2)
105 | hashdiff (1.0.1)
106 | i18n (1.10.0)
107 | concurrent-ruby (~> 1.0)
108 | jwt (2.4.1)
109 | kwalify (0.7.2)
110 | launchy (2.5.0)
111 | addressable (~> 2.7)
112 | letter_opener (1.8.1)
113 | launchy (>= 2.2, < 3)
114 | listen (3.7.1)
115 | rb-fsevent (~> 0.10, >= 0.10.3)
116 | rb-inotify (~> 0.9, >= 0.9.10)
117 | loofah (2.18.0)
118 | crass (~> 1.0.2)
119 | nokogiri (>= 1.5.9)
120 | mail (2.7.1)
121 | mini_mime (>= 0.1.1)
122 | marcel (1.0.2)
123 | method_source (1.0.0)
124 | mini_mime (1.1.2)
125 | mini_portile2 (2.8.0)
126 | minitest (5.15.0)
127 | msgpack (1.5.2)
128 | nio4r (2.5.8)
129 | nokogiri (1.13.6)
130 | mini_portile2 (~> 2.8.0)
131 | racc (~> 1.4)
132 | parallel (1.22.1)
133 | parser (3.1.2.0)
134 | ast (~> 2.4.1)
135 | pg (1.4.1)
136 | promise.rb (0.7.4)
137 | public_suffix (4.0.7)
138 | puma (5.6.4)
139 | nio4r (~> 2.0)
140 | racc (1.6.0)
141 | rack (2.2.3.1)
142 | rack-cors (1.1.1)
143 | rack (>= 2.0.0)
144 | rack-test (1.1.0)
145 | rack (>= 1.0, < 3)
146 | rails (6.1.5.1)
147 | actioncable (= 6.1.5.1)
148 | actionmailbox (= 6.1.5.1)
149 | actionmailer (= 6.1.5.1)
150 | actionpack (= 6.1.5.1)
151 | actiontext (= 6.1.5.1)
152 | actionview (= 6.1.5.1)
153 | activejob (= 6.1.5.1)
154 | activemodel (= 6.1.5.1)
155 | activerecord (= 6.1.5.1)
156 | activestorage (= 6.1.5.1)
157 | activesupport (= 6.1.5.1)
158 | bundler (>= 1.15.0)
159 | railties (= 6.1.5.1)
160 | sprockets-rails (>= 2.0.0)
161 | rails-dom-testing (2.0.3)
162 | activesupport (>= 4.2.0)
163 | nokogiri (>= 1.6)
164 | rails-html-sanitizer (1.4.2)
165 | loofah (~> 2.3)
166 | railties (6.1.5.1)
167 | actionpack (= 6.1.5.1)
168 | activesupport (= 6.1.5.1)
169 | method_source
170 | rake (>= 12.2)
171 | thor (~> 1.0)
172 | rainbow (3.1.1)
173 | rake (13.0.6)
174 | rb-fsevent (0.11.1)
175 | rb-inotify (0.10.1)
176 | ffi (~> 1.0)
177 | redis (4.6.0)
178 | reek (6.1.1)
179 | kwalify (~> 0.7.0)
180 | parser (~> 3.1.0)
181 | rainbow (>= 2.0, < 4.0)
182 | regexp_parser (2.5.0)
183 | rexml (3.2.5)
184 | rspec-core (3.11.0)
185 | rspec-support (~> 3.11.0)
186 | rspec-expectations (3.11.0)
187 | diff-lcs (>= 1.2.0, < 2.0)
188 | rspec-support (~> 3.11.0)
189 | rspec-json_expectations (2.2.0)
190 | rspec-mocks (3.11.1)
191 | diff-lcs (>= 1.2.0, < 2.0)
192 | rspec-support (~> 3.11.0)
193 | rspec-rails (5.1.2)
194 | actionpack (>= 5.2)
195 | activesupport (>= 5.2)
196 | railties (>= 5.2)
197 | rspec-core (~> 3.10)
198 | rspec-expectations (~> 3.10)
199 | rspec-mocks (~> 3.10)
200 | rspec-support (~> 3.10)
201 | rspec-support (3.11.0)
202 | rubocop (1.30.1)
203 | parallel (~> 1.10)
204 | parser (>= 3.1.0.0)
205 | rainbow (>= 2.2.2, < 4.0)
206 | regexp_parser (>= 1.8, < 3.0)
207 | rexml (>= 3.2.5, < 4.0)
208 | rubocop-ast (>= 1.18.0, < 2.0)
209 | ruby-progressbar (~> 1.7)
210 | unicode-display_width (>= 1.4.0, < 3.0)
211 | rubocop-ast (1.18.0)
212 | parser (>= 3.1.1.0)
213 | rubocop-rails (2.15.0)
214 | activesupport (>= 4.2.0)
215 | rack (>= 1.1)
216 | rubocop (>= 1.7.0, < 2.0)
217 | rubocop-rspec (2.11.1)
218 | rubocop (~> 1.19)
219 | ruby-progressbar (1.11.0)
220 | sentry-rails (5.3.1)
221 | railties (>= 5.0)
222 | sentry-ruby-core (~> 5.3.1)
223 | sentry-ruby (5.3.1)
224 | concurrent-ruby (~> 1.0, >= 1.0.2)
225 | sentry-ruby-core (= 5.3.1)
226 | sentry-ruby-core (5.3.1)
227 | concurrent-ruby
228 | shoulda-matchers (5.1.0)
229 | activesupport (>= 5.2.0)
230 | simplecov (0.21.2)
231 | docile (~> 1.1)
232 | simplecov-html (~> 0.11)
233 | simplecov_json_formatter (~> 0.1)
234 | simplecov-html (0.12.3)
235 | simplecov_json_formatter (0.1.4)
236 | spring (2.1.1)
237 | spring-watcher-listen (2.0.1)
238 | listen (>= 2.7, < 4.0)
239 | spring (>= 1.2, < 3.0)
240 | sprockets (4.0.3)
241 | concurrent-ruby (~> 1.0)
242 | rack (> 1, < 3)
243 | sprockets-rails (3.4.2)
244 | actionpack (>= 5.2)
245 | activesupport (>= 5.2)
246 | sprockets (>= 3.0.0)
247 | strong_migrations (1.2.0)
248 | activerecord (>= 5.2)
249 | thor (1.2.1)
250 | tzinfo (2.0.4)
251 | concurrent-ruby (~> 1.0)
252 | unicode-display_width (2.1.0)
253 | uniform_notifier (1.16.0)
254 | webmock (3.14.0)
255 | addressable (>= 2.8.0)
256 | crack (>= 0.3.2)
257 | hashdiff (>= 0.4.0, < 2.0.0)
258 | websocket-driver (0.7.5)
259 | websocket-extensions (>= 0.1.0)
260 | websocket-extensions (0.1.5)
261 | zeitwerk (2.6.0)
262 |
263 | PLATFORMS
264 | ruby
265 |
266 | DEPENDENCIES
267 | annotate (~> 3.2.0)
268 | bcrypt (~> 3.1.18)
269 | bootsnap (>= 1.4.2)
270 | bullet (~> 7.0.2)
271 | byebug
272 | dotenv-rails (~> 2.7.6)
273 | factory_bot_rails (~> 6.2.0)
274 | faker (~> 2.21.0)
275 | graphiql-rails (~> 1.8.0)
276 | graphql (~> 2.0.11)
277 | graphql-batch (~> 0.5.1)
278 | jwt (~> 2.4.1)
279 | letter_opener (~> 1.8.1)
280 | listen (>= 3.0.5, < 3.8)
281 | pg (~> 1.4.1)
282 | puma (~> 5.6)
283 | rack-cors (~> 1.1.1)
284 | rails (~> 6.1.5)
285 | redis (~> 4.6.0)
286 | reek (~> 6.1.1)
287 | rspec-json_expectations (~> 2.2.0)
288 | rspec-rails (~> 5.1.2)
289 | rubocop (~> 1.30.1)
290 | rubocop-rails (~> 2.15.0)
291 | rubocop-rspec (~> 2.11.1)
292 | sentry-rails
293 | sentry-ruby
294 | shoulda-matchers (~> 5.1.0)
295 | simplecov (~> 0.21.2)
296 | spring (~> 2.1.1)
297 | spring-watcher-listen (~> 2.0.0)
298 | strong_migrations (~> 1.2.0)
299 | tzinfo-data
300 | webmock (~> 3.14.0)
301 |
302 | RUBY VERSION
303 | ruby 2.7.2p137
304 |
305 | BUNDLED WITH
306 | 2.1.4
307 |
--------------------------------------------------------------------------------