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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -1
- data/app/controllers/active_admin/graphql_controller.rb +19 -4
- data/docs/changelogs/20261010091415_graphql-resource-enabled-default.md +34 -0
- data/docs/changelogs/20261012050051_graphql-surface-toggles.md +40 -0
- data/docs/changelogs/20261012064500_graphql-capabilities.md +35 -0
- data/docs/changelogs/20261012113500_release-0-3-3.md +45 -0
- 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 +173 -10
- data/docs/issues/20261012200000_host-customization-gaps.md +26 -0
- data/lib/active_admin/graphql/capability.rb +35 -0
- data/lib/active_admin/graphql/capability_definition_dsl.rb +69 -0
- data/lib/active_admin/graphql/collection_query_policy.rb +103 -0
- data/lib/active_admin/graphql/integration.rb +43 -1
- 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/policy_adapter.rb +94 -0
- data/lib/active_admin/graphql/policy_set_cache.rb +10 -6
- data/lib/active_admin/graphql/resource_capability_dsl.rb +32 -0
- data/lib/active_admin/graphql/resource_capability_maps.rb +77 -0
- data/lib/active_admin/graphql/resource_config.rb +19 -2
- data/lib/active_admin/graphql/resource_definition_dsl.rb +22 -10
- data/lib/active_admin/graphql/resource_methods.rb +7 -1
- data/lib/active_admin/graphql/resource_surface_dsl.rb +48 -0
- 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/build.rb +52 -3
- data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +7 -0
- data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +9 -0
- data/lib/active_admin/graphql/schema_builder/mutation_create.rb +1 -0
- data/lib/active_admin/graphql/schema_builder/mutation_member.rb +13 -0
- data/lib/active_admin/graphql/schema_builder/mutation_type_builder.rb +15 -10
- data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +2 -0
- data/lib/active_admin/graphql/schema_builder/query_type.rb +2 -1
- data/lib/active_admin/graphql/schema_builder/query_type_collection.rb +3 -0
- data/lib/active_admin/graphql/schema_builder/query_type_member.rb +1 -0
- data/lib/active_admin/graphql/schema_builder/query_type_policies.rb +9 -63
- data/lib/active_admin/graphql/schema_builder/resolvers.rb +46 -7
- data/lib/active_admin/graphql/schema_builder/resources.rb +34 -0
- data/lib/active_admin/graphql/schema_builder/types_object.rb +19 -8
- data/lib/active_admin/graphql/schema_builder/visibility.rb +22 -7
- data/lib/active_admin/graphql/schema_builder/wire.rb +4 -1
- data/lib/active_admin/graphql/schema_field.rb +97 -6
- data/lib/active_admin/graphql/version.rb +1 -1
- data/lib/active_admin/graphql.rb +9 -0
- 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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|