activeadmin-graphql 0.3.1 → 0.3.3
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 +15 -0
- data/app/controllers/active_admin/graphql_controller.rb +19 -4
- data/docs/changelogs/20261010091415_graphql-resource-enabled-default.md +34 -0
- data/docs/changelogs/20261010101500_nonconventional-id-fields.md +16 -2
- data/docs/changelogs/20261010103800_release-0-3-1.md +9 -2
- data/docs/changelogs/20261010110600_graphql-identity-dsl.md +34 -0
- data/docs/changelogs/20261010111800_release-0-3-2.md +46 -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/graphql-api.md +166 -20
- data/docs/issues/20261010111159_rails-composite-primary-keys-alignment.md +81 -0
- data/docs/issues/20261010111300_partitions-and-shards-support.md +55 -0
- data/docs/issues/20261010111500_graphql-federation-support.md +46 -0
- data/lib/active_admin/graphql/capability.rb +35 -0
- data/lib/active_admin/graphql/capability_definition_dsl.rb +64 -0
- data/lib/active_admin/graphql/integration.rb +19 -1
- data/lib/active_admin/graphql/policy_adapter.rb +94 -0
- data/lib/active_admin/graphql/policy_set_cache.rb +18 -9
- data/lib/active_admin/graphql/resource_capability_dsl.rb +31 -0
- data/lib/active_admin/graphql/resource_capability_maps.rb +59 -0
- data/lib/active_admin/graphql/resource_config.rb +23 -2
- data/lib/active_admin/graphql/resource_definition_dsl.rb +12 -2
- data/lib/active_admin/graphql/resource_identity.rb +98 -0
- data/lib/active_admin/graphql/resource_methods.rb +8 -2
- data/lib/active_admin/graphql/resource_query_proxy/identity.rb +4 -4
- data/lib/active_admin/graphql/resource_query_proxy.rb +5 -4
- data/lib/active_admin/graphql/resource_surface_dsl.rb +48 -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 +8 -0
- data/lib/active_admin/graphql/schema_builder/mutation_create.rb +1 -0
- data/lib/active_admin/graphql/schema_builder/mutation_member.rb +8 -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 +4 -2
- 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 +5 -2
- data/lib/active_admin/graphql/schema_builder/query_type_policies.rb +9 -63
- data/lib/active_admin/graphql/schema_builder/resources.rb +34 -0
- data/lib/active_admin/graphql/schema_builder/types_object.rb +27 -11
- data/lib/active_admin/graphql/schema_builder/visibility.rb +22 -7
- data/lib/active_admin/graphql/schema_builder/wire.rb +3 -1
- data/lib/active_admin/graphql/schema_field.rb +18 -2
- data/lib/active_admin/graphql/version.rb +1 -1
- data/lib/active_admin/graphql.rb +3 -0
- metadata +17 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module ActiveAdmin
|
|
6
|
+
module GraphQL
|
|
7
|
+
# Resolves GraphQL identity columns and id encode/decode for a registered resource.
|
|
8
|
+
# Prefer `graphql { identity ... }` when ActiveRecord has no primary key / id column.
|
|
9
|
+
module ResourceIdentity
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def configured?(aa_res)
|
|
13
|
+
aa_res.graphql_config.identity_columns.present?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def columns(aa_res)
|
|
17
|
+
configured = aa_res.graphql_config.identity_columns
|
|
18
|
+
return configured if configured.present?
|
|
19
|
+
|
|
20
|
+
ActiveAdmin::PrimaryKey.columns(aa_res.resource_class)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def graphql_id_value(aa_res, record)
|
|
24
|
+
cfg = aa_res.graphql_config
|
|
25
|
+
if (encode = cfg.identity_encode_proc)
|
|
26
|
+
return encode.call(record).to_s
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
cols = columns(aa_res)
|
|
30
|
+
return ActiveAdmin::PrimaryKey.graphql_id_value(record) unless configured?(aa_res)
|
|
31
|
+
|
|
32
|
+
encode_columns(record, cols, cfg.identity_separator)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def attributes_for_id(aa_res, id)
|
|
36
|
+
cfg = aa_res.graphql_config
|
|
37
|
+
if (decode = cfg.identity_decode_proc)
|
|
38
|
+
return stringify_attrs(decode.call(id))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
cols = columns(aa_res)
|
|
42
|
+
return ActiveAdmin::PrimaryKey.find_attributes(aa_res.resource_class, id) unless configured?(aa_res)
|
|
43
|
+
|
|
44
|
+
decode_columns(cols, id, cfg.identity_separator)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def encode_columns(record, cols, separator)
|
|
48
|
+
if cols.size == 1
|
|
49
|
+
record.public_send(cols.first).to_s
|
|
50
|
+
elsif separator
|
|
51
|
+
cols.map { |c| record.public_send(c) }.join(separator)
|
|
52
|
+
else
|
|
53
|
+
payload = cols.each_with_object({}) { |c, memo| memo[c] = record.read_attribute(c) }
|
|
54
|
+
JSON.generate(payload)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def decode_columns(cols, id, separator)
|
|
59
|
+
if cols.size == 1
|
|
60
|
+
{cols.first => id.to_s}
|
|
61
|
+
elsif separator
|
|
62
|
+
parts = id.to_s.split(separator, cols.size)
|
|
63
|
+
raise ArgumentError, "identity id segment count mismatch" if parts.size != cols.size
|
|
64
|
+
|
|
65
|
+
cols.zip(parts).to_h
|
|
66
|
+
else
|
|
67
|
+
parsed = JSON.parse(id.to_s)
|
|
68
|
+
raise ArgumentError, "composite id must be a JSON object" unless parsed.is_a?(Hash)
|
|
69
|
+
|
|
70
|
+
parsed.stringify_keys.slice(*cols)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def field_kw_to_param_hash(aa_res, id:, **kw)
|
|
75
|
+
return ActiveAdmin::PrimaryKey.field_kw_to_param_hash(aa_res.resource_class, id: id, **kw) unless configured?(aa_res)
|
|
76
|
+
|
|
77
|
+
raise ArgumentError, "id is required" if id.blank?
|
|
78
|
+
|
|
79
|
+
{"id" => id.to_s}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def member_param_hash(aa_res, blob)
|
|
83
|
+
return ActiveAdmin::PrimaryKey.member_param_hash(aa_res.resource_class, blob) unless configured?(aa_res)
|
|
84
|
+
|
|
85
|
+
blob = blob.stringify_keys
|
|
86
|
+
raise ArgumentError, "id is required" if blob["id"].blank?
|
|
87
|
+
|
|
88
|
+
{"id" => blob["id"].to_s}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def stringify_attrs(hash)
|
|
92
|
+
raise ArgumentError, "identity decode must return a Hash" unless hash.is_a?(Hash)
|
|
93
|
+
|
|
94
|
+
hash.stringify_keys.transform_values { |value| value&.to_s }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -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
|
|
@@ -22,7 +28,7 @@ module ActiveAdmin
|
|
|
22
28
|
|
|
23
29
|
def graphql_assignable_attribute_names
|
|
24
30
|
names = attributes_for_graphql.map(&:to_s)
|
|
25
|
-
pk_cols = ActiveAdmin::
|
|
31
|
+
pk_cols = ActiveAdmin::GraphQL::ResourceIdentity.columns(self)
|
|
26
32
|
return names if pk_cols.size > 1
|
|
27
33
|
|
|
28
34
|
names - pk_cols
|
|
@@ -6,11 +6,11 @@ module ActiveAdmin
|
|
|
6
6
|
module Identity
|
|
7
7
|
private
|
|
8
8
|
|
|
9
|
-
def
|
|
10
|
-
|
|
11
|
-
raise ActiveRecord::RecordNotFound
|
|
9
|
+
def find_identity_member(controller, model, id)
|
|
10
|
+
attrs = ActiveAdmin::GraphQL::ResourceIdentity.attributes_for_id(@aa_resource, id)
|
|
11
|
+
raise ActiveRecord::RecordNotFound if attrs.blank?
|
|
12
12
|
|
|
13
|
-
records = controller.send(:scoped_collection).where(
|
|
13
|
+
records = controller.send(:scoped_collection).where(attrs).limit(2).to_a
|
|
14
14
|
raise_ambiguous_id!(model) if records.size > 1
|
|
15
15
|
|
|
16
16
|
records.first || raise(ActiveRecord::RecordNotFound)
|
|
@@ -31,8 +31,8 @@ module ActiveAdmin
|
|
|
31
31
|
finder = controller.send(:method_for_find)
|
|
32
32
|
return controller.send(:find_resource) if model.primary_key.present? || finder != :find
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
rescue ActiveRecord::RecordNotFound
|
|
34
|
+
find_identity_member(controller, model, extra.fetch("id"))
|
|
35
|
+
rescue ActiveRecord::RecordNotFound, ArgumentError
|
|
36
36
|
nil
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -41,13 +41,14 @@ module ActiveAdmin
|
|
|
41
41
|
return {} if string_ids.empty?
|
|
42
42
|
|
|
43
43
|
model = @aa_resource.resource_class
|
|
44
|
-
|
|
44
|
+
identity_cols = ActiveAdmin::GraphQL::ResourceIdentity.columns(@aa_resource)
|
|
45
|
+
if ActiveAdmin::PrimaryKey.composite?(model) || identity_cols.size > 1
|
|
45
46
|
return string_ids.index_with { |identifier| find_member(identifier) }
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
controller = controller_for("index")
|
|
49
50
|
relation = controller.send(:apply_authorization_scope, controller.send(:scoped_collection))
|
|
50
|
-
primary_key =
|
|
51
|
+
primary_key = identity_cols.first&.to_sym
|
|
51
52
|
return string_ids.index_with { nil } if primary_key.nil?
|
|
52
53
|
|
|
53
54
|
indexed = index_records_by_id(relation.where(primary_key => string_ids), model, primary_key)
|
|
@@ -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
|
|
@@ -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: aa_res.graphql_config.capability_for_mutation(:batch_action),
|
|
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
|
+
aa_res.graphql_config.capability_for_mutation(:"batch_#{ba.tr("-", "_")}")
|
|
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: aa_res.graphql_config.capability_for_mutation(:collection_action),
|
|
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
|
+
aa_res.graphql_config.capability_for_mutation(:"collection_#{act.tr("-", "_")}")
|
|
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,
|
|
@@ -79,6 +86,7 @@ module ActiveAdmin
|
|
|
79
86
|
field_name: field_name,
|
|
80
87
|
collection_action: action_name
|
|
81
88
|
},
|
|
89
|
+
capability: aa_res.graphql_config.capability_for_mutation(:"collection_#{action_name.tr("-", "_")}"),
|
|
82
90
|
description: "Invoke +collection_action+ #{action_name} on #{aa_res.resource_name} (same scoping as REST +index+)." do
|
|
83
91
|
argument :params, [KeyValuePairInput], required: false, camelize: false,
|
|
84
92
|
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: aa_res.graphql_config.capability_for_mutation(:member_action),
|
|
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
|
+
aa_res.graphql_config.capability_for_mutation(:"member_#{act.tr("-", "_")}")
|
|
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,
|
|
@@ -81,6 +88,7 @@ module ActiveAdmin
|
|
|
81
88
|
field_name: field_name,
|
|
82
89
|
member_action: action_name
|
|
83
90
|
},
|
|
91
|
+
capability: aa_res.graphql_config.capability_for_mutation(:"member_#{action_name.tr("-", "_")}"),
|
|
84
92
|
description: "Invoke +member_action+ #{action_name} on #{aa_res.resource_name} (same scoping as REST +show+)." do
|
|
85
93
|
argument :id, ::GraphQL::Types::ID, required: true, camelize: false
|
|
86
94
|
argument :params, [KeyValuePairInput], required: false, camelize: false,
|
|
@@ -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
|
|
@@ -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
|
|
@@ -24,7 +25,7 @@ module ActiveAdmin
|
|
|
24
25
|
blob = where.to_h.stringify_keys
|
|
25
26
|
graph = sb.graph_params_from_find_blob(aa_res, blob)
|
|
26
27
|
begin
|
|
27
|
-
rid = ActiveAdmin::
|
|
28
|
+
rid = ActiveAdmin::GraphQL::ResourceIdentity.member_param_hash(aa_res, blob)
|
|
28
29
|
rescue ArgumentError => e
|
|
29
30
|
raise ::GraphQL::ExecutionError, e.message
|
|
30
31
|
end
|
|
@@ -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
|
|
@@ -90,7 +92,7 @@ module ActiveAdmin
|
|
|
90
92
|
blob = where.to_h.stringify_keys
|
|
91
93
|
graph = sb.graph_params_from_find_blob(aa_res, blob)
|
|
92
94
|
begin
|
|
93
|
-
rid = ActiveAdmin::
|
|
95
|
+
rid = ActiveAdmin::GraphQL::ResourceIdentity.member_param_hash(aa_res, blob)
|
|
94
96
|
rescue ArgumentError => e
|
|
95
97
|
raise ::GraphQL::ExecutionError, e.message
|
|
96
98
|
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)
|
|
@@ -51,10 +52,12 @@ module ActiveAdmin
|
|
|
51
52
|
if where
|
|
52
53
|
blob = where.to_h.stringify_keys
|
|
53
54
|
graph = builder.graph_params_from_find_blob(aa_res, blob)
|
|
54
|
-
rid = ActiveAdmin::
|
|
55
|
+
rid = ActiveAdmin::GraphQL::ResourceIdentity.member_param_hash(aa_res, blob)
|
|
55
56
|
else
|
|
56
57
|
graph = builder.graph_params_for_mutation(aa_res, kw)
|
|
57
|
-
rid = ActiveAdmin::
|
|
58
|
+
rid = ActiveAdmin::GraphQL::ResourceIdentity.field_kw_to_param_hash(
|
|
59
|
+
aa_res, id: id, **kw
|
|
60
|
+
)
|
|
58
61
|
end
|
|
59
62
|
rescue ArgumentError => e
|
|
60
63
|
raise ::GraphQL::ExecutionError, e.message
|
|
@@ -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
|
|
@@ -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)
|