activeadmin-graphql 0.3.3 → 0.4.1

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -3
  3. data/README.md +2 -2
  4. data/docs/changelogs/20260818111600_rfc-0002-problems.md +1 -1
  5. data/docs/changelogs/20260818114000_rfc-as-built-coverage.md +1 -1
  6. data/docs/changelogs/20260818114700_rfc-design-not-version.md +1 -1
  7. data/docs/changelogs/20260907141000_advisory-lockfile-refresh.md +1 -1
  8. data/docs/changelogs/20260921103000_mutation-attribute-allowlists.md +1 -1
  9. data/docs/changelogs/20260921111000_release-0-3-0.md +1 -1
  10. data/docs/changelogs/20260921112800_drop-standard-rubocop-direct.md +1 -1
  11. data/docs/changelogs/20260921120000_graphql-floor-and-activeadmin-4.md +1 -1
  12. data/docs/changelogs/20261010091415_graphql-resource-enabled-default.md +1 -1
  13. data/docs/changelogs/20261010103800_release-0-3-1.md +1 -1
  14. data/docs/changelogs/20261010110600_graphql-identity-dsl.md +4 -0
  15. data/docs/changelogs/20261010111800_release-0-3-2.md +1 -1
  16. data/docs/changelogs/20261012050051_graphql-surface-toggles.md +1 -1
  17. data/docs/changelogs/20261012064500_graphql-capabilities.md +1 -1
  18. data/docs/changelogs/20261012113500_release-0-3-3.md +1 -1
  19. data/docs/changelogs/20261012200000_host-customization-hooks.md +31 -0
  20. data/docs/changelogs/20261013080000_release-0-4-0.md +48 -0
  21. data/docs/changelogs/20261103145000_release-0-4-1.md +26 -0
  22. data/docs/graphql-api.md +154 -102
  23. data/docs/issues/20260907141000_engineering-and-dependency-audit.md +1 -1
  24. data/docs/issues/20260917123253_ci-drop-automatic-dependency-audit.md +1 -1
  25. data/docs/issues/20260921103000_mutation-attribute-allowlists.md +1 -1
  26. data/docs/issues/20260921104400_convention-alignment-audit.md +7 -7
  27. data/docs/issues/20260921114500_dependency-audit.md +13 -13
  28. data/docs/issues/20260921115500_file-length-metrics.md +1 -1
  29. data/docs/issues/20261010111159_rails-composite-primary-keys-alignment.md +13 -13
  30. data/docs/issues/20261010111300_partitions-and-shards-support.md +3 -3
  31. data/docs/issues/20261010111500_graphql-federation-support.md +3 -3
  32. data/docs/issues/20261012200000_host-customization-gaps.md +30 -0
  33. data/docs/issues/20261103145000_engineering-prose-release-audit.md +39 -0
  34. data/docs/plan/20260817201500_rfc-starter.md +1 -1
  35. data/docs/reviews/20261028_engineering-audit-release-preparation.md +24 -0
  36. data/docs/reviews/20261103145000_engineering-prose-release-audit.md +38 -0
  37. data/lib/active_admin/graphql/capability_definition_dsl.rb +9 -4
  38. data/lib/active_admin/graphql/collection_query_policy.rb +103 -0
  39. data/lib/active_admin/graphql/integration.rb +24 -0
  40. data/lib/active_admin/graphql/member_action_capability.rb +14 -0
  41. data/lib/active_admin/graphql/member_action_dry_run.rb +121 -0
  42. data/lib/active_admin/graphql/resource_capability_dsl.rb +4 -3
  43. data/lib/active_admin/graphql/resource_capability_maps.rb +20 -2
  44. data/lib/active_admin/graphql/resource_definition_dsl.rb +17 -6
  45. data/lib/active_admin/graphql/run_action_capability.rb +62 -0
  46. data/lib/active_admin/graphql/run_action_control_extension.rb +72 -0
  47. data/lib/active_admin/graphql/run_action_field.rb +103 -0
  48. data/lib/active_admin/graphql/run_action_mutation_config.rb +8 -0
  49. data/lib/active_admin/graphql/run_action_mutation_dsl.rb +18 -0
  50. data/lib/active_admin/graphql/run_action_preview.rb +20 -0
  51. data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +2 -2
  52. data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +4 -3
  53. data/lib/active_admin/graphql/schema_builder/mutation_create.rb +20 -20
  54. data/lib/active_admin/graphql/schema_builder/mutation_member.rb +8 -3
  55. data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +2 -4
  56. data/lib/active_admin/graphql/schema_builder/query_type_member.rb +1 -4
  57. data/lib/active_admin/graphql/schema_builder/query_type_registered.rb +1 -4
  58. data/lib/active_admin/graphql/schema_builder/resolvers.rb +46 -7
  59. data/lib/active_admin/graphql/schema_builder/types_object.rb +2 -1
  60. data/lib/active_admin/graphql/schema_builder/wire.rb +2 -1
  61. data/lib/active_admin/graphql/schema_field.rb +85 -10
  62. data/lib/active_admin/graphql/version.rb +1 -1
  63. data/lib/active_admin/graphql.rb +7 -0
  64. metadata +15 -1
@@ -2,12 +2,16 @@
2
2
 
3
3
  module ActiveAdmin
4
4
  module GraphQL
5
- # Capability → member maps on {ResourceConfig} (RFC 0008).
5
+ # Capability → member maps on {ResourceConfig} (RFC 0008, RFC 0009).
6
6
  module ResourceCapabilityMaps
7
7
  def field_capabilities
8
8
  @field_capabilities ||= {}
9
9
  end
10
10
 
11
+ def field_capability_modes
12
+ @field_capability_modes ||= {}
13
+ end
14
+
11
15
  def query_capabilities
12
16
  @query_capabilities ||= {}
13
17
  end
@@ -20,6 +24,10 @@ module ActiveAdmin
20
24
  field_capabilities[name.to_sym]
21
25
  end
22
26
 
27
+ def capability_mode_for_field(name)
28
+ field_capability_modes[name.to_sym]
29
+ end
30
+
23
31
  def capability_for_query(kind)
24
32
  query_capabilities[kind.to_sym]
25
33
  end
@@ -28,8 +36,18 @@ module ActiveAdmin
28
36
  mutation_capabilities[kind.to_sym]
29
37
  end
30
38
 
31
- def assign_field_capabilities!(capability_name, names)
39
+ def assign_field_capabilities!(capability_name, names, mode: nil)
32
40
  assign_capabilities!(field_capabilities, capability_name, names, label: "field")
41
+ return if mode.nil?
42
+
43
+ resolved = mode.to_sym
44
+ unless %i[omit null].include?(resolved)
45
+ raise ArgumentError, "capability mode must be :omit or :null, got #{mode.inspect}"
46
+ end
47
+
48
+ names.flatten.each do |name|
49
+ field_capability_modes[name.to_sym] = resolved
50
+ end
33
51
  end
34
52
 
35
53
  def assign_query_capabilities!(capability_name, names)
@@ -103,21 +103,32 @@ module ActiveAdmin
103
103
  end
104
104
 
105
105
  # Pair +type+ and +resolve+ for +posts_batch_action+ (graphql-ruby-style block).
106
- def batch_action_mutation(&block)
107
- 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
108
112
  end
109
113
 
110
114
  # With no name: configures the aggregate +posts_member_action(action: …)+ field.
111
115
  # With a symbol/string: configures +posts_member_<action>+ (one field per +member_action+), so each
112
116
  # action can use its own +type+, +resolve+, +arguments+, and GraphQL inputs.
113
- 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)
114
119
  cfg = name.nil? ? @config.member_run_action : @config.member_action_mutation_for(name)
115
- 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
116
124
  end
117
125
 
118
- def collection_action_mutation(name = nil, &block)
126
+ def collection_action_mutation(name = nil, dry_run: false, capability: nil, &block)
119
127
  cfg = name.nil? ? @config.collection_run_action : @config.collection_action_mutation_for(name)
120
- 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
121
132
  end
122
133
 
123
134
  # Default return type for all run-action fields unless a kind-specific +type+ is set inside
@@ -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
@@ -15,7 +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: aa_res.graphql_config.capability_for_mutation(:batch_action),
18
+ capability: RunActionCapability.resolve(ns, aa_res, :batch),
19
19
  description: "Run a batch action registered on #{aa_res.resource_name} (+batch_action+ DSL), " \
20
20
  "same as the index batch UI (+collection_selection+ / +batch_action_inputs+)." do
21
21
  argument :batch_action, ::GraphQL::Types::String, required: true, camelize: false
@@ -42,7 +42,7 @@ module ActiveAdmin
42
42
  end
43
43
  unless Capability.granted?(
44
44
  context,
45
- aa_res.graphql_config.capability_for_mutation(:"batch_#{ba.tr("-", "_")}")
45
+ RunActionCapability.resolve(ns, aa_res, :batch, ba)
46
46
  )
47
47
  raise ::GraphQL::ExecutionError, "not authorized for batch action #{ba}"
48
48
  end
@@ -21,7 +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: aa_res.graphql_config.capability_for_mutation(:collection_action),
24
+ capability: RunActionCapability.resolve(ns, aa_res, :collection),
25
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
26
26
  argument :action, ::GraphQL::Types::String, required: true, camelize: false
27
27
  argument :params, [KeyValuePairInput], required: false, camelize: false,
@@ -46,7 +46,7 @@ module ActiveAdmin
46
46
  end
47
47
  unless Capability.granted?(
48
48
  context,
49
- aa_res.graphql_config.capability_for_mutation(:"collection_#{act.tr("-", "_")}")
49
+ RunActionCapability.resolve(ns, aa_res, :collection, act)
50
50
  )
51
51
  raise ::GraphQL::ExecutionError, "not authorized for collection action #{act}"
52
52
  end
@@ -76,6 +76,7 @@ module ActiveAdmin
76
76
  fname = field_name.to_sym
77
77
  per_cfg = aa_res.graphql_config.collection_action_mutations[action_name]
78
78
  collection_payload_t = collection_action_return_type(aa_res, action_name)
79
+ capability = RunActionCapability.resolve(ns, aa_res, :collection, action_name)
79
80
 
80
81
  proc do
81
82
  field fname, collection_payload_t, null: false, camelize: false,
@@ -86,7 +87,7 @@ module ActiveAdmin
86
87
  field_name: field_name,
87
88
  collection_action: action_name
88
89
  },
89
- capability: aa_res.graphql_config.capability_for_mutation(:"collection_#{action_name.tr("-", "_")}"),
90
+ capability: capability,
90
91
  description: "Invoke +collection_action+ #{action_name} on #{aa_res.resource_name} (same scoping as REST +index+)." do
91
92
  argument :params, [KeyValuePairInput], required: false, camelize: false,
92
93
  description: "Extra request params as flat key/value pairs."
@@ -35,32 +35,32 @@ module ActiveAdmin
35
35
  input,
36
36
  attribute_names: aa_res.graphql_create_attribute_names
37
37
  )
38
- record = if (hook = aa_res.graphql_config.resolve_create_proc)
39
- hook.call(
40
- proxy: proxy,
41
- input: input,
42
- attributes: attrs,
43
- context: context,
44
- auth: auth,
45
- aa_resource: aa_res
46
- )
38
+ if (hook = aa_res.graphql_config.resolve_create_proc)
39
+ ActiveRecord::Base.transaction(requires_new: true) do
40
+ record = hook.call(
41
+ proxy: proxy,
42
+ input: input,
43
+ attributes: attrs,
44
+ context: context,
45
+ auth: auth,
46
+ aa_resource: aa_res
47
+ )
48
+ unless auth.authorized?(aa_res, ActiveAdmin::Authorization::CREATE, record)
49
+ raise ::GraphQL::ExecutionError, "not authorized to create this record"
50
+ end
51
+ record
52
+ end
47
53
  else
48
- r = proxy.build_new(attrs)
49
- unless auth.authorized?(aa_res, ActiveAdmin::Authorization::CREATE, r)
54
+ record = proxy.build_new(attrs)
55
+ unless auth.authorized?(aa_res, ActiveAdmin::Authorization::CREATE, record)
50
56
  raise ::GraphQL::ExecutionError, "not authorized to create this record"
51
57
  end
52
58
 
53
- unless r.save
54
- raise MutationExecutionError.validation(r)
59
+ unless record.save
60
+ raise MutationExecutionError.validation(record)
55
61
  end
56
- r
57
- end
58
-
59
- unless auth.authorized?(aa_res, ActiveAdmin::Authorization::CREATE, record)
60
- raise ::GraphQL::ExecutionError, "not authorized to create this record"
62
+ record
61
63
  end
62
-
63
- record
64
64
  end
65
65
  end
66
66
  end
@@ -21,7 +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: aa_res.graphql_config.capability_for_mutation(:member_action),
24
+ capability: MemberActionCapability.resolve(ns, aa_res),
25
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
26
26
  argument :action, ::GraphQL::Types::String, required: true, camelize: false
27
27
  argument :id, ::GraphQL::Types::ID, required: true, camelize: false
@@ -47,7 +47,7 @@ module ActiveAdmin
47
47
  end
48
48
  unless Capability.granted?(
49
49
  context,
50
- aa_res.graphql_config.capability_for_mutation(:"member_#{act.tr("-", "_")}")
50
+ MemberActionCapability.resolve(ns, aa_res, act)
51
51
  )
52
52
  raise ::GraphQL::ExecutionError, "not authorized for member action #{act}"
53
53
  end
@@ -78,6 +78,7 @@ module ActiveAdmin
78
78
  fname = field_name.to_sym
79
79
  per_cfg = aa_res.graphql_config.member_action_mutations[action_name]
80
80
  member_payload_t = member_action_return_type(aa_res, action_name)
81
+ capability = MemberActionCapability.resolve(ns, aa_res, action_name)
81
82
 
82
83
  proc do
83
84
  field fname, member_payload_t, null: false, camelize: false,
@@ -88,11 +89,15 @@ module ActiveAdmin
88
89
  field_name: field_name,
89
90
  member_action: action_name
90
91
  },
91
- capability: aa_res.graphql_config.capability_for_mutation(:"member_#{action_name.tr("-", "_")}"),
92
+ capability: capability,
92
93
  description: "Invoke +member_action+ #{action_name} on #{aa_res.resource_name} (same scoping as REST +show+)." do
93
94
  argument :id, ::GraphQL::Types::ID, required: true, camelize: false
94
95
  argument :params, [KeyValuePairInput], required: false, camelize: false,
95
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
96
101
  if (btc = aa_res.belongs_to_config)
97
102
  argument btc.to_param.to_sym, ::GraphQL::Types::ID, required: btc.required?, camelize: false
98
103
  end
@@ -38,9 +38,8 @@ module ActiveAdmin
38
38
  )
39
39
  record = proxy.find_member(rid)
40
40
  raise ::GraphQL::ExecutionError, "not found" unless record
41
-
42
41
  unless auth.authorized?(aa_res, ActiveAdmin::Authorization::UPDATE, record)
43
- raise ::GraphQL::ExecutionError, "not authorized to update this record"
42
+ raise ::GraphQL::ExecutionError, "not found"
44
43
  end
45
44
 
46
45
  attrs = sb.assignable_slice_from_input(
@@ -105,9 +104,8 @@ module ActiveAdmin
105
104
  )
106
105
  record = proxy.find_member(rid)
107
106
  raise ::GraphQL::ExecutionError, "not found" unless record
108
-
109
107
  unless auth.authorized?(aa_res, ActiveAdmin::Authorization::DESTROY, record)
110
- raise ::GraphQL::ExecutionError, "not authorized to destroy this record"
108
+ raise ::GraphQL::ExecutionError, "not found"
111
109
  end
112
110
 
113
111
  if (hook = aa_res.graphql_config.resolve_destroy_proc)
@@ -79,10 +79,7 @@ module ActiveAdmin
79
79
  id_argument: id
80
80
  )
81
81
  return nil unless record
82
-
83
- unless auth.authorized?(aa_res, ActiveAdmin::Authorization::READ, record)
84
- raise ::GraphQL::ExecutionError, "not authorized to read this record"
85
- end
82
+ return nil unless auth.authorized?(aa_res, ActiveAdmin::Authorization::READ, record)
86
83
 
87
84
  record
88
85
  end
@@ -42,10 +42,7 @@ module ActiveAdmin
42
42
  graph_params: graph_params
43
43
  )
44
44
  return nil unless record
45
-
46
- unless auth.authorized?(aa_res, ActiveAdmin::Authorization::READ, record)
47
- raise ::GraphQL::ExecutionError, "not authorized to read this record"
48
- end
45
+ return nil unless auth.authorized?(aa_res, ActiveAdmin::Authorization::READ, record)
49
46
 
50
47
  record
51
48
  end