activeadmin-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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/CODE_OF_CONDUCT.md +31 -0
  4. data/CONTRIBUTING.md +27 -0
  5. data/LICENSE.md +21 -0
  6. data/README.md +49 -0
  7. data/activeadmin-graphql.gemspec +66 -0
  8. data/app/controllers/active_admin/graphql_controller.rb +168 -0
  9. data/docs/graphql-api.md +486 -0
  10. data/lib/active_admin/graphql/auth_context.rb +35 -0
  11. data/lib/active_admin/graphql/engine.rb +9 -0
  12. data/lib/active_admin/graphql/integration.rb +135 -0
  13. data/lib/active_admin/graphql/key_value_pair_input.rb +48 -0
  14. data/lib/active_admin/graphql/railtie.rb +10 -0
  15. data/lib/active_admin/graphql/record_source.rb +30 -0
  16. data/lib/active_admin/graphql/resource_config.rb +68 -0
  17. data/lib/active_admin/graphql/resource_definition_dsl.rb +117 -0
  18. data/lib/active_admin/graphql/resource_interface.rb +25 -0
  19. data/lib/active_admin/graphql/resource_query_proxy/controller.rb +149 -0
  20. data/lib/active_admin/graphql/resource_query_proxy.rb +112 -0
  21. data/lib/active_admin/graphql/run_action_mutation_config.rb +23 -0
  22. data/lib/active_admin/graphql/run_action_mutation_dsl.rb +32 -0
  23. data/lib/active_admin/graphql/run_action_payload.rb +27 -0
  24. data/lib/active_admin/graphql/schema_builder/build.rb +84 -0
  25. data/lib/active_admin/graphql/schema_builder/graph_params.rb +75 -0
  26. data/lib/active_admin/graphql/schema_builder/mutation_action_types.rb +52 -0
  27. data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +61 -0
  28. data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +118 -0
  29. data/lib/active_admin/graphql/schema_builder/mutation_create.rb +65 -0
  30. data/lib/active_admin/graphql/schema_builder/mutation_member.rb +122 -0
  31. data/lib/active_admin/graphql/schema_builder/mutation_type_builder.rb +52 -0
  32. data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +120 -0
  33. data/lib/active_admin/graphql/schema_builder/query_type.rb +53 -0
  34. data/lib/active_admin/graphql/schema_builder/query_type_collection.rb +84 -0
  35. data/lib/active_admin/graphql/schema_builder/query_type_member.rb +91 -0
  36. data/lib/active_admin/graphql/schema_builder/query_type_pages.rb +44 -0
  37. data/lib/active_admin/graphql/schema_builder/query_type_registered.rb +57 -0
  38. data/lib/active_admin/graphql/schema_builder/resolvers.rb +116 -0
  39. data/lib/active_admin/graphql/schema_builder/resources.rb +48 -0
  40. data/lib/active_admin/graphql/schema_builder/types_inputs.rb +119 -0
  41. data/lib/active_admin/graphql/schema_builder/types_object.rb +96 -0
  42. data/lib/active_admin/graphql/schema_builder/visibility.rb +58 -0
  43. data/lib/active_admin/graphql/schema_builder/wire.rb +36 -0
  44. data/lib/active_admin/graphql/schema_builder.rb +62 -0
  45. data/lib/active_admin/graphql/schema_field.rb +29 -0
  46. data/lib/active_admin/graphql/version.rb +7 -0
  47. data/lib/active_admin/graphql.rb +68 -0
  48. data/lib/active_admin/primary_key.rb +117 -0
  49. data/lib/activeadmin/graphql.rb +5 -0
  50. metadata +389 -0
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Shared input for flat string key/value pairs (Rails param-style). Used instead of a JSON map
6
+ # for nested route segments and custom action arguments so the schema stays explicit.
7
+ class KeyValuePairInput < ::GraphQL::Schema::InputObject
8
+ graphql_name "ActiveAdminKeyValuePair"
9
+ description "Param entry: string key and value (same shape as flat Rails query/form fields)."
10
+
11
+ argument :key, ::GraphQL::Types::String, required: true, camelize: false
12
+ argument :value, ::GraphQL::Types::String, required: true, camelize: false
13
+ end
14
+
15
+ module KeyValuePairs
16
+ module_function
17
+
18
+ # @param entries [nil, Array<#key, #value>, Array<Hash>]
19
+ # @return [Hash<String, String>]
20
+ def to_hash(entries)
21
+ return {} if entries.nil?
22
+
23
+ unless entries.is_a?(Array)
24
+ raise ArgumentError, "expected an array of key/value pairs, got #{entries.class}"
25
+ end
26
+
27
+ entries.each_with_object({}) do |e, h|
28
+ key, val = extract_pair(e)
29
+ h[key.to_s] = val.to_s
30
+ end
31
+ end
32
+
33
+ def extract_pair(e)
34
+ if e.respond_to?(:key) && e.respond_to?(:value) && !e.is_a?(Hash)
35
+ [e.key, e.value]
36
+ elsif e.is_a?(Hash)
37
+ k = e["key"] || e[:key]
38
+ v = e["value"] || e[:value]
39
+ raise ::GraphQL::ExecutionError, "key/value pair missing key" if k.nil?
40
+
41
+ [k, v]
42
+ else
43
+ raise ::GraphQL::ExecutionError, "invalid key/value pair entry: #{e.class}"
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "engine"
4
+
5
+ module ActiveAdmin
6
+ module GraphQL
7
+ class Railtie < ::Rails::Railtie
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Batches +find+ by primary key for belongs_to-style fields (graphql-ruby dataloader).
6
+ class RecordSource < ::GraphQL::Dataloader::Source
7
+ def initialize(model_class)
8
+ @model_class = model_class
9
+ @pk = model_class.primary_key
10
+ end
11
+
12
+ def fetch(ids)
13
+ keys = ids.map { |raw| raw.nil? ? nil : ActiveAdmin::PrimaryKey.dataloader_tuple(@model_class, raw) }
14
+ uniq = keys.compact.uniq
15
+ return ids.map { nil } if uniq.empty?
16
+
17
+ if ActiveAdmin::PrimaryKey.composite?(@model_class)
18
+ records = @model_class.where(@pk => uniq).to_a
19
+ by_tuple = records.index_by { |r| ActiveAdmin::PrimaryKey.dataloader_tuple_from_record(r) }
20
+ keys.map { |t| t.nil? ? nil : by_tuple[t] }
21
+ else
22
+ pk_sym = @pk.to_sym
23
+ records = @model_class.where(pk_sym => uniq).to_a
24
+ by_id = records.index_by { |r| r.public_send(pk_sym) }
25
+ keys.map { |k| k.nil? ? nil : by_id[k] }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "run_action_mutation_config"
4
+
5
+ module ActiveAdmin
6
+ module GraphQL
7
+ # Per-resource GraphQL options set via +graphql+ in +ActiveAdmin.register+.
8
+ class ResourceConfig
9
+ attr_accessor :enabled
10
+ attr_accessor :graphql_type_name
11
+ attr_accessor :only_attributes
12
+ attr_accessor :exclude_attributes
13
+ attr_accessor :extension_block
14
+
15
+ # Optional resolver overrides (set from +graphql do+). SchemaBuilder still owns field names,
16
+ # arguments, and types; procs replace only the Ruby resolution body.
17
+ attr_accessor :resolve_index_proc
18
+ attr_accessor :resolve_show_proc
19
+ attr_accessor :resolve_create_proc
20
+ attr_accessor :resolve_update_proc
21
+ attr_accessor :resolve_destroy_proc
22
+
23
+ # Default return type for run-action mutations (+batch+, +member+, +collection+) when a kind-specific
24
+ # {RunActionMutationConfig#payload_type} is not set. Falls back to {RunActionPayload}.
25
+ attr_accessor :run_action_payload_type
26
+
27
+ def initialize
28
+ @enabled = true
29
+ @exclude_attributes = []
30
+ end
31
+
32
+ def disabled?
33
+ !enabled
34
+ end
35
+
36
+ def batch_run_action
37
+ @batch_run_action ||= RunActionMutationConfig.new
38
+ end
39
+
40
+ def member_run_action
41
+ @member_run_action ||= RunActionMutationConfig.new
42
+ end
43
+
44
+ def collection_run_action
45
+ @collection_run_action ||= RunActionMutationConfig.new
46
+ end
47
+
48
+ # Per +member_action+ name (string) -> {RunActionMutationConfig} for typed fields like +posts_member_publish+.
49
+ def member_action_mutations
50
+ @member_action_mutations ||= {}
51
+ end
52
+
53
+ def member_action_mutation_for(name)
54
+ key = name.to_s
55
+ member_action_mutations[key] ||= RunActionMutationConfig.new
56
+ end
57
+
58
+ def collection_action_mutations
59
+ @collection_action_mutations ||= {}
60
+ end
61
+
62
+ def collection_action_mutation_for(name)
63
+ key = name.to_s
64
+ collection_action_mutations[key] ||= RunActionMutationConfig.new
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "run_action_mutation_config"
4
+ require_relative "run_action_mutation_dsl"
5
+
6
+ module ActiveAdmin
7
+ module GraphQL
8
+ # DSL for +graphql do ... end+ inside +ActiveAdmin.register+.
9
+ class ResourceDefinitionDSL
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ def disable!
15
+ @config.enabled = false
16
+ end
17
+
18
+ def type_name(name)
19
+ @config.graphql_type_name = name.to_s
20
+ end
21
+
22
+ def only(*attrs)
23
+ @config.only_attributes = attrs.flatten.map(&:to_sym)
24
+ end
25
+
26
+ def except(*attrs)
27
+ @config.exclude_attributes.concat(attrs.flatten.map(&:to_sym))
28
+ end
29
+ alias_method :exclude, :except
30
+
31
+ def configure(&block)
32
+ @config.extension_block = block
33
+ end
34
+
35
+ # Override the list field resolver (+posts+, etc.). Must return an +ActiveRecord::Relation+
36
+ # (or compatible with the connection type). Same auth and +ResourceQueryProxy+ as the default.
37
+ def resolve_index(&block)
38
+ @config.resolve_index_proc = block
39
+ end
40
+ alias_method :resolve_collection, :resolve_index
41
+
42
+ # Override the singular field resolver (+post+, +registered_resource+ for this type).
43
+ # Must return a record instance or +nil+.
44
+ def resolve_show(&block)
45
+ @config.resolve_show_proc = block
46
+ end
47
+ alias_method :resolve_member, :resolve_show
48
+
49
+ def resolve_create(&block)
50
+ @config.resolve_create_proc = block
51
+ end
52
+
53
+ def resolve_update(&block)
54
+ @config.resolve_update_proc = block
55
+ end
56
+
57
+ def resolve_destroy(&block)
58
+ @config.resolve_destroy_proc = block
59
+ end
60
+
61
+ # @see #batch_action_mutation
62
+ def resolve_batch_action(&block)
63
+ @config.batch_run_action.resolve_proc = block
64
+ end
65
+
66
+ # @see #member_action_mutation
67
+ def resolve_member_action(&block)
68
+ @config.member_run_action.resolve_proc = block
69
+ end
70
+
71
+ # @see #collection_action_mutation
72
+ def resolve_collection_action(&block)
73
+ @config.collection_run_action.resolve_proc = block
74
+ end
75
+
76
+ # Pair +type+ and +resolve+ for +posts_batch_action+ (graphql-ruby-style block).
77
+ def batch_action_mutation(&block)
78
+ RunActionMutationDSL.new(@config.batch_run_action).instance_exec(&block)
79
+ end
80
+
81
+ # With no name: configures the aggregate +posts_member_action(action: …)+ field.
82
+ # With a symbol/string: configures +posts_member_<action>+ (one field per +member_action+), so each
83
+ # action can use its own +type+, +resolve+, +arguments+, and GraphQL inputs.
84
+ def member_action_mutation(name = nil, &block)
85
+ cfg = name.nil? ? @config.member_run_action : @config.member_action_mutation_for(name)
86
+ RunActionMutationDSL.new(cfg).instance_exec(&block)
87
+ end
88
+
89
+ def collection_action_mutation(name = nil, &block)
90
+ cfg = name.nil? ? @config.collection_run_action : @config.collection_action_mutation_for(name)
91
+ RunActionMutationDSL.new(cfg).instance_exec(&block)
92
+ end
93
+
94
+ # Default return type for all run-action fields unless a kind-specific +type+ is set inside
95
+ # +batch_action_mutation+ / +member_action_mutation+ / +collection_action_mutation+.
96
+ def run_action_payload_type(type)
97
+ RunActionMutationConfig.ensure_graphql_object_subclass!(type)
98
+ @config.run_action_payload_type = type
99
+ end
100
+
101
+ def batch_action_run_action_payload_type(type)
102
+ RunActionMutationConfig.ensure_graphql_object_subclass!(type)
103
+ @config.batch_run_action.payload_type = type
104
+ end
105
+
106
+ def member_action_run_action_payload_type(type)
107
+ RunActionMutationConfig.ensure_graphql_object_subclass!(type)
108
+ @config.member_run_action.payload_type = type
109
+ end
110
+
111
+ def collection_action_run_action_payload_type(type)
112
+ RunActionMutationConfig.ensure_graphql_object_subclass!(type)
113
+ @config.collection_run_action.payload_type = type
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Implemented by every namespace resource +GraphQL::Schema::Object+ type so clients can
6
+ # fragment on a shared +id+ field or use abstract selections consistently.
7
+ module ResourceInterface
8
+ include ::GraphQL::Schema::Interface
9
+
10
+ graphql_name "ActiveAdminResource"
11
+ description "Shared shape for Active Admin resource records exposed in this namespace."
12
+
13
+ field :id, ::GraphQL::Types::ID, null: false
14
+
15
+ definition_methods do
16
+ def visible?(context)
17
+ hook = context[:namespace]&.graphql_schema_visible
18
+ return super if hook.nil?
19
+
20
+ super && !!hook.call(context, {kind: :resource_interface})
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ class ResourceQueryProxy
6
+ module Controller
7
+ private
8
+
9
+ def normalize_graph_params(graph_params)
10
+ h = graph_params.respond_to?(:to_unsafe_h) ? graph_params.to_unsafe_h : graph_params.to_h
11
+ h.stringify_keys
12
+ end
13
+
14
+ def param_hash_for(action, extra)
15
+ {
16
+ "action" => action,
17
+ "controller" => "active_admin/graphql"
18
+ }.merge(extra.stringify_keys).tap do |h|
19
+ merge_ransack!(h)
20
+ h["scope"] = @graph_params["scope"].to_s if @graph_params["scope"].present?
21
+ h["order"] = @graph_params["order"].to_s if @graph_params["order"].present?
22
+ merge_belongs_to!(h)
23
+ end
24
+ end
25
+
26
+ def merge_ransack!(h)
27
+ q = @graph_params["q"]
28
+ return if q.blank? && !@graph_params.key?("q")
29
+
30
+ h["q"] = normalize_q(q)
31
+ end
32
+
33
+ def normalize_q(q)
34
+ case q
35
+ when Hash
36
+ q.deep_stringify_keys
37
+ when ActionController::Parameters
38
+ q.to_unsafe_h.deep_stringify_keys
39
+ else
40
+ {}
41
+ end
42
+ end
43
+
44
+ def merge_belongs_to!(h)
45
+ btc = @aa_resource.belongs_to_config
46
+ return unless btc
47
+
48
+ key = btc.to_param.to_s
49
+ val = @graph_params[key]
50
+ h[key] = val.to_s if val.present?
51
+ end
52
+
53
+ def stub_controller!(controller, hash)
54
+ user = @user
55
+ params_obj = ActionController::Parameters.new(hash)
56
+ params_obj.permit!
57
+
58
+ controller.define_singleton_method(:params) { params_obj }
59
+ controller.define_singleton_method(:current_active_admin_user) { user }
60
+ controller.define_singleton_method(:current_user) { user }
61
+ end
62
+
63
+ def normalize_batch_inputs(inputs)
64
+ return {} if inputs.nil?
65
+
66
+ case inputs
67
+ when Array
68
+ KeyValuePairs.to_hash(inputs)
69
+ when String
70
+ JSON.parse(inputs)
71
+ when Hash
72
+ inputs
73
+ when ActionController::Parameters
74
+ inputs.to_unsafe_h
75
+ else
76
+ raise ::GraphQL::ExecutionError, "batch inputs must be a list of key/value pairs or a JSON object"
77
+ end.then do |h|
78
+ raise ::GraphQL::ExecutionError, "batch inputs must resolve to a hash" unless h.is_a?(Hash)
79
+
80
+ h.deep_stringify_keys
81
+ end
82
+ rescue JSON::ParserError => e
83
+ raise ::GraphQL::ExecutionError, "invalid batch inputs JSON (#{e.message})"
84
+ end
85
+
86
+ def normalize_extra_params(extra_params)
87
+ return {} if extra_params.nil?
88
+
89
+ if extra_params.is_a?(Array)
90
+ return KeyValuePairs.to_hash(extra_params)
91
+ end
92
+
93
+ h = extra_params.respond_to?(:to_unsafe_h) ? extra_params.to_unsafe_h : extra_params.to_h
94
+ h.stringify_keys
95
+ end
96
+
97
+ def attach_graphql_request!(controller)
98
+ return if controller.instance_variable_defined?(:@_request) && controller.request
99
+
100
+ env = Rack::MockRequest.env_for("http://test.host/", method: "POST", params: {_graphql: "1"})
101
+ req = ActionDispatch::Request.new(env)
102
+ res = ActionDispatch::Response.new
103
+ controller.send(:set_request!, req)
104
+ controller.send(:set_response!, res)
105
+ controller.define_singleton_method(:default_url_options) { {host: "test.host"} }
106
+ end
107
+
108
+ def perform_controller_command!(controller)
109
+ attach_graphql_request!(controller)
110
+ yield
111
+ build_run_payload(controller)
112
+ rescue ActiveAdmin::AccessDenied => e
113
+ raise ::GraphQL::ExecutionError, e.message
114
+ end
115
+
116
+ def build_run_payload(controller)
117
+ res = controller.response
118
+ RunActionPayload::Result.new(
119
+ ok: true,
120
+ status: res&.status,
121
+ location: extract_location(res),
122
+ body: extract_response_body(res)
123
+ )
124
+ end
125
+
126
+ def extract_location(res)
127
+ return nil unless res
128
+
129
+ loc = res.location if res.respond_to?(:location)
130
+ loc.presence || res.headers&.[]("Location")
131
+ end
132
+
133
+ def extract_response_body(res)
134
+ return nil unless res
135
+
136
+ b = res.body
137
+ return nil if b.nil? || (b.respond_to?(:empty?) && b.empty?)
138
+
139
+ s = b.is_a?(String) ? b : b.to_s
140
+ s = +s
141
+ s.force_encoding("UTF-8")
142
+ s.valid_encoding? ? s : nil
143
+ rescue
144
+ nil
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/mock"
4
+
5
+ require_relative "resource_query_proxy/controller"
6
+
7
+ module ActiveAdmin
8
+ module GraphQL
9
+ # Reuses {ResourceController} data-access behaviour (+scoped_collection+, +find_collection+,
10
+ # +find_resource+, authorization scoping, Ransack, menu scopes, +sorting+, +includes+)
11
+ # so GraphQL list/detail mutations align with the HTML/JSON list and member REST endpoints.
12
+ class ResourceQueryProxy
13
+ include Controller
14
+
15
+ def initialize(aa_resource:, user:, namespace:, graph_params: {})
16
+ @aa_resource = aa_resource
17
+ @user = user
18
+ @namespace = namespace
19
+ @graph_params = normalize_graph_params(graph_params)
20
+ end
21
+
22
+ def relation_for_index
23
+ controller_for("index").send(:find_collection, except: %i[pagination collection_decorator])
24
+ end
25
+
26
+ def find_member(id)
27
+ extra = member_route_params_for_find(id)
28
+ controller_for("show", extra).send(:find_resource)
29
+ end
30
+
31
+ def build_new(attributes)
32
+ if missing_required_belongs_to?
33
+ raise ::GraphQL::ExecutionError,
34
+ "#{@aa_resource.belongs_to_config.to_param} is required for nested resource #{@aa_resource.resource_name}"
35
+ end
36
+
37
+ c = controller_for("new")
38
+ chain = c.send(:apply_authorization_scope, c.send(:scoped_collection))
39
+ permitted = attributes.stringify_keys.slice(*@aa_resource.graphql_assignable_attribute_names)
40
+ chain.build(permitted)
41
+ end
42
+
43
+ def run_batch_action(batch_sym, ids, inputs: {})
44
+ unless @aa_resource.batch_actions_enabled?
45
+ raise ::GraphQL::ExecutionError, "batch actions are disabled for #{@aa_resource.resource_name}"
46
+ end
47
+
48
+ inputs = normalize_batch_inputs(inputs)
49
+ extra = {
50
+ "batch_action" => batch_sym.to_s,
51
+ "collection_selection" => Array(ids).map(&:to_s),
52
+ "batch_action_inputs" => inputs.to_json
53
+ }
54
+ c = controller_for("batch_action", extra)
55
+ perform_controller_command!(c) { c.send(:batch_action) }
56
+ end
57
+
58
+ def run_member_action(action_name, id, extra_params: {})
59
+ action_name = action_name.to_s
60
+ unless @aa_resource.member_actions.any? { |a| a.name.to_s == action_name }
61
+ raise ::GraphQL::ExecutionError, "unknown member action #{action_name.inspect}"
62
+ end
63
+
64
+ extras = normalize_extra_params(extra_params)
65
+ c = controller_for(action_name, {"id" => id.to_s}.merge(extras))
66
+ perform_controller_command!(c) { c.send(action_name) }
67
+ end
68
+
69
+ def run_collection_action(action_name, extra_params: {})
70
+ action_name = action_name.to_s
71
+ unless @aa_resource.collection_actions.any? { |a| a.name.to_s == action_name }
72
+ raise ::GraphQL::ExecutionError, "unknown collection action #{action_name.inspect}"
73
+ end
74
+
75
+ extras = normalize_extra_params(extra_params)
76
+ c = controller_for(action_name, extras)
77
+ perform_controller_command!(c) { c.send(action_name) }
78
+ end
79
+
80
+ private
81
+
82
+ def member_route_params_for_find(id)
83
+ model = @aa_resource.resource_class
84
+ if ActiveAdmin::PrimaryKey.composite?(model)
85
+ attrs = ActiveAdmin::PrimaryKey.find_attributes(model, id)
86
+ tuple = ActiveAdmin::PrimaryKey.ordered_columns(model).map { |c| attrs[c] }
87
+ {"id" => tuple}
88
+ elsif id.is_a?(Hash)
89
+ id.stringify_keys
90
+ else
91
+ {"id" => id.to_s}
92
+ end
93
+ end
94
+
95
+ def missing_required_belongs_to?
96
+ btc = @aa_resource.belongs_to_config
97
+ return false unless btc&.required?
98
+
99
+ key = btc.to_param.to_s
100
+ val = @graph_params[key] || @graph_params[key.to_sym]
101
+ val.blank?
102
+ end
103
+
104
+ def controller_for(action, extra = {})
105
+ c = @aa_resource.controller.new
106
+ h = param_hash_for(action, extra)
107
+ stub_controller!(c, h)
108
+ c
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Pairs return type and optional resolver for one run-action mutation kind (+batch+, +member+,
6
+ # or +collection+). Default return type resolution falls back to {ResourceConfig#run_action_payload_type},
7
+ # then {RunActionPayload}.
8
+ class RunActionMutationConfig
9
+ # @return [Class, nil] +GraphQL::Schema::Object+ subclass
10
+ attr_accessor :payload_type
11
+ # @return [Proc, nil]
12
+ attr_accessor :resolve_proc
13
+ # Optional block evaluated in the graphql-ruby +field+ DSL context (+argument+, …) for per-action fields.
14
+ attr_accessor :arguments_proc
15
+
16
+ def self.ensure_graphql_object_subclass!(type)
17
+ unless type.is_a?(Class) && type < ::GraphQL::Schema::Object
18
+ raise ArgumentError, "Expected a GraphQL::Schema::Object subclass, got #{type.inspect}"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "run_action_mutation_config"
4
+
5
+ module ActiveAdmin
6
+ module GraphQL
7
+ # Nested DSL for a single run-action mutation kind (+batch_action_mutation+, etc.): +type+ and +resolve+,
8
+ # similar to graphql-ruby pairing field type with resolution.
9
+ class RunActionMutationDSL
10
+ def initialize(mutation_config)
11
+ @mutation_config = mutation_config
12
+ end
13
+
14
+ # GraphQL object type for this mutation field (alias: +payload_type+).
15
+ def type(gql_object_class)
16
+ RunActionMutationConfig.ensure_graphql_object_subclass!(gql_object_class)
17
+ @mutation_config.payload_type = gql_object_class
18
+ end
19
+ alias_method :payload_type, :type
20
+
21
+ def resolve(&block)
22
+ @mutation_config.resolve_proc = block
23
+ end
24
+
25
+ # Per-action fields only: extra GraphQL arguments (+argument :reason, String, …+).
26
+ # Not used on the aggregate +posts_member_action(action: …)+ field.
27
+ def arguments(&block)
28
+ @mutation_config.arguments_proc = block
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Return type for mutations that run batch, member, or collection controller actions.
6
+ class RunActionPayload < ::GraphQL::Schema::Object
7
+ graphql_name "ActiveAdminRunActionPayload"
8
+
9
+ def self.visible?(context)
10
+ hook = context[:namespace]&.graphql_schema_visible
11
+ return super if hook.nil?
12
+
13
+ super && !!hook.call(context, {kind: :run_action_payload})
14
+ end
15
+
16
+ field :ok, ::GraphQL::Types::Boolean, null: false
17
+ field :status, ::GraphQL::Types::Int, null: true,
18
+ description: "HTTP-style status from the controller response, when set."
19
+ field :location, ::GraphQL::Types::String, null: true,
20
+ description: "Redirect target, if the action called +redirect_to+."
21
+ field :body, ::GraphQL::Types::String, null: true,
22
+ description: "Response body text when the action rendered (e.g. JSON)."
23
+
24
+ Result = Struct.new(:ok, :status, :location, :body, keyword_init: true)
25
+ end
26
+ end
27
+ end