activeadmin-graphql 0.3.3 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -1
- data/docs/changelogs/20261012200000_host-customization-hooks.md +27 -0
- data/docs/changelogs/20261013080000_release-0-4-0.md +48 -0
- data/docs/graphql-api.md +53 -6
- data/docs/issues/20261012200000_host-customization-gaps.md +26 -0
- data/lib/active_admin/graphql/capability_definition_dsl.rb +9 -4
- data/lib/active_admin/graphql/collection_query_policy.rb +103 -0
- data/lib/active_admin/graphql/integration.rb +24 -0
- data/lib/active_admin/graphql/member_action_capability.rb +14 -0
- data/lib/active_admin/graphql/member_action_dry_run.rb +121 -0
- data/lib/active_admin/graphql/resource_capability_dsl.rb +4 -3
- data/lib/active_admin/graphql/resource_capability_maps.rb +20 -2
- data/lib/active_admin/graphql/resource_definition_dsl.rb +17 -6
- data/lib/active_admin/graphql/run_action_capability.rb +62 -0
- data/lib/active_admin/graphql/run_action_control_extension.rb +72 -0
- data/lib/active_admin/graphql/run_action_field.rb +103 -0
- data/lib/active_admin/graphql/run_action_mutation_config.rb +8 -0
- data/lib/active_admin/graphql/run_action_mutation_dsl.rb +18 -0
- data/lib/active_admin/graphql/run_action_preview.rb +20 -0
- data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +2 -2
- data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +4 -3
- data/lib/active_admin/graphql/schema_builder/mutation_member.rb +8 -3
- data/lib/active_admin/graphql/schema_builder/resolvers.rb +46 -7
- data/lib/active_admin/graphql/schema_builder/types_object.rb +2 -1
- data/lib/active_admin/graphql/schema_builder/wire.rb +2 -1
- data/lib/active_admin/graphql/schema_field.rb +85 -10
- data/lib/active_admin/graphql/version.rb +1 -1
- data/lib/active_admin/graphql.rb +7 -0
- metadata +11 -1
|
@@ -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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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."
|
|
@@ -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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
@@ -4,22 +4,29 @@ module ActiveAdmin
|
|
|
4
4
|
module GraphQL
|
|
5
5
|
class SchemaBuilder
|
|
6
6
|
module Resolvers
|
|
7
|
-
def mutation_extra_keyword_params(aa_res, kw)
|
|
7
|
+
def mutation_extra_keyword_params(aa_res, kw, allowlist: nil)
|
|
8
8
|
skip =
|
|
9
9
|
if (btc = aa_res.belongs_to_config)
|
|
10
10
|
[btc.to_param.to_s]
|
|
11
11
|
else
|
|
12
12
|
[]
|
|
13
13
|
end
|
|
14
|
+
skip << "id"
|
|
15
|
+
skip << "dry_run"
|
|
16
|
+
allow = allowlist&.map(&:to_s)
|
|
17
|
+
|
|
14
18
|
kw.each_with_object({}) do |(key, val), h|
|
|
15
19
|
ks = key.to_s
|
|
16
20
|
next if skip.include?(ks)
|
|
21
|
+
next if allow && !allow.include?(ks)
|
|
17
22
|
|
|
18
23
|
h[ks] = val
|
|
19
24
|
end
|
|
20
25
|
end
|
|
21
26
|
|
|
22
27
|
def graphql_resolve_index(aa_res, proxy:, context:, graph_params:, **field_kwargs)
|
|
28
|
+
invoke_collection_query_policy!(aa_res, context, field_kwargs)
|
|
29
|
+
|
|
23
30
|
proc_ = aa_res.graphql_config.resolve_index_proc
|
|
24
31
|
base_kwargs = {graph_params: graph_params}.merge(field_kwargs)
|
|
25
32
|
if proc_
|
|
@@ -57,9 +64,26 @@ module ActiveAdmin
|
|
|
57
64
|
end
|
|
58
65
|
|
|
59
66
|
def graphql_resolve_member_action(aa_res, proxy:, context:, action:, id:, params: nil, **kw)
|
|
60
|
-
extras = mutation_extra_keyword_params(aa_res, kw)
|
|
61
|
-
params_h = coerce_action_param_map(params).merge(extras.transform_keys(&:to_s))
|
|
62
67
|
per = aa_res.graphql_config.member_action_mutations[action.to_s]
|
|
68
|
+
dry_run = !!kw[:dry_run]
|
|
69
|
+
extras = mutation_extra_keyword_params(aa_res, kw, allowlist: per&.extra_param_keys)
|
|
70
|
+
params_h = coerce_action_param_map(params).merge(extras.transform_keys(&:to_s))
|
|
71
|
+
params_h.delete("id")
|
|
72
|
+
|
|
73
|
+
if per&.dry_run
|
|
74
|
+
return MemberActionDryRun.resolve(
|
|
75
|
+
aa_res,
|
|
76
|
+
proxy: proxy,
|
|
77
|
+
context: context,
|
|
78
|
+
action: action,
|
|
79
|
+
id: id,
|
|
80
|
+
params_h: params_h,
|
|
81
|
+
extras: extras.transform_keys(&:to_sym),
|
|
82
|
+
per: per,
|
|
83
|
+
dry_run: dry_run
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
63
87
|
resolve = per&.resolve_proc || aa_res.graphql_config.member_run_action.resolve_proc
|
|
64
88
|
if resolve
|
|
65
89
|
resolve.call(
|
|
@@ -70,7 +94,7 @@ module ActiveAdmin
|
|
|
70
94
|
action: action,
|
|
71
95
|
id: id,
|
|
72
96
|
params: params_h,
|
|
73
|
-
**extras
|
|
97
|
+
**extras.transform_keys(&:to_sym)
|
|
74
98
|
)
|
|
75
99
|
else
|
|
76
100
|
proxy.run_member_action(action, id, extra_params: params_h)
|
|
@@ -78,9 +102,10 @@ module ActiveAdmin
|
|
|
78
102
|
end
|
|
79
103
|
|
|
80
104
|
def graphql_resolve_collection_action(aa_res, proxy:, context:, action:, params: nil, **kw)
|
|
81
|
-
extras = mutation_extra_keyword_params(aa_res, kw)
|
|
82
|
-
params_h = coerce_action_param_map(params).merge(extras.transform_keys(&:to_s))
|
|
83
105
|
per = aa_res.graphql_config.collection_action_mutations[action.to_s]
|
|
106
|
+
extras = mutation_extra_keyword_params(aa_res, kw, allowlist: per&.extra_param_keys)
|
|
107
|
+
params_h = coerce_action_param_map(params).merge(extras.transform_keys(&:to_s))
|
|
108
|
+
params_h.delete("id")
|
|
84
109
|
resolve = per&.resolve_proc || aa_res.graphql_config.collection_run_action.resolve_proc
|
|
85
110
|
if resolve
|
|
86
111
|
resolve.call(
|
|
@@ -90,7 +115,7 @@ module ActiveAdmin
|
|
|
90
115
|
auth: context[:auth],
|
|
91
116
|
action: action,
|
|
92
117
|
params: params_h,
|
|
93
|
-
**extras
|
|
118
|
+
**extras.transform_keys(&:to_sym)
|
|
94
119
|
)
|
|
95
120
|
else
|
|
96
121
|
proxy.run_collection_action(action, extra_params: params_h)
|
|
@@ -105,6 +130,20 @@ module ActiveAdmin
|
|
|
105
130
|
h.stringify_keys
|
|
106
131
|
end
|
|
107
132
|
|
|
133
|
+
def invoke_collection_query_policy!(aa_res, context, field_kwargs)
|
|
134
|
+
hook = context[:namespace]&.graphql_collection_query_policy
|
|
135
|
+
return if hook.nil?
|
|
136
|
+
|
|
137
|
+
meta = field_kwargs.merge(
|
|
138
|
+
aa_resource: aa_res,
|
|
139
|
+
q: field_kwargs[:q],
|
|
140
|
+
scope: field_kwargs[:scope],
|
|
141
|
+
order: field_kwargs[:order],
|
|
142
|
+
filter: field_kwargs[:filter]
|
|
143
|
+
)
|
|
144
|
+
hook.call(context, meta)
|
|
145
|
+
end
|
|
146
|
+
|
|
108
147
|
public :graphql_resolve_index,
|
|
109
148
|
:graphql_resolve_show,
|
|
110
149
|
:graphql_resolve_batch_action,
|
|
@@ -119,7 +119,8 @@ module ActiveAdmin
|
|
|
119
119
|
null: true,
|
|
120
120
|
camelize: false,
|
|
121
121
|
authorize: false,
|
|
122
|
-
capability: aa_res.graphql_config.capability_for_field(name)
|
|
122
|
+
capability: aa_res.graphql_config.capability_for_field(name),
|
|
123
|
+
capability_mode: aa_res.graphql_config.capability_mode_for_field(name)
|
|
123
124
|
)
|
|
124
125
|
|
|
125
126
|
type_class.define_method(name.to_sym) { object.public_send(name) }
|
|
@@ -22,7 +22,8 @@ module ActiveAdmin
|
|
|
22
22
|
|
|
23
23
|
type_class.field field_name, @object_types[target], null: true, camelize: false,
|
|
24
24
|
authorize: false,
|
|
25
|
-
capability: aa_res.graphql_config.capability_for_field(field_name)
|
|
25
|
+
capability: aa_res.graphql_config.capability_for_field(field_name),
|
|
26
|
+
capability_mode: aa_res.graphql_config.capability_mode_for_field(field_name)
|
|
26
27
|
|
|
27
28
|
type_class.define_method(field_name) do
|
|
28
29
|
fk_val = object.public_send(fk)
|
|
@@ -2,17 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
module ActiveAdmin
|
|
4
4
|
module GraphQL
|
|
5
|
-
# Subclass of {::GraphQL::Schema::Field} for namespace +Query+ / +Mutation+ fields
|
|
5
|
+
# Subclass of {::GraphQL::Schema::Field} for namespace +Query+ / +Mutation+ fields
|
|
6
|
+
# and resource object fields.
|
|
6
7
|
#
|
|
7
8
|
# The +visibility:+ keyword accepts an optional Hash of metadata forwarded to
|
|
8
9
|
# +graphql_schema_visible+ (second argument) when {GraphQL::Schema::Visibility} is enabled.
|
|
9
|
-
# That hook runs from {#visible?}; it does not replace graphql-ruby's visibility system—it
|
|
10
|
-
# composes with +super+ like any custom +Field+ class.
|
|
11
10
|
#
|
|
12
|
-
# +capability:+ (RFC 0008)
|
|
11
|
+
# +capability:+ (RFC 0008) and +capability_mode:+ (RFC 0009) gate +visible?+ /
|
|
12
|
+
# +authorized?+ via +context[:capabilities]+. +graphql_field_visible+ may omit fields.
|
|
13
|
+
# +dry_run:+ / +confirm:+ / +preview:+ (RFC 0013) control custom run-action fields.
|
|
13
14
|
class SchemaField < ::GraphQL::Schema::Field
|
|
14
|
-
|
|
15
|
+
CAPABILITY_MODES = %i[omit null].freeze
|
|
16
|
+
|
|
17
|
+
def initialize(
|
|
18
|
+
visibility: nil,
|
|
19
|
+
authorize: nil,
|
|
20
|
+
authorize_action: nil,
|
|
21
|
+
capability: nil,
|
|
22
|
+
capability_mode: nil,
|
|
23
|
+
dry_run: false,
|
|
24
|
+
confirm: false,
|
|
25
|
+
preview: nil,
|
|
26
|
+
extensions: ::GraphQL::EmptyObjects::EMPTY_ARRAY,
|
|
27
|
+
**kwargs,
|
|
28
|
+
&block
|
|
29
|
+
)
|
|
15
30
|
@capability = capability&.to_sym
|
|
31
|
+
@capability_mode = normalize_capability_mode(capability_mode)
|
|
32
|
+
@inject_dry_run = dry_run
|
|
33
|
+
@inject_confirm = confirm
|
|
34
|
+
@preview_proc = preview
|
|
16
35
|
@visibility =
|
|
17
36
|
if @capability && visibility.is_a?(Hash) && !visibility.key?(:capability)
|
|
18
37
|
visibility.merge(capability: @capability)
|
|
@@ -21,16 +40,30 @@ module ActiveAdmin
|
|
|
21
40
|
end
|
|
22
41
|
@authorize = authorize
|
|
23
42
|
@authorize_action = authorize_action
|
|
24
|
-
|
|
43
|
+
field_extensions = Array(extensions)
|
|
44
|
+
if @inject_confirm || @preview_proc
|
|
45
|
+
field_extensions += [{
|
|
46
|
+
RunActionControlExtension => {confirm: @inject_confirm, preview: @preview_proc}
|
|
47
|
+
}]
|
|
48
|
+
end
|
|
49
|
+
if @inject_dry_run || @inject_confirm || @preview_proc || block
|
|
50
|
+
super(extensions: field_extensions, **kwargs) do
|
|
51
|
+
inject_run_action_control_arguments!
|
|
52
|
+
instance_exec(&block) if block
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
super(extensions: field_extensions, **kwargs)
|
|
56
|
+
end
|
|
25
57
|
end
|
|
26
58
|
|
|
27
59
|
def visible?(ctx)
|
|
28
60
|
return false unless super
|
|
61
|
+
return false unless field_visible_by_hook?(ctx)
|
|
29
62
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
63
|
+
if ctx[:namespace]&.graphql_visibility &&
|
|
64
|
+
@capability &&
|
|
65
|
+
!Capability.granted?(ctx, @capability) &&
|
|
66
|
+
capability_mode_for(ctx) == :omit
|
|
34
67
|
return false
|
|
35
68
|
end
|
|
36
69
|
|
|
@@ -60,6 +93,48 @@ module ActiveAdmin
|
|
|
60
93
|
action = @authorize_action || ActiveAdmin::Authorization::READ
|
|
61
94
|
!!ctx[:auth]&.authorized?(aa_resource, action, object)
|
|
62
95
|
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def inject_run_action_control_arguments!
|
|
100
|
+
if @inject_dry_run
|
|
101
|
+
argument :dry_run, ::GraphQL::Types::Boolean, required: false, default_value: false, camelize: false,
|
|
102
|
+
description: "When true, preview without performing the side-effecting action."
|
|
103
|
+
end
|
|
104
|
+
if @inject_confirm
|
|
105
|
+
argument :confirm, ::GraphQL::Types::Boolean, required: false, default_value: false, camelize: false,
|
|
106
|
+
description: "When false and dry_run is false, refuse with body confirm_required."
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def normalize_capability_mode(mode)
|
|
111
|
+
return nil if mode.nil?
|
|
112
|
+
|
|
113
|
+
sym = mode.to_sym
|
|
114
|
+
unless CAPABILITY_MODES.include?(sym)
|
|
115
|
+
raise ArgumentError, "capability_mode must be :omit or :null, got #{mode.inspect}"
|
|
116
|
+
end
|
|
117
|
+
sym
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def capability_mode_for(ctx)
|
|
121
|
+
mode = @capability_mode || ctx[:namespace]&.graphql_field_capability_mode_default || :omit
|
|
122
|
+
mode.to_sym
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def field_visible_by_hook?(ctx)
|
|
126
|
+
hook = ctx[:namespace]&.graphql_field_visible
|
|
127
|
+
return true if hook.nil?
|
|
128
|
+
|
|
129
|
+
owner = self.owner
|
|
130
|
+
meta = {
|
|
131
|
+
graphql_type_name: owner.respond_to?(:graphql_name) ? owner.graphql_name : nil,
|
|
132
|
+
field_name: graphql_name,
|
|
133
|
+
resource: owner.respond_to?(:activeadmin_graphql_resource) ? owner.activeadmin_graphql_resource : nil
|
|
134
|
+
}
|
|
135
|
+
meta = meta.merge(@visibility) if @visibility.is_a?(Hash)
|
|
136
|
+
!!hook.call(ctx, meta)
|
|
137
|
+
end
|
|
63
138
|
end
|
|
64
139
|
end
|
|
65
140
|
end
|