activeadmin-graphql 0.3.2 → 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 +10 -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/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 +124 -8
- 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 +10 -6
- 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 +19 -2
- data/lib/active_admin/graphql/resource_definition_dsl.rb +5 -4
- 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/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 +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/resources.rb +34 -0
- data/lib/active_admin/graphql/schema_builder/types_object.rb +18 -8
- 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 +2 -0
- metadata +11 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "capability_definition_dsl"
|
|
4
|
+
|
|
5
|
+
module ActiveAdmin
|
|
6
|
+
module GraphQL
|
|
7
|
+
# +capability+ entry on {ResourceDefinitionDSL} (RFC 0008).
|
|
8
|
+
module ResourceCapabilityDSL
|
|
9
|
+
def capability(
|
|
10
|
+
name,
|
|
11
|
+
fields: nil,
|
|
12
|
+
queries: nil,
|
|
13
|
+
mutations: nil,
|
|
14
|
+
member_actions: nil,
|
|
15
|
+
batch_actions: nil,
|
|
16
|
+
collection_actions: nil,
|
|
17
|
+
&block
|
|
18
|
+
)
|
|
19
|
+
body = CapabilityDefinitionDSL.new(@config, name)
|
|
20
|
+
body.fields(*fields) if fields
|
|
21
|
+
body.queries(*queries) if queries
|
|
22
|
+
body.mutations(*mutations) if mutations
|
|
23
|
+
body.member_actions(*member_actions) if member_actions
|
|
24
|
+
body.batch_actions(*batch_actions) if batch_actions
|
|
25
|
+
body.collection_actions(*collection_actions) if collection_actions
|
|
26
|
+
body.instance_exec(&block) if block
|
|
27
|
+
name.to_sym
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Capability → member maps on {ResourceConfig} (RFC 0008).
|
|
6
|
+
module ResourceCapabilityMaps
|
|
7
|
+
def field_capabilities
|
|
8
|
+
@field_capabilities ||= {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def query_capabilities
|
|
12
|
+
@query_capabilities ||= {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def mutation_capabilities
|
|
16
|
+
@mutation_capabilities ||= {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def capability_for_field(name)
|
|
20
|
+
field_capabilities[name.to_sym]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def capability_for_query(kind)
|
|
24
|
+
query_capabilities[kind.to_sym]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def capability_for_mutation(kind)
|
|
28
|
+
mutation_capabilities[kind.to_sym]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def assign_field_capabilities!(capability_name, names)
|
|
32
|
+
assign_capabilities!(field_capabilities, capability_name, names, label: "field")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def assign_query_capabilities!(capability_name, names)
|
|
36
|
+
assign_capabilities!(query_capabilities, capability_name, names, label: "query")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def assign_mutation_capabilities!(capability_name, names)
|
|
40
|
+
assign_capabilities!(mutation_capabilities, capability_name, names, label: "mutation")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def assign_capabilities!(map, capability_name, names, label:)
|
|
46
|
+
capability = capability_name.to_sym
|
|
47
|
+
names.flatten.each do |name|
|
|
48
|
+
key = name.to_sym
|
|
49
|
+
existing = map[key]
|
|
50
|
+
if existing && existing != capability
|
|
51
|
+
raise ArgumentError,
|
|
52
|
+
"GraphQL #{label} #{key.inspect} already requires capability #{existing.inspect}"
|
|
53
|
+
end
|
|
54
|
+
map[key] = capability
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "run_action_mutation_config"
|
|
4
4
|
require_relative "mutation_input_config"
|
|
5
|
+
require_relative "resource_capability_maps"
|
|
5
6
|
|
|
6
7
|
module ActiveAdmin
|
|
7
8
|
module GraphQL
|
|
8
9
|
# Per-resource GraphQL options set via +graphql+ in +ActiveAdmin.register+.
|
|
9
10
|
class ResourceConfig
|
|
11
|
+
include ResourceCapabilityMaps
|
|
12
|
+
|
|
10
13
|
attr_accessor :enabled
|
|
14
|
+
attr_accessor :queries_enabled
|
|
15
|
+
attr_accessor :mutations_enabled
|
|
16
|
+
attr_accessor :run_actions_enabled
|
|
17
|
+
attr_accessor :policies_enabled
|
|
11
18
|
attr_accessor :graphql_type_name
|
|
12
19
|
attr_accessor :only_attributes
|
|
13
20
|
attr_accessor :exclude_attributes
|
|
@@ -31,8 +38,18 @@ module ActiveAdmin
|
|
|
31
38
|
# {RunActionMutationConfig#payload_type} is not set. Falls back to {RunActionPayload}.
|
|
32
39
|
attr_accessor :run_action_payload_type
|
|
33
40
|
|
|
34
|
-
def initialize
|
|
35
|
-
|
|
41
|
+
def initialize(
|
|
42
|
+
enabled: true,
|
|
43
|
+
queries_enabled: true,
|
|
44
|
+
mutations_enabled: true,
|
|
45
|
+
run_actions_enabled: true,
|
|
46
|
+
policies_enabled: true
|
|
47
|
+
)
|
|
48
|
+
@enabled = enabled
|
|
49
|
+
@queries_enabled = queries_enabled
|
|
50
|
+
@mutations_enabled = mutations_enabled
|
|
51
|
+
@run_actions_enabled = run_actions_enabled
|
|
52
|
+
@policies_enabled = policies_enabled
|
|
36
53
|
@exclude_attributes = []
|
|
37
54
|
end
|
|
38
55
|
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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)
|