quail-graphql 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/AGENTS.md +19 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +39 -0
  5. data/Rakefile +12 -0
  6. data/lib/generators/quail/channel_generator.rb +17 -0
  7. data/lib/generators/quail/install_generator.rb +75 -0
  8. data/lib/generators/quail/resource_generator.rb +79 -0
  9. data/lib/generators/quail/templates/graphql_channel.rb.tt +4 -0
  10. data/lib/generators/quail/templates/graphql_channel_custom.rb.tt +23 -0
  11. data/lib/generators/quail/templates/graphql_controller.rb.tt +40 -0
  12. data/lib/generators/quail/templates/initializer.rb.tt +5 -0
  13. data/lib/generators/quail/templates/resource.rb.tt +23 -0
  14. data/lib/generators/quail/templates/schema.rb.tt +21 -0
  15. data/lib/quail/channel.rb +52 -0
  16. data/lib/quail/controller_helpers.rb +27 -0
  17. data/lib/quail/railtie.rb +41 -0
  18. data/lib/quail/resource/dsl.rb +121 -0
  19. data/lib/quail/resource/mutation_builder/context.rb +22 -0
  20. data/lib/quail/resource/mutation_builder/resolvers.rb +42 -0
  21. data/lib/quail/resource/mutation_builder.rb +139 -0
  22. data/lib/quail/resource/query_builder.rb +42 -0
  23. data/lib/quail/resource/subscription_builder.rb +53 -0
  24. data/lib/quail/resource/type_builder/association_builder.rb +92 -0
  25. data/lib/quail/resource/type_builder/field_builder.rb +57 -0
  26. data/lib/quail/resource/type_builder.rb +68 -0
  27. data/lib/quail/resource.rb +33 -0
  28. data/lib/quail/schema_builder/discovery.rb +42 -0
  29. data/lib/quail/schema_builder/type_definitions.rb +44 -0
  30. data/lib/quail/schema_builder.rb +127 -0
  31. data/lib/quail/tasks/quail.rake +25 -0
  32. data/lib/quail/type_map.rb +31 -0
  33. data/lib/quail/version.rb +5 -0
  34. data/lib/quail.rb +99 -0
  35. data/quail_logo.png +0 -0
  36. data/quail_logo_new.svg +129 -0
  37. data/sig/quail.rbs +4 -0
  38. metadata +123 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f4a7270de975b06407fbe6b81ed40135a4c94e2816cc556dbd388fd5b673db12
4
+ data.tar.gz: 5deb33482ef22b0b18dc230fca7a8f90051f09ed81c09e88438d8ff5c19b9f3d
5
+ SHA512:
6
+ metadata.gz: afa695056fd8c70acf472243252c5415c5c0a3b89e236499718ced6adfd8ba80a5ae7089fc105df615ba5208c2ef18598a548a98c8aa0d0fbcdc8d78451c5c94
7
+ data.tar.gz: 7ff9e3ed9a03729f13b64d40549fe1fcb9a4304a76a45db46426f41f42cb155598eb91a75f4778a1e5b6469c8adcb616c8adbe6abd99cb50572d3001d479a9b2
data/AGENTS.md ADDED
@@ -0,0 +1,19 @@
1
+ # AGENTS.md
2
+
3
+ This project provides machine-readable documentation for AI coding agents.
4
+
5
+ ## LLMs.txt Endpoints
6
+
7
+ The following files are dynamically generated from the Quail documentation site root:
8
+
9
+ - **llms.txt** Index of all documentation pages with links
10
+ - **llms-full.txt** Full content of all pages in one file
11
+ - **llms-small.txt** Condensed version
12
+
13
+ ## Usage
14
+
15
+ Coding agents can fetch these URLs to get context about the Quail documentation:
16
+
17
+ - `https://quail.taywils.me/llms.txt`
18
+ - `https://quail.taywils.me/llms-full.txt`
19
+ - `https://quail.taywils.me/llms-small.txt`
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Demetrious Wilson
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ <div align="center">
2
+ <img src="./quail_logo_new.svg" alt="Quail Logo" width="320">
3
+
4
+ <strong>A Rails-first GraphQL library with a declarative, <a href="https://github.com/okuramasafumi/alba">Alba</a>-inspired DSL built on top of <a href="https://github.com/rmosolgo/graphql-ruby">graphql-ruby</a>.</strong>
5
+
6
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop/rubocop)
7
+ </div>
8
+
9
+ > ⚠️ **This gem is under active development and has not been published to RubyGems. Please do not use in production.**
10
+
11
+ ## Installation
12
+
13
+ Add Quail to your Gemfile:
14
+
15
+ ```Gemfile
16
+ gem "quail", git: "https://github.com/taywils/quail.git", branch: "main"
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```bash
22
+ rails generate quail:install
23
+ ```
24
+
25
+ ## Documentation
26
+
27
+ 📖 **Full documentation:** [Quail Docs Website](https://quail.taywils.me/)
28
+
29
+ 📂 **Documentation source:** [quail-docs Github Repo](https://github.com/taywils/quail-docs)
30
+
31
+ The docs site covers the resource DSL, mutations, queries, subscriptions, custom types, authentication wiring, and more.
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/taywils/quail>.
36
+
37
+ ## License
38
+
39
+ Released under the [MIT License](./LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module Quail
6
+ module Generators
7
+ # Generates a customizable GraphQL ActionCable channel for subscriptions.
8
+ class ChannelGenerator < Rails::Generators::Base
9
+ desc "Generate a customizable GraphQL ActionCable channel"
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ def create_channel
13
+ template "graphql_channel_custom.rb.tt", "app/channels/graphql_channel.rb"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module Quail
6
+ module Generators
7
+ # Sets up Quail in a Rails app: schema, controller, channel, initializer, and directories.
8
+ class InstallGenerator < Rails::Generators::Base
9
+ desc "Set up Quail: schema, controller, route, initializer, and resource directory"
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ class_option :schema_name,
13
+ type: :string,
14
+ default: nil,
15
+ desc: "Name for the schema class (default: AppSchema)"
16
+
17
+ class_option :skip_controller,
18
+ type: :boolean,
19
+ default: nil,
20
+ desc: "Skip generating the GraphQL controller"
21
+
22
+ class_option :skip_channel,
23
+ type: :boolean,
24
+ default: false,
25
+ desc: "Skip generating the ActionCable channel"
26
+
27
+ def create_graphql_directories
28
+ %w[resources mutations queries subscriptions types].each do |dir|
29
+ empty_directory "app/graphql/#{dir}"
30
+ create_file "app/graphql/#{dir}/.keep"
31
+ end
32
+ end
33
+
34
+ def create_schema
35
+ template "schema.rb.tt", "app/graphql/#{schema_name.underscore}.rb"
36
+ end
37
+
38
+ def create_controller
39
+ return if options[:skip_controller]
40
+
41
+ template "graphql_controller.rb.tt", "app/controllers/graphql_controller.rb"
42
+ end
43
+
44
+ def create_channel
45
+ return if options[:skip_channel]
46
+
47
+ template "graphql_channel.rb.tt", "app/channels/graphql_channel.rb"
48
+ end
49
+
50
+ def create_initializer
51
+ template "initializer.rb.tt", "config/initializers/quail.rb"
52
+ end
53
+
54
+ def add_route
55
+ return if options[:skip_controller]
56
+
57
+ route 'post "/graphql", to: "graphql#execute"'
58
+ end
59
+
60
+ private
61
+
62
+ def schema_name
63
+ options[:schema_name] || "AppSchema"
64
+ end
65
+
66
+ def app_name
67
+ if Rails.application.class.respond_to?(:module_parent_name)
68
+ Rails.application.class.module_parent_name
69
+ else
70
+ Rails.application.class.parent_name
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module Quail
6
+ module Generators
7
+ # Generates a Quail resource file for a given ActiveRecord model.
8
+ class ResourceGenerator < Rails::Generators::NamedBase
9
+ desc "Generate a Quail resource for a model. Usage: rails g quail:resource Article"
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ class_option :attributes,
13
+ type: :array,
14
+ default: [],
15
+ desc: "Attributes to expose (default: all columns)"
16
+
17
+ class_option :skip_mutations,
18
+ type: :array,
19
+ default: [],
20
+ desc: "Mutations to skip: create update delete"
21
+
22
+ class_option :skip_queries,
23
+ type: :array,
24
+ default: [],
25
+ desc: "Queries to skip: find list"
26
+
27
+ class_option :subscribe_on,
28
+ type: :array,
29
+ default: [],
30
+ desc: "Events to subscribe on: create update delete"
31
+
32
+ def create_resource
33
+ template "resource.rb.tt", "app/graphql/resources/#{file_name}_resource.rb"
34
+ end
35
+
36
+ private
37
+
38
+ def model_class
39
+ class_name.constantize
40
+ rescue NameError
41
+ nil
42
+ end
43
+
44
+ def attribute_names
45
+ if options[:attributes].any?
46
+ options[:attributes].map(&:to_sym)
47
+ elsif model_class
48
+ model_class.column_names.map(&:to_sym)
49
+ else
50
+ [:id]
51
+ end
52
+ end
53
+
54
+ def association_lines
55
+ return [] unless model_class
56
+
57
+ model_class.reflect_on_all_associations.map do |association|
58
+ case association.macro
59
+ when :has_many then " has_many :#{association.name}"
60
+ when :has_one then " has_one :#{association.name}"
61
+ when :belongs_to then " belongs_to :#{association.name}"
62
+ end
63
+ end.compact
64
+ end
65
+
66
+ def writable_attribute_names
67
+ attribute_names.reject { |c| %i[id created_at updated_at].include?(c) }
68
+ end
69
+
70
+ def skip_mutations_list
71
+ options[:skip_mutations].map { |m| ":#{m}" }.join(", ")
72
+ end
73
+
74
+ def subscribe_on_list
75
+ options[:subscribe_on].map { |e| ":#{e}" }.join(", ")
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,4 @@
1
+ # Default Quail subscription channel.
2
+ # For most apps, this just works out of the box.
3
+ class GraphqlChannel < Quail::Channel
4
+ end
@@ -0,0 +1,23 @@
1
+ # Customizable GraphQL subscription channel.
2
+ # Override context_for_subscription to inject your own auth context.
3
+ class GraphqlChannel < Quail::Channel
4
+ private
5
+
6
+ # Add your auth context here. This is passed to every subscription query.
7
+ # Example:
8
+ #
9
+ # def context_for_subscription
10
+ # {
11
+ # channel: self,
12
+ # current_user: find_verified_user
13
+ # }
14
+ # end
15
+ #
16
+ # def find_verified_user
17
+ # # Your auth logic - token-based, cookie-based, JWT, etc...
18
+ # end
19
+ #
20
+ def context_for_subscription
21
+ { channel: self }
22
+ end
23
+ end
@@ -0,0 +1,40 @@
1
+ class GraphqlController < ApplicationController
2
+ # If accessing from outside this domain, nullify the session
3
+ # This allows for outside API access while preventing CSRF attacks,
4
+ # but you'll have to authenticate your user separately
5
+ # protect_from_forgery with: :null_session
6
+
7
+ def execute
8
+ result = <%= schema_name %>.execute(
9
+ params[:query],
10
+ variables: normalize_request_params(params[:variables]),
11
+ context: {
12
+ # Bring Your Own Auth - add your current_user here:
13
+ # current_user: current_user,
14
+ },
15
+ operation_name: params[:operationName]
16
+ )
17
+ render json: result
18
+ rescue StandardError => e
19
+ raise e unless Rails.env.development?
20
+ handle_error_in_development(e)
21
+ end
22
+
23
+ private
24
+
25
+ def normalize_request_params(request_params)
26
+ case request_params
27
+ when String then request_params.present? ? JSON.parse(request_params) : {}
28
+ when Hash then request_params
29
+ when ActionController::Parameters then request_params.to_unsafe_hash
30
+ when nil then {}
31
+ else raise ArgumentError, "Unexpected parameter: #{request_params}"
32
+ end
33
+ end
34
+
35
+ def handle_error_in_development(e)
36
+ logger.error e.message
37
+ logger.error e.backtrace.join("\n")
38
+ render json: { errors: [{ message: e.message, backtrace: e.backtrace }], data: {} }, status: 500
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ Rails.application.config.quail.schema_class = "<%= schema_name %>"
2
+
3
+ # Uncomment to use custom base classes from your app:
4
+ # Quail.base_object_class = Types::BaseObject
5
+ # Quail.base_mutation_class = Mutations::BaseMutation
@@ -0,0 +1,23 @@
1
+ class <%= class_name %>Resource
2
+ include Quail::Resource
3
+ <% if attribute_names.any? -%>
4
+
5
+ attributes <%= attribute_names.map { |a| ":#{a}" }.join(", ") %>
6
+ <% end -%>
7
+ <% if association_lines.any? -%>
8
+
9
+ <%= association_lines.join("\n") %>
10
+ <% end -%>
11
+ <% if writable_attribute_names.any? -%>
12
+
13
+ writable_attributes <%= writable_attribute_names.map { |a| ":#{a}" }.join(", ") %>
14
+ <% end -%>
15
+ <% if options[:skip_mutations].any? -%>
16
+
17
+ skip_mutations <%= skip_mutations_list %>
18
+ <% end -%>
19
+ <% if options[:subscribe_on].any? -%>
20
+
21
+ subscribe_on <%= subscribe_on_list %>
22
+ <% end -%>
23
+ end
@@ -0,0 +1,21 @@
1
+ class <%= schema_name %> < GraphQL::Schema
2
+ Quail::SchemaBuilder.call(self)
3
+
4
+ # Limit the size of incoming queries:
5
+ max_query_string_tokens(5_000)
6
+
7
+ # Stop validating when it encounters this many errors:
8
+ validate_max_errors(100)
9
+
10
+ # Relay-style Object Identification:
11
+
12
+ # Return a string UUID for `object`
13
+ def self.id_from_object(object, type_definition, query_ctx)
14
+ object.to_gid_param
15
+ end
16
+
17
+ # Given a string UUID, find the object
18
+ def self.object_from_id(global_id, query_ctx)
19
+ GlobalID.find(global_id)
20
+ end
21
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quail
4
+ # ActionCable channel for handling GraphQL subscriptions over WebSocket.
5
+ class Channel < ActionCable::Channel::Base
6
+ def subscribed
7
+ @subscription_ids = []
8
+ result = execute_query
9
+ track_subscription(result)
10
+ transmit(result: result.to_h, more: result.subscription?)
11
+ end
12
+
13
+ def unsubscribed
14
+ @subscription_ids&.each do |subscription_id|
15
+ schema_class.subscriptions.delete_subscription(subscription_id)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def execute_query
22
+ schema_class.execute(
23
+ params[:query],
24
+ context: context_for_subscription,
25
+ variables: ensure_hash(params[:variables]),
26
+ operation_name: params[:operation_name]
27
+ )
28
+ end
29
+
30
+ def track_subscription(result)
31
+ @subscription_ids << result.context[:subscription_id] if result.context[:subscription_id]
32
+ end
33
+
34
+ def context_for_subscription
35
+ { channel: self }
36
+ end
37
+
38
+ def schema_class
39
+ Rails.application.config.quail.schema_class
40
+ end
41
+
42
+ def ensure_hash(some_param)
43
+ case some_param
44
+ when String then some_param.present? ? JSON.parse(some_param) : {}
45
+ when Hash then some_param
46
+ when ActionController::Parameters then some_param.to_unsafe_hash
47
+ when nil then {}
48
+ else raise ArgumentError, "Unexpected parameter: #{some_param.class}"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quail
4
+ # Provides shared helper methods for GraphQL controllers,
5
+ # including request parameter normalization and development error handling.
6
+ module ControllerHelpers
7
+ extend ActiveSupport::Concern
8
+
9
+ private
10
+
11
+ def normalize_request_params(request_params)
12
+ case request_params
13
+ when String then request_params.present? ? JSON.parse(request_params) : {}
14
+ when Hash then request_params
15
+ when ActionController::Parameters then request_params.to_unsafe_hash
16
+ when nil then {}
17
+ else raise ArgumentError, "Unexpected parameter: #{request_params}"
18
+ end
19
+ end
20
+
21
+ def handle_error_in_development(error)
22
+ logger.error error.message
23
+ logger.error error.backtrace.join("\n")
24
+ render json: { errors: [{ message: error.message, backtrace: error.backtrace }], data: {} }, status: 500
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quail
4
+ # Rails integration for Quail: configures eager loading and schema resolution.
5
+ class Railtie < Rails::Railtie
6
+ config.quail = ActiveSupport::OrderedOptions.new
7
+ config.quail.schema_class = nil
8
+
9
+ initializer "quail.autoload_paths", before: :set_autoload_paths do |app|
10
+ # These subdirectories define top-level constants (e.g. UserResource, not Resources::UserResource)
11
+ # Note: types/ is excluded because custom types use the Types:: namespace by convention
12
+ %w[resources queries mutations subscriptions].each do |subdir|
13
+ path = Rails.root.join("app/graphql/#{subdir}").to_s
14
+ next unless Dir.exist?(path)
15
+
16
+ app.config.autoload_paths << path
17
+ app.config.eager_load_paths << path
18
+ end
19
+ end
20
+
21
+ # Tell Zeitwerk to ignore these subdirectories from the parent app/graphql/ root
22
+ # so they are only loaded as top-level autoload roots (no module namespace).
23
+ initializer "quail.zeitwerk_ignore", before: :setup_main_autoloader do
24
+ Rails.autoloaders.main.ignore(
25
+ Rails.root.join("app/graphql/resources"),
26
+ Rails.root.join("app/graphql/queries"),
27
+ Rails.root.join("app/graphql/mutations"),
28
+ Rails.root.join("app/graphql/subscriptions")
29
+ )
30
+ end
31
+
32
+ config.after_initialize do |app|
33
+ schema = app.config.quail.schema_class
34
+ app.config.quail.schema_class = schema.constantize if schema.is_a?(String)
35
+ end
36
+
37
+ rake_tasks do
38
+ load File.expand_path("tasks/quail.rake", __dir__)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quail
4
+ module Resource
5
+ # DSL for declaring attributes, associations, mutations, and subscriptions on a resource.
6
+ module DSL
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ # Class-level DSL methods mixed into resource classes.
12
+ module ClassMethods
13
+ def model(klass = nil)
14
+ if klass
15
+ @model_class = klass
16
+ else
17
+ @model ||= name.delete_suffix("Resource").constantize
18
+ end
19
+ end
20
+ alias model_class model
21
+
22
+ def attribute_definitions
23
+ @attribute_definitions ||= {}
24
+ end
25
+
26
+ def attributes(*names)
27
+ names.each { |name| attribute_definitions[name] = { type: :column } }
28
+ end
29
+
30
+ def attribute(name, type: nil, null: nil, &block)
31
+ attribute_definitions[name] = { type: :computed, graphql_type: type, null: null, block: block }
32
+ end
33
+
34
+ def association_definitions
35
+ @association_definitions ||= {}
36
+ end
37
+
38
+ def has_many(name, resource: nil, **options)
39
+ association_definitions[name] = { kind: :has_many, resource: resource, **options }
40
+ end
41
+
42
+ def has_one(name, resource: nil, **options)
43
+ association_definitions[name] = { kind: :has_one, resource: resource, **options }
44
+ end
45
+
46
+ def belongs_to(name, resource: nil, **options)
47
+ if options[:polymorphic]
48
+ validate_polymorphic_options!(options[:polymorphic])
49
+ options[:polymorphic_types] = options[:polymorphic][:types]
50
+ options[:union_name] = options[:polymorphic][:union_name] if options[:polymorphic][:union_name]
51
+ options[:polymorphic] = true
52
+ end
53
+ association_definitions[name] = { kind: :belongs_to, resource: resource, **options }
54
+ end
55
+
56
+ def skip_mutations(*actions)
57
+ @skipped_mutations = actions.map(&:to_sym)
58
+ end
59
+
60
+ def skipped_mutations
61
+ @skipped_mutations || []
62
+ end
63
+
64
+ def override_mutation(action, klass)
65
+ mutation_overrides[action.to_sym] = klass
66
+ end
67
+
68
+ def mutation_overrides
69
+ @mutation_overrides ||= {}
70
+ end
71
+
72
+ # Resolves mutation override values, constantizing strings lazily.
73
+ def resolved_mutation_overrides
74
+ mutation_overrides.transform_values do |klass|
75
+ klass.is_a?(String) ? klass.constantize : klass
76
+ end
77
+ end
78
+
79
+ def writable_attributes(*names)
80
+ if names.any?
81
+ @writable_attributes = names.map(&:to_sym)
82
+ else
83
+ @writable_attributes
84
+ end
85
+ end
86
+
87
+ def subscription_definitions
88
+ @subscription_definitions ||= {}
89
+ end
90
+
91
+ def subscribe_on(*events, scope: nil)
92
+ if scope && !scope.is_a?(Symbol) && !scope.is_a?(Hash)
93
+ raise ArgumentError, "subscribe_on scope: must be a Symbol or Hash { key: proc }, got #{scope.class}"
94
+ end
95
+
96
+ events.each { |event| subscription_definitions[event.to_sym] = { scope: scope } }
97
+ end
98
+
99
+ def skip_queries(*actions)
100
+ @skipped_queries = actions.map(&:to_sym)
101
+ end
102
+
103
+ def skipped_queries
104
+ @skipped_queries || []
105
+ end
106
+
107
+ private
108
+
109
+ def validate_polymorphic_options!(poly_opt)
110
+ unless poly_opt.is_a?(Hash) && poly_opt.key?(:types)
111
+ raise ArgumentError,
112
+ "polymorphic requires a Hash with a :types key, e.g. polymorphic: { types: [FooResource] }"
113
+ end
114
+ return unless poly_opt[:types].empty?
115
+
116
+ raise ArgumentError, "polymorphic :types must contain at least one resource class or class name string"
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quail
4
+ module Resource
5
+ module MutationBuilder
6
+ # Holds the shared context needed by all mutation builders.
7
+ MutationContext = Struct.new(:resource_class) do
8
+ def model = resource_class.model_class
9
+ def type_class = resource_class.graphql_type
10
+ def underscore_name = model.name.underscore
11
+
12
+ def writable
13
+ resource_class.writable_attributes || MutationBuilder.default_writable(model,
14
+ resource_class)
15
+ end
16
+
17
+ def subscriptions = resource_class.subscription_definitions
18
+ def base = Quail.base_mutation_class || GraphQL::Schema::RelayClassicMutation
19
+ end
20
+ end
21
+ end
22
+ end