class GraphqlRailsApi::InstallGenerator

Public Instance Methods

generate_files() click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 9
def generate_files
  @app_name = File.basename(Rails.root.to_s).underscore
  system('mkdir -p app/graphql/')

  write_uuid_extensions_migration

  write_service
  write_schema
  write_query_type
  write_mutation_type
  write_collection_ids_resolver

  write_controller

  write_websocket_models
  write_websocket_connection
  write_subscriptions_channel

  write_application_record_methods
  write_initializer
  write_require_application_rb
  write_route if options.generate_graphql_route?
end

Private Instance Methods

apollo_compat() click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 307
    def apollo_compat
      <<~'STRING'
        # /!\ do not remove /!\
        # Apollo Data compat.
        ClientDirective = GraphQL::Directive.define do
          name 'client'
          locations([GraphQL::Directive::FIELD])
          default_directive true
        end
        ConnectionDirective = GraphQL::Directive.define do
          name 'connection'
          locations([GraphQL::Directive::FIELD])
          argument :key, GraphQL::STRING_TYPE
          argument :filter, GraphQL::STRING_TYPE.to_list_type
          default_directive true
        end
        # end of Apollo Data compat.
      STRING
    end
write_application_record_methods() click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 75
    def write_application_record_methods
      lines_count = File.read('app/models/application_record.rb').lines.count

      return if File.read('app/models/application_record.rb').include?('def self.visible_for')
      write_at(
        'app/models/application_record.rb',
        lines_count,
        <<-STRING

  def self.visible_for(*)
    all
  end

  def self.writable_by(*)
    all
  end

  def self.broadcast_queries
    WebsocketConnection.all.each do |wsc|
      wsc.subscribed_queries.each do |sq|
        result = #{@app_name.camelize}Schema.execute(sq.query, context: { current_user: wsc.user })
        hex = Digest::SHA1.hexdigest(result.to_s)
        next if sq.result_hash == hex

        sq.update_attributes(result_hash: hex)
        SubscriptionsChannel.broadcast_to(wsc, query: sq.query, result: result.to_s)
      end
    end
  end

        STRING
      )
    end
write_at(file_name, line, data) click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 506
def write_at(file_name, line, data)
  open(file_name, 'r+') do |f|
    while (line -= 1).positive?
      f.readline
    end
    pos = f.pos
    rest = f.read
    f.seek pos
    f.write data
    f.write rest
  end
end
write_collection_ids_resolver() click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 56
    def write_collection_ids_resolver
      File.write(
        'app/graphql/collection_ids_resolver.rb',
        <<~STRING
          class CollectionIdsResolver

            def self.call(obj, _args, ctx)
              if obj.is_a?(OpenStruct)
                obj[ctx.field.name.gsub('_ids', '').pluralize]&.map(&:id)
              else
                obj.send(ctx.field.name)
              end
            end

          end
        STRING
      )
    end
write_controller() click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 206
    def write_controller
      File.write(
        'app/controllers/graphql_controller.rb',
        <<~STRING
          class GraphqlController < ApplicationController

            # GraphQL endpoint
            def execute
              result = #{@app_name.camelize}Schema.execute(
                params[:query],
                variables: ensure_hash(params[:variables]),
                context: { current_user: authenticated_user },
                operation_name: params[:operationName]
              )
              ApplicationRecord.broadcast_queries
              render json: result
            end

            private

            def authenticated_user
              # Here you need to authenticate the user.
            end

            # Handle form data, JSON body, or a blank value
            def ensure_hash(ambiguous_param)
              case ambiguous_param
              when String
                ambiguous_param.present? ? ensure_hash(JSON.parse(ambiguous_param)) : {}
              when Hash, ActionController::Parameters
                ambiguous_param
              when nil
                {}
              else
                raise ArgumentError, 'Unexpected parameter: ' + ambiguous_param
              end
            end

          end
        STRING
      )
    end
write_initializer() click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 131
    def write_initializer
      File.write(
        'config/initializers/graphql_rails_api_config.rb',
        <<~STRING
          require 'graphql/rails/api/config'

          config = Graphql::Rails::Api::Config.instance
        STRING
      )
    end
write_mutation_type() click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 249
    def write_mutation_type
      File.write(
        'app/graphql/mutation_type.rb',
        <<~'STRING'
          MutationType = GraphQL::ObjectType.define do
            name 'Mutation'

            Graphql::Rails::Api::Config.mutation_resources.each do |methd, resources|
              resources.each do |resource|
                field(
                  "#{methd}_#{resource.singularize}".to_sym,
                  "#{resource.camelize}::Mutations::#{methd.camelize}".constantize
                )
              end
            end

          end
        STRING
      )
    end
write_query_type() click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 270
    def write_query_type
      File.write(
        'app/graphql/query_type.rb',
        <<~'STRING'
          QueryType = GraphQL::ObjectType.define do
            name 'Query'

            Graphql::Rails::Api::Config.query_resources.each do |resource|
              field resource.singularize do
                description "Returns a #{resource.classify}"
                type !"#{resource.camelize}::Type".constantize
                argument :id, !types.String
                resolve ApplicationService.call(resource, :show)
              end

              field resource.pluralize do
                description "Returns a #{resource.classify}"
                type !types[!"#{resource.camelize}::Type".constantize]
                argument :page, types.Int
                argument :per_page, types.Int
                argument :filter, types.String
                argument :order_by, types.String
                resolve ApplicationService.call(resource, :index)
              end

            end

            field :me, Users::Type do
              description 'Returns the current user'
              resolve ->(_, _, ctx) { ctx[:current_user] }
            end

          end
        STRING
      )
    end
write_require_application_rb() click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 109
def write_require_application_rb
  write_at('config/application.rb', 5, "require 'graphql/hydrate_query'\nrequire 'rkelly'\n")
end
write_route() click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 41
def write_route
  route_file = File.read('config/routes.rb')
  return if route_file.include?('graphql')

  File.write(
    'config/routes.rb',
    route_file.gsub(
      "Rails.application.routes.draw do\n",
      "Rails.application.routes.draw do\n" \
      "  post '/graphql', to: 'graphql#execute'\n" \
      "  mount ActionCable.server => '/cable'\n"
    )
  )
end
write_schema() click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 327
    def write_schema
      logger = <<~'STRING'
        type_error_logger = Logger.new("#{Rails.root}/log/graphql_type_errors.log")
      STRING

      error_handler = <<~'STRING'
        type_error_logger.error "#{err} for #{query_ctx.query.query_string} \
            with #{query_ctx.query.provided_variables}"
      STRING

      File.write(
        "app/graphql/#{@app_name}_schema.rb",
        <<~STRING
          #{logger}
          #{apollo_compat if options.apollo_compatibility?}
          # Schema definition
          #{@app_name.camelize}Schema = GraphQL::Schema.define do
            mutation(MutationType)
            query(QueryType)
            #{'directives [ConnectionDirective, ClientDirective]' if options.apollo_compatibility?}
            type_error lambda { |err, query_ctx|
              #{error_handler}
            }
          end
        STRING
      )
    end
write_service() click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 355
    def write_service
      File.write(
        'app/graphql/application_service.rb',
        <<~'STRING'
          class ApplicationService

            attr_accessor :params, :object, :fields, :user

            def initialize(params: {}, object: nil, object_id: nil, user: nil, context: nil)
              @params = params.is_a?(Array) ? params.map { |p| p.to_h.symbolize_keys } : params.to_h.symbolize_keys
              @context = context
              @object = object || (object_id && model.visible_for(user: user).find_by(id: object_id))
              @object_id = object_id
              @user = user
            end

            def self.call(resource, meth)
              lambda { |_obj, args, context|
                params = args && args[resource] ? args[resource] : args
                "#{resource.to_s.pluralize.camelize.constantize}::Service".constantize.new(
                  params: params, user: context[:current_user],
                  object_id: args[:id], context: context
                ).send(meth)
              }
            end

            def index
              Graphql::HydrateQuery.new(
                model.all,
                @context,
                order_by: params[:order_by],
                filter: params[:filter],
                per_page: params[:per_page],
                page: params[:page],
                user: user
              ).run.compact
            end

            def show
              object = Graphql::HydrateQuery.new(model.all, @context, user: user, id: params[:id]).run
              return not_allowed if object.blank?

              object
            end

            def create
              object = model.new(params.select { |p| model.new.respond_to?(p) })
              return not_allowed if not_allowed_to_create_resource(object)

              if object.save
                object
              else
                graphql_error(object.errors.full_messages.join(', '))
              end
            end

            def bulk_create
              result = model.import(params.map { |p| p.select { |param| model.new.respond_to?(param) } })
              result.each { |e| e.run_callbacks(:save) }
              hyd = Graphql::HydrateQuery.new(model.where(id: result.ids), @context).run.compact + result.failed_instances.map do |i|
                graphql_error(i.errors.full_messages)
              end
              return hyd.first if hyd.all? { |e| e.is_a?(GraphQL::ExecutionError) }

              hyd
            end

            def bulk_update
              visible_ids = model.where(id: params.map { |p| p[:id] }).pluck(:id)
              return not_allowed if (model.visible_for(user: user).pluck(:id) & visible_ids).size < visible_ids.size

              hash = params.each_with_object({}) { |p, h| h[p.delete(:id)] = p }
              failed_instances = []
              result = model.update(hash.keys, hash.values).map { |e| e.errors.blank? ? e : (failed_instances << e && nil) }
              hyd = Graphql::HydrateQuery.new(model.where(id: result.compact.map(&:id)), @context).run.compact + failed_instances.map do |i|
                graphql_error(i.errors.full_messages)
              end
              hyd.all? { |e| e.is_a?(GraphQL::ExecutionError) } ? hyd.first : hyd
            end

            def update
              return not_allowed if write_not_allowed

              if object.update_attributes(params)
                object
              else
                graphql_error(object.errors.full_messages.join(', '))
              end
            end

            def destroy
              object = model.find_by(id: params[:id])
              return not_allowed if write_not_allowed

              if object.destroy
                object
              else
                graphql_error(object.errors.full_messages.join(', '))
              end
            end

            private

            def write_not_allowed
              !model.visible_for(user: user).include?(object) if object
            end

            def access_not_allowed
              !model.visible_for(user: user).include?(object) if object
            end

            def not_allowed
              graphql_error('403 - Not allowed')
            end

            def graphql_error(message)
              GraphQL::ExecutionError.new(message)
            end

            def singular_resource
              resource_name.singularize
            end

            def model
              singular_resource.camelize.constantize
            end

            def resource_name
              self.class.to_s.split(':').first.underscore
            end

            def not_allowed_to_create_resource(created_resource)
              params.select { |k, _| k.to_s.end_with?('_id') }.each do |belongs_relation, rel_id|
                klass = created_resource.class.reflect_on_association(belongs_relation.to_s.gsub('_id', '')).klass
                return true if rel_id.present? && !klass.visible_for(user: user).pluck(:id).include?(rel_id)
              end

              params.select { |k, _| k.to_s.end_with?('_ids') }.each do |many_relation, rel_ids|
                klass = created_resource.class.reflect_on_association(many_relation.to_s.gsub('_ids', '').pluralize).klass
                ids = klass.visible_for(user: user).pluck(:id)
                rel_ids.each { |id| return true if id.present? && !ids.include?(id) }
              end
              false
            end

          end

        STRING
      )
    end
write_subscriptions_channel() click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 164
    def write_subscriptions_channel
      File.write(
        'app/channels/subscriptions_channel.rb',
        <<~STRING
          class SubscriptionsChannel < ApplicationCable::Channel

            def subscribed
              stream_for(websocket_connection)
              websocket_connection.update_attributes(connection_identifier: connection.connection_identifier)
              ci = ActionCable.server.connections.map(&:connection_identifier)
              WebsocketConnection.all.each do |wsc|
                wsc.destroy unless ci.include?(wsc.connection_identifier)
              end
            end

            def subscribe_to_query(data)
              websocket_connection.subscribed_queries.find_or_create_by(query: data['query'])
              SubscriptionsChannel.broadcast_to(
                websocket_connection,
                query: data['query'],
                result: #{@app_name.camelize}Schema.execute(data['query'], context: { current_user: websocket_connection.user })
              )
            end

            def unsubscribe_to_query(data)
              websocket_connection.subscribed_queries.find_by(query: data['query'])&.destroy
            end

            def unsubscribed
              websocket_connection.destroy
              ci = ActionCable.server.connections.map(&:connection_identifier)
              WebsocketConnection.all.each do |wsc|
                wsc.destroy unless ci.include?(wsc.connection_identifier)
              end
            end

          end

        STRING
      )
    end
write_uuid_extensions_migration() click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 113
    def write_uuid_extensions_migration
      system('bundle exec rails generate migration uuid_pg_extensions --skip')
      migration_file = Dir.glob('db/migrate/*uuid_pg_extensions*').last
      File.write(
        migration_file,
        <<~STRING
          class UuidPgExtensions < ActiveRecord::Migration[5.2]

            def change
              execute 'CREATE EXTENSION "pgcrypto" SCHEMA pg_catalog;'
              execute 'CREATE EXTENSION "uuid-ossp" SCHEMA pg_catalog;'
            end

          end
        STRING
      )
    end
write_websocket_connection() click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 142
    def write_websocket_connection
      File.write(
        'app/channels/application_cable/connection.rb',
        <<~'STRING'
          module ApplicationCable
            class Connection < ActionCable::Connection::Base

              identified_by :websocket_connection

              def connect
                # Check authentication, and define current user
                self.websocket_connection = WebsocketConnection.create(
                  # user_id: current_user.id
                )
              end

            end
          end
        STRING
      )
    end
write_websocket_models() click to toggle source
# File lib/generators/graphql_rails_api/install_generator.rb, line 35
def write_websocket_models
  system 'rails g graphql_resource user first_name:string last_name:string email:string'
  system 'rails g graphql_resource websocket_connection belongs_to:user connection_identifier:string'
  system 'rails g graphql_resource subscribed_query belongs_to:websocket_connection result_hash:string query:string'
end