activeadmin-graphql 0.3.2 → 0.4.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -1
  3. data/app/controllers/active_admin/graphql_controller.rb +19 -4
  4. data/docs/changelogs/20261010091415_graphql-resource-enabled-default.md +34 -0
  5. data/docs/changelogs/20261012050051_graphql-surface-toggles.md +40 -0
  6. data/docs/changelogs/20261012064500_graphql-capabilities.md +35 -0
  7. data/docs/changelogs/20261012113500_release-0-3-3.md +45 -0
  8. data/docs/changelogs/20261012200000_host-customization-hooks.md +27 -0
  9. data/docs/changelogs/20261013080000_release-0-4-0.md +48 -0
  10. data/docs/graphql-api.md +173 -10
  11. data/docs/issues/20261012200000_host-customization-gaps.md +26 -0
  12. data/lib/active_admin/graphql/capability.rb +35 -0
  13. data/lib/active_admin/graphql/capability_definition_dsl.rb +69 -0
  14. data/lib/active_admin/graphql/collection_query_policy.rb +103 -0
  15. data/lib/active_admin/graphql/integration.rb +43 -1
  16. data/lib/active_admin/graphql/member_action_capability.rb +14 -0
  17. data/lib/active_admin/graphql/member_action_dry_run.rb +121 -0
  18. data/lib/active_admin/graphql/policy_adapter.rb +94 -0
  19. data/lib/active_admin/graphql/policy_set_cache.rb +10 -6
  20. data/lib/active_admin/graphql/resource_capability_dsl.rb +32 -0
  21. data/lib/active_admin/graphql/resource_capability_maps.rb +77 -0
  22. data/lib/active_admin/graphql/resource_config.rb +19 -2
  23. data/lib/active_admin/graphql/resource_definition_dsl.rb +22 -10
  24. data/lib/active_admin/graphql/resource_methods.rb +7 -1
  25. data/lib/active_admin/graphql/resource_surface_dsl.rb +48 -0
  26. data/lib/active_admin/graphql/run_action_capability.rb +62 -0
  27. data/lib/active_admin/graphql/run_action_control_extension.rb +72 -0
  28. data/lib/active_admin/graphql/run_action_field.rb +103 -0
  29. data/lib/active_admin/graphql/run_action_mutation_config.rb +8 -0
  30. data/lib/active_admin/graphql/run_action_mutation_dsl.rb +18 -0
  31. data/lib/active_admin/graphql/run_action_preview.rb +20 -0
  32. data/lib/active_admin/graphql/schema_builder/build.rb +52 -3
  33. data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +7 -0
  34. data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +9 -0
  35. data/lib/active_admin/graphql/schema_builder/mutation_create.rb +1 -0
  36. data/lib/active_admin/graphql/schema_builder/mutation_member.rb +13 -0
  37. data/lib/active_admin/graphql/schema_builder/mutation_type_builder.rb +15 -10
  38. data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +2 -0
  39. data/lib/active_admin/graphql/schema_builder/query_type.rb +2 -1
  40. data/lib/active_admin/graphql/schema_builder/query_type_collection.rb +3 -0
  41. data/lib/active_admin/graphql/schema_builder/query_type_member.rb +1 -0
  42. data/lib/active_admin/graphql/schema_builder/query_type_policies.rb +9 -63
  43. data/lib/active_admin/graphql/schema_builder/resolvers.rb +46 -7
  44. data/lib/active_admin/graphql/schema_builder/resources.rb +34 -0
  45. data/lib/active_admin/graphql/schema_builder/types_object.rb +19 -8
  46. data/lib/active_admin/graphql/schema_builder/visibility.rb +22 -7
  47. data/lib/active_admin/graphql/schema_builder/wire.rb +4 -1
  48. data/lib/active_admin/graphql/schema_field.rb +97 -6
  49. data/lib/active_admin/graphql/version.rb +1 -1
  50. data/lib/active_admin/graphql.rb +9 -0
  51. metadata +21 -1
@@ -3,19 +3,20 @@
3
3
  require_relative "run_action_mutation_config"
4
4
  require_relative "run_action_mutation_dsl"
5
5
  require_relative "mutation_input_definition_dsl"
6
+ require_relative "resource_surface_dsl"
7
+ require_relative "resource_capability_dsl"
6
8
 
7
9
  module ActiveAdmin
8
10
  module GraphQL
9
11
  # DSL for +graphql do ... end+ inside +ActiveAdmin.register+.
10
12
  class ResourceDefinitionDSL
13
+ include ResourceSurfaceDSL
14
+ include ResourceCapabilityDSL
15
+
11
16
  def initialize(config)
12
17
  @config = config
13
18
  end
14
19
 
15
- def disable!
16
- @config.enabled = false
17
- end
18
-
19
20
  # Declare GraphQL identity columns when ActiveRecord has no primary key / id column.
20
21
  # See RFC 0007.
21
22
  def identity(*columns, separator: nil, encode: nil, decode: nil)
@@ -102,21 +103,32 @@ module ActiveAdmin
102
103
  end
103
104
 
104
105
  # Pair +type+ and +resolve+ for +posts_batch_action+ (graphql-ruby-style block).
105
- def batch_action_mutation(&block)
106
- RunActionMutationDSL.new(@config.batch_run_action).instance_exec(&block)
106
+ # +capability:+ overrides the resource map / namespace default for the aggregate field (RFC 0012).
107
+ def batch_action_mutation(capability: nil, &block)
108
+ cfg = @config.batch_run_action
109
+ cfg.capability = capability.to_sym if capability
110
+ RunActionMutationDSL.new(cfg).instance_exec(&block) if block
111
+ cfg
107
112
  end
108
113
 
109
114
  # With no name: configures the aggregate +posts_member_action(action: …)+ field.
110
115
  # With a symbol/string: configures +posts_member_<action>+ (one field per +member_action+), so each
111
116
  # action can use its own +type+, +resolve+, +arguments+, and GraphQL inputs.
112
- def member_action_mutation(name = nil, &block)
117
+ # +dry_run:+ / +capability:+ apply to named per-action fields (RFC 0010, RFC 0012).
118
+ def member_action_mutation(name = nil, dry_run: false, capability: nil, &block)
113
119
  cfg = name.nil? ? @config.member_run_action : @config.member_action_mutation_for(name)
114
- RunActionMutationDSL.new(cfg).instance_exec(&block)
120
+ cfg.dry_run = true if dry_run
121
+ cfg.capability = capability.to_sym if capability
122
+ RunActionMutationDSL.new(cfg).instance_exec(&block) if block
123
+ cfg
115
124
  end
116
125
 
117
- def collection_action_mutation(name = nil, &block)
126
+ def collection_action_mutation(name = nil, dry_run: false, capability: nil, &block)
118
127
  cfg = name.nil? ? @config.collection_run_action : @config.collection_action_mutation_for(name)
119
- RunActionMutationDSL.new(cfg).instance_exec(&block)
128
+ cfg.dry_run = true if dry_run
129
+ cfg.capability = capability.to_sym if capability
130
+ RunActionMutationDSL.new(cfg).instance_exec(&block) if block
131
+ cfg
120
132
  end
121
133
 
122
134
  # Default return type for all run-action fields unless a kind-specific +type+ is set inside
@@ -7,7 +7,13 @@ module ActiveAdmin
7
7
  SERVER_OWNED_TIMESTAMP_COLUMNS = %w[created_at updated_at].freeze
8
8
 
9
9
  def graphql_config
10
- @graphql_config ||= ActiveAdmin::GraphQL::ResourceConfig.new
10
+ @graphql_config ||= ActiveAdmin::GraphQL::ResourceConfig.new(
11
+ enabled: namespace.graphql_resource_enabled_default,
12
+ queries_enabled: namespace.graphql_queries_enabled_default,
13
+ mutations_enabled: namespace.graphql_mutations_enabled_default,
14
+ run_actions_enabled: namespace.graphql_run_actions_enabled_default,
15
+ policies_enabled: namespace.graphql_policies_enabled_default
16
+ )
11
17
  end
12
18
 
13
19
  def attributes_for_graphql
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Inclusion and schema-surface toggles for +graphql do ... end+.
6
+ module ResourceSurfaceDSL
7
+ def enable!
8
+ @config.enabled = true
9
+ end
10
+
11
+ def disable!
12
+ @config.enabled = false
13
+ end
14
+
15
+ def enable_queries!
16
+ @config.queries_enabled = true
17
+ end
18
+
19
+ def disable_queries!
20
+ @config.queries_enabled = false
21
+ end
22
+
23
+ def enable_mutations!
24
+ @config.mutations_enabled = true
25
+ end
26
+
27
+ def disable_mutations!
28
+ @config.mutations_enabled = false
29
+ end
30
+
31
+ def enable_run_actions!
32
+ @config.run_actions_enabled = true
33
+ end
34
+
35
+ def disable_run_actions!
36
+ @config.run_actions_enabled = false
37
+ end
38
+
39
+ def enable_policies!
40
+ @config.policies_enabled = true
41
+ end
42
+
43
+ def disable_policies!
44
+ @config.policies_enabled = false
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Resolves the capability for batch / member / collection run-action fields (RFC 0012).
6
+ module RunActionCapability
7
+ KINDS = %i[batch member collection].freeze
8
+
9
+ module_function
10
+
11
+ # @param kind [Symbol] +:batch+, +:member+, or +:collection+
12
+ # @param action_name [String, Symbol, nil] per-action name; +nil+ for the aggregate field
13
+ def resolve(namespace, aa_res, kind, action_name = nil)
14
+ kind = kind.to_sym
15
+ unless KINDS.include?(kind)
16
+ raise ArgumentError, "run-action kind must be one of #{KINDS.inspect}, got #{kind.inspect}"
17
+ end
18
+
19
+ if action_name
20
+ per_capability = per_action_config_capability(aa_res, kind, action_name)
21
+ return per_capability if per_capability
22
+
23
+ mapped = aa_res.graphql_config.capability_for_mutation(
24
+ :"#{kind}_#{action_name.to_s.tr("-", "_")}"
25
+ )
26
+ else
27
+ aggregate_capability = aggregate_config_capability(aa_res, kind)
28
+ return aggregate_capability if aggregate_capability
29
+
30
+ mapped = aa_res.graphql_config.capability_for_mutation(:"#{kind}_action")
31
+ end
32
+ return mapped if mapped
33
+
34
+ namespace.public_send(:"graphql_#{kind}_action_capability_default")&.to_sym
35
+ end
36
+
37
+ def per_action_config_capability(aa_res, kind, action_name)
38
+ map =
39
+ case kind
40
+ when :member then aa_res.graphql_config.member_action_mutations
41
+ when :collection then aa_res.graphql_config.collection_action_mutations
42
+ when :batch then nil
43
+ end
44
+ return nil unless map
45
+
46
+ map[action_name.to_s]&.capability
47
+ end
48
+ module_function :per_action_config_capability
49
+
50
+ def aggregate_config_capability(aa_res, kind)
51
+ cfg =
52
+ case kind
53
+ when :member then aa_res.graphql_config.member_run_action
54
+ when :batch then aa_res.graphql_config.batch_run_action
55
+ when :collection then aa_res.graphql_config.collection_run_action
56
+ end
57
+ cfg&.capability
58
+ end
59
+ module_function :aggregate_config_capability
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Confirm gate and optional preview short-circuit for custom run-action fields (RFC 0013).
6
+ class RunActionControlExtension < ::GraphQL::Schema::FieldExtension
7
+ def resolve(arguments:, context:, object: nil, objects: nil)
8
+ if options[:confirm] && !arguments[:dry_run] && !arguments[:confirm]
9
+ result = RunActionPayload::Result.new(
10
+ ok: false, status: 422, location: nil, body: "confirm_required"
11
+ )
12
+ audit!(context, result, dry_run: false)
13
+ return result
14
+ end
15
+
16
+ preview_proc = options[:preview]
17
+ if preview_proc
18
+ preview_result = run_preview(preview_proc, arguments, context)
19
+
20
+ if arguments[:dry_run]
21
+ result = preview_result || default_dry_run_result
22
+ audit!(context, result, dry_run: true)
23
+ return result
24
+ end
25
+
26
+ if preview_result&.respond_to?(:ok) && !preview_result.ok
27
+ audit!(context, preview_result, dry_run: false)
28
+ return preview_result
29
+ end
30
+ end
31
+
32
+ yield(object.nil? ? objects : object, arguments, nil)
33
+ end
34
+
35
+ private
36
+
37
+ def run_preview(preview_proc, arguments, context)
38
+ kwargs = arguments.respond_to?(:to_h) ? arguments.to_h : arguments
39
+ kwargs = kwargs.merge(context: context)
40
+ RunActionPreview.call(preview_proc, **kwargs.transform_keys(&:to_sym))
41
+ end
42
+
43
+ def default_dry_run_result
44
+ RunActionPayload::Result.new(
45
+ ok: true,
46
+ status: 200,
47
+ location: nil,
48
+ body: "would_run:#{field.graphql_name}"
49
+ )
50
+ end
51
+
52
+ def audit!(context, result, dry_run:)
53
+ hook = context[:namespace]&.graphql_run_action_audit
54
+ return if hook.nil?
55
+
56
+ hook.call(
57
+ context,
58
+ {
59
+ action: field.graphql_name,
60
+ id: nil,
61
+ status: result.respond_to?(:status) ? result.status : nil,
62
+ dry_run: dry_run,
63
+ ok: result.respond_to?(:ok) ? result.ok : nil,
64
+ reason: result.respond_to?(:body) ? result.body : nil
65
+ }
66
+ )
67
+ rescue
68
+ nil
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # DSL sugar for custom run-action mutation fields (RFC 0013).
6
+ #
7
+ # ActiveAdmin::GraphQL::RunActionField.define(schema.mutation, :create_batch,
8
+ # dry_run: true, confirm: true, capability: :support_act) do
9
+ # argument :q, GraphQL::Types::JSON, required: true, camelize: false
10
+ # preview { |**| ok("would_run:create_batch") }
11
+ # resolve { |q:, **| HostService.call(q: q) }
12
+ # end
13
+ module RunActionField
14
+ module_function
15
+
16
+ def define(
17
+ owner,
18
+ name,
19
+ type: RunActionPayload,
20
+ null: false,
21
+ camelize: false,
22
+ dry_run: false,
23
+ confirm: false,
24
+ capability: nil,
25
+ authorize: false,
26
+ visibility: nil,
27
+ **kwargs,
28
+ &block
29
+ )
30
+ definition = Definition.new
31
+ definition.instance_exec(&block) if block
32
+ unless definition.resolve_proc
33
+ raise ArgumentError, "run_action_field #{name.inspect} requires a resolve block"
34
+ end
35
+
36
+ owner.field(
37
+ name,
38
+ type,
39
+ null: null,
40
+ camelize: camelize,
41
+ dry_run: dry_run,
42
+ confirm: confirm,
43
+ preview: definition.preview_proc,
44
+ capability: capability,
45
+ authorize: authorize,
46
+ visibility: visibility,
47
+ **kwargs,
48
+ &definition.field_configuration_block
49
+ )
50
+
51
+ resolve_proc = definition.resolve_proc
52
+ owner.define_method(name) do |**args|
53
+ resolve_proc.call(**args)
54
+ end
55
+ end
56
+
57
+ # Block body for {RunActionField.define}.
58
+ class Definition
59
+ attr_reader :preview_proc, :resolve_proc
60
+
61
+ def initialize
62
+ @argument_calls = []
63
+ @arguments_proc = nil
64
+ end
65
+
66
+ def argument(*args, **kwargs)
67
+ @argument_calls << [args, kwargs]
68
+ end
69
+
70
+ def arguments(&block)
71
+ @arguments_proc = block
72
+ end
73
+
74
+ def preview(&block)
75
+ @preview_proc = block
76
+ end
77
+
78
+ def resolve(&block)
79
+ @resolve_proc = block
80
+ end
81
+
82
+ def ok(body, status: 200)
83
+ RunActionPreview.new.ok(body, status: status)
84
+ end
85
+
86
+ def refuse(body, status: 422)
87
+ RunActionPreview.new.refuse(body, status: status)
88
+ end
89
+
90
+ def field_configuration_block
91
+ argument_calls = @argument_calls
92
+ arguments_proc = @arguments_proc
93
+ proc do
94
+ instance_exec(&arguments_proc) if arguments_proc
95
+ argument_calls.each do |args, kwargs|
96
+ argument(*args, **kwargs)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -14,6 +14,14 @@ module ActiveAdmin
14
14
  attr_accessor :arguments_proc
15
15
  # Optional authorization toggle for this mutation configuration. +nil+ means namespace default.
16
16
  attr_accessor :authorize
17
+ # When true, inject +dry_run+ and honor +preview+ (RFC 0010).
18
+ attr_accessor :dry_run
19
+ # @return [Proc, nil] preview block for dry-run / refuse-before-run
20
+ attr_accessor :preview_proc
21
+ # @return [Array<String>, nil] allowlisted extra GraphQL kwargs forwarded into AA params
22
+ attr_accessor :extra_param_keys
23
+ # Optional capability override for this action field (RFC 0012).
24
+ attr_accessor :capability
17
25
 
18
26
  def self.ensure_graphql_object_subclass!(type)
19
27
  unless type.is_a?(Class) && type < ::GraphQL::Schema::Object
@@ -31,6 +31,24 @@ module ActiveAdmin
31
31
  def authorize(value = true)
32
32
  @mutation_config.authorize = !!value
33
33
  end
34
+
35
+ # Preview before run / dry-run short-circuit (RFC 0010). Block receives the member record.
36
+ def preview(&block)
37
+ @mutation_config.preview_proc = block
38
+ end
39
+
40
+ # Allowlisted GraphQL kwargs forwarded into the ActiveAdmin request (never +id+).
41
+ def extra_params(*names)
42
+ @mutation_config.extra_param_keys = names.flatten.map(&:to_s)
43
+ end
44
+
45
+ def ok(body, status: 200)
46
+ RunActionPreview.new.ok(body, status: status)
47
+ end
48
+
49
+ def refuse(body, status: 422)
50
+ RunActionPreview.new.refuse(body, status: status)
51
+ end
34
52
  end
35
53
  end
36
54
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Shared +ok+ / +refuse+ helpers for run-action preview blocks (RFC 0010, RFC 0013).
6
+ class RunActionPreview
7
+ def ok(body, status: 200)
8
+ RunActionPayload::Result.new(ok: true, status: status, location: nil, body: body.to_s)
9
+ end
10
+
11
+ def refuse(body, status: 422)
12
+ RunActionPayload::Result.new(ok: false, status: status, location: nil, body: body.to_s)
13
+ end
14
+
15
+ def self.call(preview_proc, **kwargs)
16
+ new.instance_exec(**kwargs, &preview_proc)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -25,7 +25,8 @@ module ActiveAdmin
25
25
  @aa_by_graphql_type_name[graphql_type_name_for(aa_res)] = aa_res
26
26
  end
27
27
 
28
- wire_belongs_to_associations
28
+ wire_belongs_to_associations if @namespace.graphql_belongs_to_associations_enabled
29
+ verify_field_capabilities!
29
30
 
30
31
  active_resources.each do |aa_res|
31
32
  model = aa_res.resource_class
@@ -35,9 +36,10 @@ module ActiveAdmin
35
36
  @find_input_types[model] = build_find_input_type(aa_res)
36
37
  end
37
38
 
38
- union_members = @object_types.values.uniq
39
+ queryable = queryable_resources
40
+ union_members = queryable.filter_map { |aa_res| @object_types[aa_res.resource_class] }.uniq
39
41
  registered_resource_union =
40
- if union_members.any?
42
+ if @namespace.graphql_registered_resource_enabled && union_members.any?
41
43
  u = Class.new(::GraphQL::Schema::Union) do
42
44
  graphql_name "ActiveAdminRegisteredResource"
43
45
  description "Any resource object type registered for GraphQL in this namespace."
@@ -47,10 +49,12 @@ module ActiveAdmin
47
49
  u
48
50
  end
49
51
 
52
+ ensure_query_surface!(registered_resource_union: registered_resource_union)
50
53
  query_type = build_query_type(registered_resource_union: registered_resource_union)
51
54
  mutation_type = build_mutation_type
52
55
 
53
56
  model_to_type = @object_types
57
+ queryable_models = queryable.map(&:resource_class)
54
58
 
55
59
  schema = Class.new(::GraphQL::Schema)
56
60
  schema.query(query_type)
@@ -61,6 +65,11 @@ module ActiveAdmin
61
65
  if registered_resource_union
62
66
  schema.define_singleton_method(:resolve_type) do |abstract_type, obj, ctx|
63
67
  if abstract_type == registered_resource_union
68
+ unless queryable_models.include?(obj.class)
69
+ raise ::GraphQL::ExecutionError,
70
+ "ActiveAdminRegisteredResource could not resolve type for #{obj.class.name}"
71
+ end
72
+
64
73
  typ = model_to_type[obj.class]
65
74
  unless typ
66
75
  raise ::GraphQL::ExecutionError,
@@ -78,6 +87,46 @@ module ActiveAdmin
78
87
  hook.call(schema) if hook.respond_to?(:call)
79
88
  schema
80
89
  end
90
+
91
+ private
92
+
93
+ def ensure_query_surface!(registered_resource_union:)
94
+ return if registered_resource_union
95
+ return if queryable_resources.any?
96
+ return if policy_adapter
97
+ return if namespace_page_graphql_fields?
98
+
99
+ raise ActiveAdmin::DependencyError,
100
+ "GraphQL Query type has no fields for namespace #{@namespace.name.inspect}. " \
101
+ "Enable list/show (graphql_queries_enabled_default / enable_queries!), " \
102
+ "policies (graphql_policies_enabled), registered_resource, " \
103
+ "or a page graphql_field — or turn off graphql for this namespace."
104
+ end
105
+
106
+ def namespace_page_graphql_fields?
107
+ @namespace.resources.any? do |resource|
108
+ resource.is_a?(ActiveAdmin::Page) && resource.graphql_fields.any?
109
+ end
110
+ end
111
+
112
+ def verify_field_capabilities!
113
+ @object_types.each do |model, type_class|
114
+ aa_res = @aa_by_model[model]
115
+ next unless aa_res
116
+
117
+ declared = aa_res.graphql_config.field_capabilities.keys
118
+ next if declared.empty?
119
+
120
+ present = type_class.own_fields.keys.map { |name| name.to_s.to_sym }
121
+ unknown = declared - present
122
+ next if unknown.empty?
123
+
124
+ raise ActiveAdmin::DependencyError,
125
+ "GraphQL capability fields #{unknown.map(&:inspect).join(", ")} are not on " \
126
+ "#{type_class.graphql_name} (resource #{aa_res.resource_name}). " \
127
+ "Use a generated attribute, belongs_to association, or configure field name."
128
+ end
129
+ end
81
130
  end
82
131
  end
83
132
  end
@@ -15,6 +15,7 @@ module ActiveAdmin
15
15
  proc do
16
16
  field fname.to_sym, batch_payload_t, null: false, camelize: false,
17
17
  visibility: {kind: :mutation_batch_action, graphql_type_name: sb.send(:graphql_type_name_for, aa_res), resource: aa_res, field_name: fname},
18
+ capability: RunActionCapability.resolve(ns, aa_res, :batch),
18
19
  description: "Run a batch action registered on #{aa_res.resource_name} (+batch_action+ DSL), " \
19
20
  "same as the index batch UI (+collection_selection+ / +batch_action_inputs+)." do
20
21
  argument :batch_action, ::GraphQL::Types::String, required: true, camelize: false
@@ -39,6 +40,12 @@ module ActiveAdmin
39
40
  unless allowed.include?(ba)
40
41
  raise ::GraphQL::ExecutionError, "Unknown batch_action #{ba.inspect} for #{plural}"
41
42
  end
43
+ unless Capability.granted?(
44
+ context,
45
+ RunActionCapability.resolve(ns, aa_res, :batch, ba)
46
+ )
47
+ raise ::GraphQL::ExecutionError, "not authorized for batch action #{ba}"
48
+ end
42
49
 
43
50
  max_ids = ns.graphql_batch_action_max_ids
44
51
  if max_ids.is_a?(Integer) && max_ids.positive? && ids.size > max_ids
@@ -21,6 +21,7 @@ module ActiveAdmin
21
21
  proc do
22
22
  field fname.to_sym, collection_payload_t, null: false, camelize: false,
23
23
  visibility: {kind: :mutation_collection_action, graphql_type_name: sb.send(:graphql_type_name_for, aa_res), resource: aa_res, field_name: fname, aggregate: true},
24
+ capability: RunActionCapability.resolve(ns, aa_res, :collection),
24
25
  description: "Invoke a +collection_action+ on #{aa_res.resource_name} by name (same scoping as REST +index+). Prefer per-action fields +#{plural}_collection_<action>+ for distinct inputs or return types." do
25
26
  argument :action, ::GraphQL::Types::String, required: true, camelize: false
26
27
  argument :params, [KeyValuePairInput], required: false, camelize: false,
@@ -43,6 +44,12 @@ module ActiveAdmin
43
44
  unless allowed.include?(act)
44
45
  raise ::GraphQL::ExecutionError, "Unknown collection action #{act.inspect} for #{plural}"
45
46
  end
47
+ unless Capability.granted?(
48
+ context,
49
+ RunActionCapability.resolve(ns, aa_res, :collection, act)
50
+ )
51
+ raise ::GraphQL::ExecutionError, "not authorized for collection action #{act}"
52
+ end
46
53
 
47
54
  proxy = ResourceQueryProxy.new(
48
55
  aa_resource: aa_res,
@@ -69,6 +76,7 @@ module ActiveAdmin
69
76
  fname = field_name.to_sym
70
77
  per_cfg = aa_res.graphql_config.collection_action_mutations[action_name]
71
78
  collection_payload_t = collection_action_return_type(aa_res, action_name)
79
+ capability = RunActionCapability.resolve(ns, aa_res, :collection, action_name)
72
80
 
73
81
  proc do
74
82
  field fname, collection_payload_t, null: false, camelize: false,
@@ -79,6 +87,7 @@ module ActiveAdmin
79
87
  field_name: field_name,
80
88
  collection_action: action_name
81
89
  },
90
+ capability: capability,
82
91
  description: "Invoke +collection_action+ #{action_name} on #{aa_res.resource_name} (same scoping as REST +index+)." do
83
92
  argument :params, [KeyValuePairInput], required: false, camelize: false,
84
93
  description: "Extra request params as flat key/value pairs."
@@ -13,6 +13,7 @@ module ActiveAdmin
13
13
  proc do
14
14
  field fname.to_sym, type_c, null: true, camelize: false,
15
15
  visibility: {kind: :mutation_create, graphql_type_name: sb.send(:graphql_type_name_for, aa_res), resource: aa_res},
16
+ capability: aa_res.graphql_config.capability_for_mutation(:create),
16
17
  description: "Create #{model.name}" do
17
18
  argument :input, create_input, required: true, camelize: false
18
19
  end
@@ -21,6 +21,7 @@ module ActiveAdmin
21
21
  proc do
22
22
  field fname.to_sym, member_payload_t, null: false, camelize: false,
23
23
  visibility: {kind: :mutation_member_action, graphql_type_name: sb.send(:graphql_type_name_for, aa_res), resource: aa_res, field_name: fname, aggregate: true},
24
+ capability: MemberActionCapability.resolve(ns, aa_res),
24
25
  description: "Invoke a +member_action+ on #{aa_res.resource_name} by name (same scoping as REST +show+). Prefer per-action fields +#{plural}_member_<action>+ for distinct inputs or return types." do
25
26
  argument :action, ::GraphQL::Types::String, required: true, camelize: false
26
27
  argument :id, ::GraphQL::Types::ID, required: true, camelize: false
@@ -44,6 +45,12 @@ module ActiveAdmin
44
45
  unless allowed.include?(act)
45
46
  raise ::GraphQL::ExecutionError, "Unknown member action #{act.inspect} for #{plural}"
46
47
  end
48
+ unless Capability.granted?(
49
+ context,
50
+ MemberActionCapability.resolve(ns, aa_res, act)
51
+ )
52
+ raise ::GraphQL::ExecutionError, "not authorized for member action #{act}"
53
+ end
47
54
 
48
55
  proxy = ResourceQueryProxy.new(
49
56
  aa_resource: aa_res,
@@ -71,6 +78,7 @@ module ActiveAdmin
71
78
  fname = field_name.to_sym
72
79
  per_cfg = aa_res.graphql_config.member_action_mutations[action_name]
73
80
  member_payload_t = member_action_return_type(aa_res, action_name)
81
+ capability = MemberActionCapability.resolve(ns, aa_res, action_name)
74
82
 
75
83
  proc do
76
84
  field fname, member_payload_t, null: false, camelize: false,
@@ -81,10 +89,15 @@ module ActiveAdmin
81
89
  field_name: field_name,
82
90
  member_action: action_name
83
91
  },
92
+ capability: capability,
84
93
  description: "Invoke +member_action+ #{action_name} on #{aa_res.resource_name} (same scoping as REST +show+)." do
85
94
  argument :id, ::GraphQL::Types::ID, required: true, camelize: false
86
95
  argument :params, [KeyValuePairInput], required: false, camelize: false,
87
96
  description: "Extra request params as flat key/value pairs."
97
+ if per_cfg&.dry_run
98
+ argument :dry_run, ::GraphQL::Types::Boolean, required: false, default_value: false, camelize: false,
99
+ description: "When true, preview without performing the member action."
100
+ end
88
101
  if (btc = aa_res.belongs_to_config)
89
102
  argument btc.to_param.to_sym, ::GraphQL::Types::ID, required: btc.required?, camelize: false
90
103
  end
@@ -16,18 +16,23 @@ module ActiveAdmin
16
16
  type_c = @object_types[model]
17
17
  next unless type_c
18
18
 
19
- actions = defined_actions_for(aa_res)
20
-
21
- if actions.include?(:create)
22
- mutations << mutation_create_field(sb, ns, aa_res, model, type_c)
23
- end
24
- if actions.include?(:update)
25
- mutations << mutation_update_field(sb, ns, aa_res, model, type_c)
26
- end
27
- if actions.include?(:destroy)
28
- mutations << mutation_destroy_field(sb, ns, aa_res, model)
19
+ cfg = aa_res.graphql_config
20
+ if cfg.mutations_enabled
21
+ actions = defined_actions_for(aa_res)
22
+
23
+ if actions.include?(:create)
24
+ mutations << mutation_create_field(sb, ns, aa_res, model, type_c)
25
+ end
26
+ if actions.include?(:update)
27
+ mutations << mutation_update_field(sb, ns, aa_res, model, type_c)
28
+ end
29
+ if actions.include?(:destroy)
30
+ mutations << mutation_destroy_field(sb, ns, aa_res, model)
31
+ end
29
32
  end
30
33
 
34
+ next unless cfg.run_actions_enabled
35
+
31
36
  if aa_res.batch_actions_enabled? && aa_res.batch_actions.any?
32
37
  mutations << mutation_batch_field(sb, ns, aa_res)
33
38
  end