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
|
@@ -14,6 +14,7 @@ module ActiveAdmin
|
|
|
14
14
|
proc do
|
|
15
15
|
field fname.to_sym, type_c, null: true, camelize: false,
|
|
16
16
|
visibility: {kind: :mutation_update, graphql_type_name: sb.send(:graphql_type_name_for, aa_res), resource: aa_res},
|
|
17
|
+
capability: aa_res.graphql_config.capability_for_mutation(:update),
|
|
17
18
|
description: "Update #{model.name}" do
|
|
18
19
|
argument :where, find_type, required: true, camelize: false
|
|
19
20
|
argument :input, update_input, required: true, camelize: false
|
|
@@ -80,6 +81,7 @@ module ActiveAdmin
|
|
|
80
81
|
[delete_name, destroy_name].each do |fname|
|
|
81
82
|
field fname.to_sym, ::GraphQL::Types::Boolean, null: false, camelize: false,
|
|
82
83
|
visibility: {kind: :mutation_delete, graphql_type_name: type_basename, resource: aa_res},
|
|
84
|
+
capability: aa_res.graphql_config.capability_for_mutation(:destroy),
|
|
83
85
|
description: "Destroy #{model.name}" do
|
|
84
86
|
argument :where, find_type, required: true, camelize: false
|
|
85
87
|
end
|
|
@@ -16,6 +16,7 @@ module ActiveAdmin
|
|
|
16
16
|
aa_by_model = @aa_by_model
|
|
17
17
|
object_types = @object_types
|
|
18
18
|
aa_by_graphql_type_name = @aa_by_graphql_type_name
|
|
19
|
+
query_type_name_map = aa_by_graphql_type_name_for_queries
|
|
19
20
|
list_filter_input_types = @list_filter_input_types
|
|
20
21
|
find_input_types = @find_input_types
|
|
21
22
|
|
|
@@ -31,7 +32,7 @@ module ActiveAdmin
|
|
|
31
32
|
builder: builder,
|
|
32
33
|
ns: ns,
|
|
33
34
|
registered_resource_union: registered_resource_union,
|
|
34
|
-
aa_by_graphql_type_name:
|
|
35
|
+
aa_by_graphql_type_name: query_type_name_map
|
|
35
36
|
)
|
|
36
37
|
end
|
|
37
38
|
|
|
@@ -7,6 +7,8 @@ module ActiveAdmin
|
|
|
7
7
|
def add_model_query_fields!(query_class, builder:, ns:, aa_by_model:, object_types:,
|
|
8
8
|
list_filter_input_types:, find_input_types:)
|
|
9
9
|
aa_by_model.each do |model, aa_res|
|
|
10
|
+
next unless aa_res.graphql_config.queries_enabled
|
|
11
|
+
|
|
10
12
|
builder.define_query_collection_field!(
|
|
11
13
|
query_class, model, aa_res, builder, ns, object_types, list_filter_input_types
|
|
12
14
|
)
|
|
@@ -32,6 +34,7 @@ module ActiveAdmin
|
|
|
32
34
|
field_name: plural_route,
|
|
33
35
|
resource: aa_res
|
|
34
36
|
},
|
|
37
|
+
capability: aa_res.graphql_config.capability_for_query(:collection),
|
|
35
38
|
description: "Paginated list of #{model.name} (#{aa_res.resource_name.human}); " \
|
|
36
39
|
"pass +filter+ or legacy +q+ / +scope+ / +order+ (Ransack +q+ matches the JSON index param). " \
|
|
37
40
|
"Nested resources require the parent foreign key (same as REST route params)." do
|
|
@@ -19,6 +19,7 @@ module ActiveAdmin
|
|
|
19
19
|
field_name: singular,
|
|
20
20
|
resource: aa_res
|
|
21
21
|
},
|
|
22
|
+
capability: aa_res.graphql_config.capability_for_query(:member),
|
|
22
23
|
description: "Find #{model.name} by id (same scoping chain as REST +show+). Use +where+ or legacy +id+." do
|
|
23
24
|
argument :where, find_type, required: false, camelize: false
|
|
24
25
|
if ActiveAdmin::PrimaryKey.composite?(model)
|
|
@@ -4,14 +4,10 @@ module ActiveAdmin
|
|
|
4
4
|
module GraphQL
|
|
5
5
|
class SchemaBuilder
|
|
6
6
|
module QueryTypePolicies
|
|
7
|
-
POLICY_ACTIONS = {
|
|
8
|
-
read: ActiveAdmin::Authorization::READ,
|
|
9
|
-
create: ActiveAdmin::Authorization::CREATE,
|
|
10
|
-
update: ActiveAdmin::Authorization::UPDATE,
|
|
11
|
-
destroy: ActiveAdmin::Authorization::DESTROY
|
|
12
|
-
}.freeze
|
|
13
|
-
|
|
14
7
|
def add_activeadmin_policies_query_field!(query_class, builder:, ns:, aa_by_graphql_type_name:)
|
|
8
|
+
adapter = builder.send(:policy_adapter)
|
|
9
|
+
return unless adapter
|
|
10
|
+
|
|
15
11
|
resource_policy_type = Class.new(::GraphQL::Schema::Object) do
|
|
16
12
|
graphql_name "ActiveAdminResourcePolicies"
|
|
17
13
|
field :type_name, ::GraphQL::Types::String, null: false, camelize: false
|
|
@@ -48,7 +44,9 @@ module ActiveAdmin
|
|
|
48
44
|
end
|
|
49
45
|
|
|
50
46
|
define_method(:activeadmin_policies) do
|
|
51
|
-
resources = builder.send(:active_resources).
|
|
47
|
+
resources = builder.send(:active_resources).filter_map do |aa_res|
|
|
48
|
+
next unless adapter.include_resource?(aa_res)
|
|
49
|
+
|
|
52
50
|
{
|
|
53
51
|
type_name: builder.send(:graphql_type_name_for, aa_res),
|
|
54
52
|
activeadmin_policies: ActiveAdmin::GraphQL::PolicySetCache.fetch(
|
|
@@ -74,6 +72,9 @@ module ActiveAdmin
|
|
|
74
72
|
define_method(:activeadmin_policies_for) do |type_name:, ids:, path: nil|
|
|
75
73
|
aa_res = aa_by_graphql_type_name[type_name.to_s]
|
|
76
74
|
raise ::GraphQL::ExecutionError, "unknown resource type_name #{type_name.inspect}" unless aa_res
|
|
75
|
+
unless adapter.include_resource?(aa_res)
|
|
76
|
+
raise ::GraphQL::ExecutionError, "policies disabled for #{type_name.inspect}"
|
|
77
|
+
end
|
|
77
78
|
|
|
78
79
|
proxy = ResourceQueryProxy.new(
|
|
79
80
|
aa_resource: aa_res,
|
|
@@ -99,61 +100,6 @@ module ActiveAdmin
|
|
|
99
100
|
end
|
|
100
101
|
end
|
|
101
102
|
end
|
|
102
|
-
|
|
103
|
-
private
|
|
104
|
-
|
|
105
|
-
def policy_action_pairs
|
|
106
|
-
hook = @namespace.graphql_policy_actions
|
|
107
|
-
if hook.respond_to?(:call)
|
|
108
|
-
raw = hook.call(@namespace)
|
|
109
|
-
return raw.to_h.transform_keys(&:to_sym) if raw.respond_to?(:to_h)
|
|
110
|
-
end
|
|
111
|
-
POLICY_ACTIONS
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def allowed_base_actions(auth:, subject_owner:, subject:, context:)
|
|
115
|
-
action_mapper = @namespace.graphql_policy_action_mapper
|
|
116
|
-
policy_action_pairs.each_with_object([]) do |(name, default_action), out|
|
|
117
|
-
allowed = if action_mapper.respond_to?(:call)
|
|
118
|
-
action_mapper.call(context, subject_owner, subject, name, default_action)
|
|
119
|
-
else
|
|
120
|
-
auth.authorized?(subject_owner, default_action, subject)
|
|
121
|
-
end
|
|
122
|
-
out << name.to_s if allowed
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def build_policy_set(auth:, subject_owner:, subject:, context:)
|
|
127
|
-
base = {
|
|
128
|
-
allowed_actions: allowed_base_actions(auth: auth, subject_owner: subject_owner, subject: subject, context: context),
|
|
129
|
-
allowed_member_actions: [],
|
|
130
|
-
allowed_collection_actions: [],
|
|
131
|
-
allowed_batch_actions: []
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if subject_owner.is_a?(ActiveAdmin::Resource)
|
|
135
|
-
class_subject = subject_owner.resource_class
|
|
136
|
-
if subject.is_a?(Class)
|
|
137
|
-
if auth.authorized?(subject_owner, ActiveAdmin::Authorization::READ, class_subject)
|
|
138
|
-
base[:allowed_member_actions] = subject_owner.member_actions.map { |a| a.name.to_s }
|
|
139
|
-
base[:allowed_collection_actions] = subject_owner.collection_actions.map { |a| a.name.to_s }
|
|
140
|
-
base[:allowed_batch_actions] = subject_owner.batch_actions.map { |a| a.sym.to_s }
|
|
141
|
-
end
|
|
142
|
-
elsif auth.authorized?(subject_owner, ActiveAdmin::Authorization::READ, subject)
|
|
143
|
-
base[:allowed_member_actions] = subject_owner.member_actions.map { |a| a.name.to_s }
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
if (extra = @namespace.graphql_policy_extra).respond_to?(:call)
|
|
148
|
-
out = extra.call(context, subject_owner, subject, base)
|
|
149
|
-
base = out if out.is_a?(Hash)
|
|
150
|
-
end
|
|
151
|
-
if (transform = @namespace.graphql_policy_transform).respond_to?(:call)
|
|
152
|
-
out = transform.call(context, subject_owner, subject, base)
|
|
153
|
-
base = out if out.is_a?(Hash)
|
|
154
|
-
end
|
|
155
|
-
base
|
|
156
|
-
end
|
|
157
103
|
end
|
|
158
104
|
end
|
|
159
105
|
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,
|
|
@@ -28,6 +28,40 @@ module ActiveAdmin
|
|
|
28
28
|
aa_res.attributes_for_graphql
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# +nil+ when policy fields are off (+graphql_policies_enabled+ false).
|
|
32
|
+
def policy_adapter
|
|
33
|
+
return @policy_adapter if defined?(@policy_adapter)
|
|
34
|
+
|
|
35
|
+
unless @namespace.graphql_policies_enabled
|
|
36
|
+
@policy_adapter = nil
|
|
37
|
+
return @policy_adapter
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
raw = @namespace.graphql_policy_adapter
|
|
41
|
+
klass = case raw
|
|
42
|
+
when nil, true, :default
|
|
43
|
+
PolicyAdapter
|
|
44
|
+
when String
|
|
45
|
+
raw.constantize
|
|
46
|
+
when Class
|
|
47
|
+
raw
|
|
48
|
+
else
|
|
49
|
+
raise ActiveAdmin::DependencyError,
|
|
50
|
+
"graphql_policy_adapter must be a Class, String, or nil (got #{raw.inspect})"
|
|
51
|
+
end
|
|
52
|
+
@policy_adapter = klass.new(namespace: @namespace)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def queryable_resources
|
|
56
|
+
active_resources.select { |r| r.graphql_config.queries_enabled }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def aa_by_graphql_type_name_for_queries
|
|
60
|
+
queryable_resources.each_with_object({}) do |aa_res, map|
|
|
61
|
+
map[graphql_type_name_for(aa_res)] = aa_res
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
31
65
|
def configure_schema_plugins(schema)
|
|
32
66
|
if (c = @namespace.graphql_max_complexity)
|
|
33
67
|
schema.max_complexity(c)
|
|
@@ -113,18 +113,29 @@ module ActiveAdmin
|
|
|
113
113
|
next unless col
|
|
114
114
|
|
|
115
115
|
gql_t = graphql_scalar_for_column(aa_res, model, col)
|
|
116
|
-
type_class.field(
|
|
116
|
+
type_class.field(
|
|
117
|
+
name.to_sym,
|
|
118
|
+
gql_t,
|
|
119
|
+
null: true,
|
|
120
|
+
camelize: false,
|
|
121
|
+
authorize: false,
|
|
122
|
+
capability: aa_res.graphql_config.capability_for_field(name),
|
|
123
|
+
capability_mode: aa_res.graphql_config.capability_mode_for_field(name)
|
|
124
|
+
)
|
|
117
125
|
|
|
118
126
|
type_class.define_method(name.to_sym) { object.public_send(name) }
|
|
119
127
|
end
|
|
120
128
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
adapter = policy_adapter
|
|
130
|
+
if adapter&.include_resource?(aa_res)
|
|
131
|
+
type_class.field :activeadmin_policies, policy_set_type, null: false, camelize: false, authorize: false
|
|
132
|
+
type_class.define_method(:activeadmin_policies) do
|
|
133
|
+
ActiveAdmin::GraphQL::PolicySetCache.fetch(
|
|
134
|
+
context,
|
|
135
|
+
subject_owner: aa_res,
|
|
136
|
+
subject: object
|
|
137
|
+
)
|
|
138
|
+
end
|
|
128
139
|
end
|
|
129
140
|
|
|
130
141
|
if (ext = aa_res.graphql_config.extension_block)
|
|
@@ -10,14 +10,29 @@ module ActiveAdmin
|
|
|
10
10
|
vis = @namespace.graphql_visibility
|
|
11
11
|
return if vis.nil?
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
options =
|
|
14
|
+
case vis
|
|
15
|
+
when true
|
|
16
|
+
{}
|
|
17
|
+
when Hash
|
|
18
|
+
vis.symbolize_keys
|
|
19
|
+
else
|
|
20
|
+
raise ActiveAdmin::DependencyError,
|
|
21
|
+
"namespace graphql_visibility must be nil, true, or a Hash of GraphQL::Schema::Visibility options"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if @namespace.graphql_capabilities.respond_to?(:call)
|
|
25
|
+
profiles = options[:profiles]
|
|
26
|
+
if profiles.respond_to?(:any?) && profiles.any?
|
|
27
|
+
raise ActiveAdmin::DependencyError,
|
|
28
|
+
"graphql_capabilities cannot be combined with named GraphQL visibility profiles " \
|
|
29
|
+
"(named profiles cache a fixed context and ignore per-request capabilities). " \
|
|
30
|
+
"Omit profiles: and use dynamic visibility, or drop graphql_capabilities."
|
|
31
|
+
end
|
|
32
|
+
options = options.merge(dynamic: true)
|
|
20
33
|
end
|
|
34
|
+
|
|
35
|
+
schema.use(::GraphQL::Schema::Visibility, **options)
|
|
21
36
|
end
|
|
22
37
|
|
|
23
38
|
def attach_registered_resource_union_visibility!(union_class)
|
|
@@ -20,7 +20,10 @@ module ActiveAdmin
|
|
|
20
20
|
field_name = ref.name.to_sym
|
|
21
21
|
fk = ref.foreign_key.to_s
|
|
22
22
|
|
|
23
|
-
type_class.field field_name, @object_types[target], null: true, camelize: false
|
|
23
|
+
type_class.field field_name, @object_types[target], null: true, camelize: false,
|
|
24
|
+
authorize: false,
|
|
25
|
+
capability: aa_res.graphql_config.capability_for_field(field_name),
|
|
26
|
+
capability_mode: aa_res.graphql_config.capability_mode_for_field(field_name)
|
|
24
27
|
|
|
25
28
|
type_class.define_method(field_name) do
|
|
26
29
|
fk_val = object.public_send(fk)
|
|
@@ -2,22 +2,70 @@
|
|
|
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
|
-
#
|
|
10
|
-
#
|
|
10
|
+
#
|
|
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.
|
|
11
14
|
class SchemaField < ::GraphQL::Schema::Field
|
|
12
|
-
|
|
13
|
-
|
|
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
|
+
)
|
|
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
|
|
35
|
+
@visibility =
|
|
36
|
+
if @capability && visibility.is_a?(Hash) && !visibility.key?(:capability)
|
|
37
|
+
visibility.merge(capability: @capability)
|
|
38
|
+
else
|
|
39
|
+
visibility
|
|
40
|
+
end
|
|
14
41
|
@authorize = authorize
|
|
15
42
|
@authorize_action = authorize_action
|
|
16
|
-
|
|
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
|
|
17
57
|
end
|
|
18
58
|
|
|
19
59
|
def visible?(ctx)
|
|
20
60
|
return false unless super
|
|
61
|
+
return false unless field_visible_by_hook?(ctx)
|
|
62
|
+
|
|
63
|
+
if ctx[:namespace]&.graphql_visibility &&
|
|
64
|
+
@capability &&
|
|
65
|
+
!Capability.granted?(ctx, @capability) &&
|
|
66
|
+
capability_mode_for(ctx) == :omit
|
|
67
|
+
return false
|
|
68
|
+
end
|
|
21
69
|
|
|
22
70
|
return true unless @visibility
|
|
23
71
|
|
|
@@ -29,6 +77,7 @@ module ActiveAdmin
|
|
|
29
77
|
|
|
30
78
|
def authorized?(object, args, ctx)
|
|
31
79
|
return false unless super
|
|
80
|
+
return false unless Capability.granted?(ctx, @capability)
|
|
32
81
|
|
|
33
82
|
owner = self.owner
|
|
34
83
|
aa_resource = owner.respond_to?(:activeadmin_graphql_resource) ? owner.activeadmin_graphql_resource : nil
|
|
@@ -44,6 +93,48 @@ module ActiveAdmin
|
|
|
44
93
|
action = @authorize_action || ActiveAdmin::Authorization::READ
|
|
45
94
|
!!ctx[:auth]&.authorized?(aa_resource, action, object)
|
|
46
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
|
|
47
138
|
end
|
|
48
139
|
end
|
|
49
140
|
end
|
data/lib/active_admin/graphql.rb
CHANGED
|
@@ -68,6 +68,9 @@ module ActiveAdmin
|
|
|
68
68
|
def require_active_admin_graphql_components!
|
|
69
69
|
%w[
|
|
70
70
|
graphql/resource_interface
|
|
71
|
+
graphql/capability
|
|
72
|
+
graphql/run_action_preview
|
|
73
|
+
graphql/run_action_control_extension
|
|
71
74
|
graphql/schema_field
|
|
72
75
|
graphql/auth_context
|
|
73
76
|
graphql/record_source
|
|
@@ -76,7 +79,13 @@ module ActiveAdmin
|
|
|
76
79
|
graphql/run_action_payload
|
|
77
80
|
graphql/run_action_mutation_config
|
|
78
81
|
graphql/run_action_mutation_dsl
|
|
82
|
+
graphql/run_action_field
|
|
83
|
+
graphql/member_action_dry_run
|
|
84
|
+
graphql/run_action_capability
|
|
85
|
+
graphql/member_action_capability
|
|
86
|
+
graphql/collection_query_policy
|
|
79
87
|
graphql/key_value_pair_input
|
|
88
|
+
graphql/policy_adapter
|
|
80
89
|
graphql/policy_set_cache
|
|
81
90
|
graphql/mutation_execution_error
|
|
82
91
|
graphql/schema_builder
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activeadmin-graphql
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -374,10 +374,16 @@ files:
|
|
|
374
374
|
- docs/changelogs/20260921111000_release-0-3-0.md
|
|
375
375
|
- docs/changelogs/20260921112800_drop-standard-rubocop-direct.md
|
|
376
376
|
- docs/changelogs/20260921120000_graphql-floor-and-activeadmin-4.md
|
|
377
|
+
- docs/changelogs/20261010091415_graphql-resource-enabled-default.md
|
|
377
378
|
- docs/changelogs/20261010101500_nonconventional-id-fields.md
|
|
378
379
|
- docs/changelogs/20261010103800_release-0-3-1.md
|
|
379
380
|
- docs/changelogs/20261010110600_graphql-identity-dsl.md
|
|
380
381
|
- docs/changelogs/20261010111800_release-0-3-2.md
|
|
382
|
+
- docs/changelogs/20261012050051_graphql-surface-toggles.md
|
|
383
|
+
- docs/changelogs/20261012064500_graphql-capabilities.md
|
|
384
|
+
- docs/changelogs/20261012113500_release-0-3-3.md
|
|
385
|
+
- docs/changelogs/20261012200000_host-customization-hooks.md
|
|
386
|
+
- docs/changelogs/20261013080000_release-0-4-0.md
|
|
381
387
|
- docs/dependencies/20260907141000_json-cve-2026-71847.md
|
|
382
388
|
- docs/dependencies/20260907141000_sqlite3-ghsa-mwm8-39rw-8826.md
|
|
383
389
|
- docs/dependencies/20260921114000_rails-json-parse-kwargs.md
|
|
@@ -393,18 +399,27 @@ files:
|
|
|
393
399
|
- docs/issues/20261010111159_rails-composite-primary-keys-alignment.md
|
|
394
400
|
- docs/issues/20261010111300_partitions-and-shards-support.md
|
|
395
401
|
- docs/issues/20261010111500_graphql-federation-support.md
|
|
402
|
+
- docs/issues/20261012200000_host-customization-gaps.md
|
|
396
403
|
- docs/plan/20260817201500_rfc-starter.md
|
|
397
404
|
- lib/active_admin/graphql.rb
|
|
398
405
|
- lib/active_admin/graphql/auth_context.rb
|
|
406
|
+
- lib/active_admin/graphql/capability.rb
|
|
407
|
+
- lib/active_admin/graphql/capability_definition_dsl.rb
|
|
408
|
+
- lib/active_admin/graphql/collection_query_policy.rb
|
|
399
409
|
- lib/active_admin/graphql/engine.rb
|
|
400
410
|
- lib/active_admin/graphql/integration.rb
|
|
401
411
|
- lib/active_admin/graphql/key_value_pair_input.rb
|
|
412
|
+
- lib/active_admin/graphql/member_action_capability.rb
|
|
413
|
+
- lib/active_admin/graphql/member_action_dry_run.rb
|
|
402
414
|
- lib/active_admin/graphql/mutation_execution_error.rb
|
|
403
415
|
- lib/active_admin/graphql/mutation_input_config.rb
|
|
404
416
|
- lib/active_admin/graphql/mutation_input_definition_dsl.rb
|
|
417
|
+
- lib/active_admin/graphql/policy_adapter.rb
|
|
405
418
|
- lib/active_admin/graphql/policy_set_cache.rb
|
|
406
419
|
- lib/active_admin/graphql/railtie.rb
|
|
407
420
|
- lib/active_admin/graphql/record_source.rb
|
|
421
|
+
- lib/active_admin/graphql/resource_capability_dsl.rb
|
|
422
|
+
- lib/active_admin/graphql/resource_capability_maps.rb
|
|
408
423
|
- lib/active_admin/graphql/resource_config.rb
|
|
409
424
|
- lib/active_admin/graphql/resource_definition_dsl.rb
|
|
410
425
|
- lib/active_admin/graphql/resource_identity.rb
|
|
@@ -413,9 +428,14 @@ files:
|
|
|
413
428
|
- lib/active_admin/graphql/resource_query_proxy.rb
|
|
414
429
|
- lib/active_admin/graphql/resource_query_proxy/controller.rb
|
|
415
430
|
- lib/active_admin/graphql/resource_query_proxy/identity.rb
|
|
431
|
+
- lib/active_admin/graphql/resource_surface_dsl.rb
|
|
432
|
+
- lib/active_admin/graphql/run_action_capability.rb
|
|
433
|
+
- lib/active_admin/graphql/run_action_control_extension.rb
|
|
434
|
+
- lib/active_admin/graphql/run_action_field.rb
|
|
416
435
|
- lib/active_admin/graphql/run_action_mutation_config.rb
|
|
417
436
|
- lib/active_admin/graphql/run_action_mutation_dsl.rb
|
|
418
437
|
- lib/active_admin/graphql/run_action_payload.rb
|
|
438
|
+
- lib/active_admin/graphql/run_action_preview.rb
|
|
419
439
|
- lib/active_admin/graphql/schema_builder.rb
|
|
420
440
|
- lib/active_admin/graphql/schema_builder/build.rb
|
|
421
441
|
- lib/active_admin/graphql/schema_builder/graph_params.rb
|