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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -1
  3. data/app/controllers/active_admin/graphql_controller.rb +19 -4
  4. data/docs/changelogs/20261010091415_graphql-resource-enabled-default.md +34 -0
  5. data/docs/changelogs/20261012050051_graphql-surface-toggles.md +40 -0
  6. data/docs/changelogs/20261012064500_graphql-capabilities.md +35 -0
  7. data/docs/changelogs/20261012113500_release-0-3-3.md +45 -0
  8. data/docs/changelogs/20261012200000_host-customization-hooks.md +27 -0
  9. data/docs/changelogs/20261013080000_release-0-4-0.md +48 -0
  10. data/docs/graphql-api.md +173 -10
  11. data/docs/issues/20261012200000_host-customization-gaps.md +26 -0
  12. data/lib/active_admin/graphql/capability.rb +35 -0
  13. data/lib/active_admin/graphql/capability_definition_dsl.rb +69 -0
  14. data/lib/active_admin/graphql/collection_query_policy.rb +103 -0
  15. data/lib/active_admin/graphql/integration.rb +43 -1
  16. data/lib/active_admin/graphql/member_action_capability.rb +14 -0
  17. data/lib/active_admin/graphql/member_action_dry_run.rb +121 -0
  18. data/lib/active_admin/graphql/policy_adapter.rb +94 -0
  19. data/lib/active_admin/graphql/policy_set_cache.rb +10 -6
  20. data/lib/active_admin/graphql/resource_capability_dsl.rb +32 -0
  21. data/lib/active_admin/graphql/resource_capability_maps.rb +77 -0
  22. data/lib/active_admin/graphql/resource_config.rb +19 -2
  23. data/lib/active_admin/graphql/resource_definition_dsl.rb +22 -10
  24. data/lib/active_admin/graphql/resource_methods.rb +7 -1
  25. data/lib/active_admin/graphql/resource_surface_dsl.rb +48 -0
  26. data/lib/active_admin/graphql/run_action_capability.rb +62 -0
  27. data/lib/active_admin/graphql/run_action_control_extension.rb +72 -0
  28. data/lib/active_admin/graphql/run_action_field.rb +103 -0
  29. data/lib/active_admin/graphql/run_action_mutation_config.rb +8 -0
  30. data/lib/active_admin/graphql/run_action_mutation_dsl.rb +18 -0
  31. data/lib/active_admin/graphql/run_action_preview.rb +20 -0
  32. data/lib/active_admin/graphql/schema_builder/build.rb +52 -3
  33. data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +7 -0
  34. data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +9 -0
  35. data/lib/active_admin/graphql/schema_builder/mutation_create.rb +1 -0
  36. data/lib/active_admin/graphql/schema_builder/mutation_member.rb +13 -0
  37. data/lib/active_admin/graphql/schema_builder/mutation_type_builder.rb +15 -10
  38. data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +2 -0
  39. data/lib/active_admin/graphql/schema_builder/query_type.rb +2 -1
  40. data/lib/active_admin/graphql/schema_builder/query_type_collection.rb +3 -0
  41. data/lib/active_admin/graphql/schema_builder/query_type_member.rb +1 -0
  42. data/lib/active_admin/graphql/schema_builder/query_type_policies.rb +9 -63
  43. data/lib/active_admin/graphql/schema_builder/resolvers.rb +46 -7
  44. data/lib/active_admin/graphql/schema_builder/resources.rb +34 -0
  45. data/lib/active_admin/graphql/schema_builder/types_object.rb +19 -8
  46. data/lib/active_admin/graphql/schema_builder/visibility.rb +22 -7
  47. data/lib/active_admin/graphql/schema_builder/wire.rb +4 -1
  48. data/lib/active_admin/graphql/schema_field.rb +97 -6
  49. data/lib/active_admin/graphql/version.rb +1 -1
  50. data/lib/active_admin/graphql.rb +9 -0
  51. metadata +21 -1
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Block body for +capability :name do ... end+ (RFC 0008, RFC 0009).
6
+ class CapabilityDefinitionDSL
7
+ QUERY_ALIASES = {
8
+ index: :collection,
9
+ list: :collection,
10
+ show: :member
11
+ }.freeze
12
+
13
+ MUTATION_ALIASES = {
14
+ delete: :destroy
15
+ }.freeze
16
+
17
+ def initialize(config, capability_name, mode: nil)
18
+ @config = config
19
+ @capability_name = capability_name.to_sym
20
+ @mode = mode
21
+ end
22
+
23
+ def fields(*names, mode: nil)
24
+ @config.assign_field_capabilities!(
25
+ @capability_name,
26
+ names,
27
+ mode: mode.nil? ? @mode : mode
28
+ )
29
+ end
30
+
31
+ def queries(*names)
32
+ keys = names.flatten.map { |name| normalize_query_key(name) }
33
+ @config.assign_query_capabilities!(@capability_name, keys)
34
+ end
35
+
36
+ def mutations(*names)
37
+ keys = names.flatten.map { |name| normalize_mutation_key(name) }
38
+ @config.assign_mutation_capabilities!(@capability_name, keys)
39
+ end
40
+
41
+ def member_actions(*names)
42
+ keys = names.flatten.map { |name| :"member_#{name.to_s.tr("-", "_")}" }
43
+ @config.assign_mutation_capabilities!(@capability_name, keys)
44
+ end
45
+
46
+ def batch_actions(*names)
47
+ keys = names.flatten.map { |name| :"batch_#{name.to_s.tr("-", "_")}" }
48
+ @config.assign_mutation_capabilities!(@capability_name, keys)
49
+ end
50
+
51
+ def collection_actions(*names)
52
+ keys = names.flatten.map { |name| :"collection_#{name.to_s.tr("-", "_")}" }
53
+ @config.assign_mutation_capabilities!(@capability_name, keys)
54
+ end
55
+
56
+ private
57
+
58
+ def normalize_query_key(name)
59
+ key = name.to_sym
60
+ QUERY_ALIASES.fetch(key, key)
61
+ end
62
+
63
+ def normalize_mutation_key(name)
64
+ key = name.to_sym
65
+ MUTATION_ALIASES.fetch(key, key)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Built-in helpers for +graphql_collection_query_policy+ (RFC 0011).
6
+ module CollectionQueryPolicy
7
+ module_function
8
+
9
+ def require_visible_fields!(
10
+ context:,
11
+ aa_resource:,
12
+ q: nil,
13
+ order: nil,
14
+ allow_q_keys: [],
15
+ field_grants: {},
16
+ infer_field_grants: true
17
+ )
18
+ allow = Array(allow_q_keys).map(&:to_s)
19
+ grants = merge_field_grants(aa_resource, field_grants, infer: infer_field_grants)
20
+
21
+ normalize_q(q).each_key do |raw_key|
22
+ key = raw_key.to_s
23
+ next if allow.include?(key)
24
+
25
+ field_name = field_from_predicate_key(key)
26
+ unless field_name
27
+ raise ::GraphQL::ExecutionError, "unsupported collection filter key #{key.inspect}"
28
+ end
29
+
30
+ assert_field_filterable!(context, aa_resource, field_name, grants)
31
+ end
32
+
33
+ assert_order_filterable!(context, aa_resource, order, grants) if order.present?
34
+ end
35
+
36
+ def merge_field_grants(aa_resource, field_grants, infer: true)
37
+ explicit = field_grants.to_h.transform_keys(&:to_s).transform_values(&:to_sym)
38
+ return explicit unless infer
39
+
40
+ inferred = aa_resource.graphql_config.field_capabilities.to_h do |name, capability|
41
+ [name.to_s, capability.to_sym]
42
+ end
43
+ inferred.merge(explicit)
44
+ end
45
+
46
+ def normalize_q(q)
47
+ return {} if q.nil?
48
+ return q.to_unsafe_h if q.respond_to?(:to_unsafe_h)
49
+
50
+ q.respond_to?(:to_h) ? q.to_h : {}
51
+ end
52
+
53
+ def field_from_predicate_key(key)
54
+ predicates = ransack_predicate_names
55
+ predicates.sort_by(&:length).reverse_each do |predicate|
56
+ suffix = "_#{predicate}"
57
+ return key[0...-suffix.length] if key.end_with?(suffix) && key.length > suffix.length
58
+ end
59
+ nil
60
+ end
61
+
62
+ def ransack_predicate_names
63
+ if defined?(Ransack) && Ransack.respond_to?(:predicates)
64
+ Ransack.predicates.keys.map(&:to_s)
65
+ else
66
+ %w[eq cont start end gt lt gteq lteq in not_in null present blank matches does_not_match]
67
+ end
68
+ end
69
+
70
+ def assert_order_filterable!(context, aa_resource, order, grants)
71
+ column = order.to_s.sub(/_(asc|desc|ascs|descs)\z/i, "")
72
+ return if column.blank?
73
+
74
+ assert_field_filterable!(context, aa_resource, column, grants)
75
+ end
76
+
77
+ def assert_field_filterable!(context, aa_resource, field_name, grants)
78
+ unless field_schema_visible?(context, aa_resource, field_name)
79
+ raise ::GraphQL::ExecutionError, "not authorized to filter on #{field_name}"
80
+ end
81
+
82
+ capability = grants[field_name.to_s]
83
+ return if capability.nil?
84
+ return if Capability.granted?(context, capability)
85
+
86
+ raise ::GraphQL::ExecutionError, "not authorized to filter on #{field_name}"
87
+ end
88
+
89
+ def field_schema_visible?(context, aa_resource, field_name)
90
+ type_name = graphql_type_name_for(aa_resource)
91
+ type = context.schema.get_type(type_name, context)
92
+ return false unless type
93
+
94
+ !context.schema.get_field(type, field_name.to_s, context).nil?
95
+ end
96
+
97
+ def graphql_type_name_for(aa_resource)
98
+ aa_resource.graphql_config.graphql_type_name.presence ||
99
+ aa_resource.resource_class.name.delete_prefix("::").gsub("::", "__")
100
+ end
101
+ end
102
+ end
103
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "policy_adapter"
3
4
  require_relative "resource_definition_dsl"
4
5
  require_relative "resource_methods"
5
6
 
@@ -25,13 +26,51 @@ module ActiveAdmin
25
26
 
26
27
  def register_namespace_settings!
27
28
  ns = ActiveAdmin::NamespaceSettings
29
+ register_surface_namespace_settings!(ns)
30
+ register_capability_namespace_settings!(ns)
31
+ register_limit_namespace_settings!(ns)
32
+ end
33
+
34
+ def register_surface_namespace_settings!(ns)
28
35
  ns.register :graphql, false
36
+ # true preserves 0.3.x: every ActiveRecord resource is in the schema once the namespace endpoint is on.
37
+ # Set false for opt-in per resource (empty graphql block or enable!).
38
+ ns.register :graphql_resource_enabled_default, true
39
+ ns.register :graphql_queries_enabled_default, true
40
+ ns.register :graphql_mutations_enabled_default, true
41
+ ns.register :graphql_run_actions_enabled_default, true
42
+ ns.register :graphql_policies_enabled, true
43
+ ns.register :graphql_policies_enabled_default, true
44
+ # Payload builder for policy fields when graphql_policies_enabled is true.
45
+ ns.register :graphql_policy_adapter, ActiveAdmin::GraphQL::PolicyAdapter
46
+ ns.register :graphql_registered_resource_enabled, true
47
+ ns.register :graphql_belongs_to_associations_enabled, true
29
48
  ns.register :graphql_path, "graphql"
30
49
  ns.register :graphql_multiplex_max, 20
31
50
  ns.register :graphql_dataloader, nil
32
51
  ns.register :graphql_visibility, nil
33
52
  ns.register :graphql_visibility_profile, nil
34
53
  ns.register :graphql_schema_visible, nil
54
+ end
55
+
56
+ def register_capability_namespace_settings!(ns)
57
+ # Optional Proc +(context_hash) -> Enumerable+ of capability names (RFC 0008).
58
+ ns.register :graphql_capabilities, nil
59
+ # RFC 0009: default field capability mode (:omit or :null).
60
+ ns.register :graphql_field_capability_mode_default, :omit
61
+ # RFC 0009: optional Proc +(context, meta) -> Boolean+ to omit object/root fields.
62
+ ns.register :graphql_field_visible, nil
63
+ # RFC 0010: optional Proc +(context, meta) -> void+ after run-action outcomes.
64
+ ns.register :graphql_run_action_audit, nil
65
+ # RFC 0011: optional Proc +(context, meta) -> void+ at collection resolve start.
66
+ ns.register :graphql_collection_query_policy, nil
67
+ # RFC 0012: optional default capabilities for run-action mutation fields.
68
+ ns.register :graphql_member_action_capability_default, nil
69
+ ns.register :graphql_batch_action_capability_default, nil
70
+ ns.register :graphql_collection_action_capability_default, nil
71
+ end
72
+
73
+ def register_limit_namespace_settings!(ns)
35
74
  ns.register :graphql_max_complexity, 300
36
75
  ns.register :graphql_max_depth, 18
37
76
  ns.register :graphql_batch_action_max_ids, 5000
@@ -64,7 +103,10 @@ module ActiveAdmin
64
103
  module ResourceDSLMethods
65
104
  def graphql(&block)
66
105
  if block
67
- ActiveAdmin::GraphQL::ResourceDefinitionDSL.new(config.graphql_config).instance_exec(&block)
106
+ cfg = config.graphql_config
107
+ # Presence of a graphql block is an intentional opt-in when the namespace default is false.
108
+ cfg.enabled = true
109
+ ActiveAdmin::GraphQL::ResourceDefinitionDSL.new(cfg).instance_exec(&block)
68
110
  end
69
111
  config.graphql_config
70
112
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Compatibility wrapper around {RunActionCapability} for member actions (RFC 0012).
6
+ module MemberActionCapability
7
+ module_function
8
+
9
+ def resolve(namespace, aa_res, action_name = nil)
10
+ RunActionCapability.resolve(namespace, aa_res, :member, action_name)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Preview context and resolve helpers for +member_action_mutation dry_run:+ (RFC 0010).
6
+ class MemberActionDryRun
7
+ PreviewContext = RunActionPreview
8
+
9
+ def self.resolve(
10
+ aa_res,
11
+ proxy:,
12
+ context:,
13
+ action:,
14
+ id:,
15
+ params_h:,
16
+ extras:,
17
+ per:,
18
+ dry_run:
19
+ )
20
+ new(aa_res, proxy:, context:, action:, id:, params_h:, extras:, per:, dry_run:).call
21
+ end
22
+
23
+ def initialize(aa_res, proxy:, context:, action:, id:, params_h:, extras:, per:, dry_run:)
24
+ @aa_res = aa_res
25
+ @proxy = proxy
26
+ @context = context
27
+ @action = action
28
+ @id = id
29
+ @params_h = params_h
30
+ @extras = extras
31
+ @per = per
32
+ @dry_run = dry_run
33
+ end
34
+
35
+ def call
36
+ record = @proxy.find_member(@id)
37
+ preview_result = run_preview(record)
38
+
39
+ if @dry_run
40
+ result = preview_result || default_dry_run_result(record)
41
+ audit!(result, dry_run: true)
42
+ return result
43
+ end
44
+
45
+ if preview_result && !preview_result.ok
46
+ audit!(preview_result, dry_run: false)
47
+ return preview_result
48
+ end
49
+
50
+ result = run_action
51
+ audit!(result, dry_run: false)
52
+ result
53
+ end
54
+
55
+ private
56
+
57
+ def run_preview(record)
58
+ return nil unless @per.preview_proc
59
+
60
+ PreviewContext.new.instance_exec(
61
+ record,
62
+ proxy: @proxy,
63
+ context: @context,
64
+ aa_resource: @aa_res,
65
+ auth: @context[:auth],
66
+ action: @action,
67
+ id: @id,
68
+ params: @params_h,
69
+ **@extras,
70
+ &@per.preview_proc
71
+ )
72
+ end
73
+
74
+ def default_dry_run_result(record)
75
+ body = if record
76
+ "would_run:#{@action}:#{record.id}"
77
+ else
78
+ "would_run:#{@action}"
79
+ end
80
+ RunActionPayload::Result.new(ok: true, status: 200, location: nil, body: body)
81
+ end
82
+
83
+ def run_action
84
+ resolve = @per.resolve_proc || @aa_res.graphql_config.member_run_action.resolve_proc
85
+ if resolve
86
+ resolve.call(
87
+ proxy: @proxy,
88
+ context: @context,
89
+ aa_resource: @aa_res,
90
+ auth: @context[:auth],
91
+ action: @action,
92
+ id: @id,
93
+ params: @params_h,
94
+ **@extras
95
+ )
96
+ else
97
+ @proxy.run_member_action(@action, @id, extra_params: @params_h)
98
+ end
99
+ end
100
+
101
+ def audit!(result, dry_run:)
102
+ hook = @context[:namespace]&.graphql_run_action_audit
103
+ return if hook.nil?
104
+
105
+ hook.call(
106
+ @context,
107
+ {
108
+ action: @action,
109
+ id: @id,
110
+ status: result.respond_to?(:status) ? result.status : nil,
111
+ dry_run: dry_run,
112
+ ok: result.respond_to?(:ok) ? result.ok : nil,
113
+ reason: result.respond_to?(:body) ? result.body : nil
114
+ }
115
+ )
116
+ rescue
117
+ nil
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Builds +activeadmin_policies+ payloads for a namespace.
6
+ #
7
+ # Set +graphql_policy_adapter+ to this class (default), another subclass, or a
8
+ # constant name +String+. Omit policy fields with +graphql_policies_enabled = false+.
9
+ class PolicyAdapter
10
+ POLICY_ACTIONS = {
11
+ read: ActiveAdmin::Authorization::READ,
12
+ create: ActiveAdmin::Authorization::CREATE,
13
+ update: ActiveAdmin::Authorization::UPDATE,
14
+ destroy: ActiveAdmin::Authorization::DESTROY
15
+ }.freeze
16
+
17
+ attr_reader :namespace
18
+
19
+ def initialize(namespace:)
20
+ @namespace = namespace
21
+ end
22
+
23
+ # Whether this resource gets an object +activeadmin_policies+ field and a
24
+ # row in the +activeadmin_policies+ resources matrix.
25
+ def include_resource?(aa_resource)
26
+ aa_resource.graphql_config.policies_enabled
27
+ end
28
+
29
+ def policy_set(auth:, subject_owner:, subject:, context:)
30
+ base = {
31
+ allowed_actions: allowed_base_actions(
32
+ auth: auth,
33
+ subject_owner: subject_owner,
34
+ subject: subject,
35
+ context: context
36
+ ),
37
+ allowed_member_actions: [],
38
+ allowed_collection_actions: [],
39
+ allowed_batch_actions: []
40
+ }
41
+
42
+ append_resource_action_names!(base, auth: auth, subject_owner: subject_owner, subject: subject)
43
+
44
+ if (extra = namespace.graphql_policy_extra).respond_to?(:call)
45
+ out = extra.call(context, subject_owner, subject, base)
46
+ base = out if out.is_a?(Hash)
47
+ end
48
+ if (transform = namespace.graphql_policy_transform).respond_to?(:call)
49
+ out = transform.call(context, subject_owner, subject, base)
50
+ base = out if out.is_a?(Hash)
51
+ end
52
+ base
53
+ end
54
+
55
+ private
56
+
57
+ def policy_action_pairs
58
+ hook = namespace.graphql_policy_actions
59
+ if hook.respond_to?(:call)
60
+ raw = hook.call(namespace)
61
+ return raw.to_h.transform_keys(&:to_sym) if raw.respond_to?(:to_h)
62
+ end
63
+ POLICY_ACTIONS
64
+ end
65
+
66
+ def allowed_base_actions(auth:, subject_owner:, subject:, context:)
67
+ action_mapper = namespace.graphql_policy_action_mapper
68
+ policy_action_pairs.each_with_object([]) do |(name, default_action), out|
69
+ allowed = if action_mapper.respond_to?(:call)
70
+ action_mapper.call(context, subject_owner, subject, name, default_action)
71
+ else
72
+ auth.authorized?(subject_owner, default_action, subject)
73
+ end
74
+ out << name.to_s if allowed
75
+ end
76
+ end
77
+
78
+ def append_resource_action_names!(base, auth:, subject_owner:, subject:)
79
+ return unless subject_owner.is_a?(ActiveAdmin::Resource)
80
+
81
+ class_subject = subject_owner.resource_class
82
+ if subject.is_a?(Class)
83
+ return unless auth.authorized?(subject_owner, ActiveAdmin::Authorization::READ, class_subject)
84
+
85
+ base[:allowed_member_actions] = subject_owner.member_actions.map { |a| a.name.to_s }
86
+ base[:allowed_collection_actions] = subject_owner.collection_actions.map { |a| a.name.to_s }
87
+ base[:allowed_batch_actions] = subject_owner.batch_actions.map { |a| a.sym.to_s }
88
+ elsif auth.authorized?(subject_owner, ActiveAdmin::Authorization::READ, subject)
89
+ base[:allowed_member_actions] = subject_owner.member_actions.map { |a| a.name.to_s }
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -6,10 +6,11 @@ module ActiveAdmin
6
6
  class PolicySetCache
7
7
  class << self
8
8
  def fetch(context, subject_owner:, subject:)
9
- namespace = context[:namespace]
10
- unless namespace
11
- return policy_builder(context).send(
12
- :build_policy_set,
9
+ adapter = policy_adapter(context)
10
+ raise ActiveAdmin::DependencyError, "graphql_policy_adapter is disabled" unless adapter
11
+
12
+ unless context[:namespace]
13
+ return adapter.policy_set(
13
14
  auth: context[:auth],
14
15
  subject_owner: subject_owner,
15
16
  subject: subject,
@@ -19,8 +20,7 @@ module ActiveAdmin
19
20
 
20
21
  cache = context[:policy_set_cache] ||= {}
21
22
  key = cache_key(subject_owner, subject)
22
- cache[key] ||= policy_builder(context).send(
23
- :build_policy_set,
23
+ cache[key] ||= adapter.policy_set(
24
24
  auth: context[:auth],
25
25
  subject_owner: subject_owner,
26
26
  subject: subject,
@@ -28,6 +28,10 @@ module ActiveAdmin
28
28
  )
29
29
  end
30
30
 
31
+ def policy_adapter(context)
32
+ policy_builder(context).send(:policy_adapter)
33
+ end
34
+
31
35
  def policy_builder(context)
32
36
  context[:policy_schema_builder] ||= SchemaBuilder.new(context[:namespace])
33
37
  end
@@ -0,0 +1,32 @@
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, RFC 0009).
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
+ mode: nil,
18
+ &block
19
+ )
20
+ body = CapabilityDefinitionDSL.new(@config, name, mode: mode)
21
+ body.fields(*fields, mode: mode) if fields
22
+ body.queries(*queries) if queries
23
+ body.mutations(*mutations) if mutations
24
+ body.member_actions(*member_actions) if member_actions
25
+ body.batch_actions(*batch_actions) if batch_actions
26
+ body.collection_actions(*collection_actions) if collection_actions
27
+ body.instance_exec(&block) if block
28
+ name.to_sym
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Capability → member maps on {ResourceConfig} (RFC 0008, RFC 0009).
6
+ module ResourceCapabilityMaps
7
+ def field_capabilities
8
+ @field_capabilities ||= {}
9
+ end
10
+
11
+ def field_capability_modes
12
+ @field_capability_modes ||= {}
13
+ end
14
+
15
+ def query_capabilities
16
+ @query_capabilities ||= {}
17
+ end
18
+
19
+ def mutation_capabilities
20
+ @mutation_capabilities ||= {}
21
+ end
22
+
23
+ def capability_for_field(name)
24
+ field_capabilities[name.to_sym]
25
+ end
26
+
27
+ def capability_mode_for_field(name)
28
+ field_capability_modes[name.to_sym]
29
+ end
30
+
31
+ def capability_for_query(kind)
32
+ query_capabilities[kind.to_sym]
33
+ end
34
+
35
+ def capability_for_mutation(kind)
36
+ mutation_capabilities[kind.to_sym]
37
+ end
38
+
39
+ def assign_field_capabilities!(capability_name, names, mode: nil)
40
+ assign_capabilities!(field_capabilities, capability_name, names, label: "field")
41
+ return if mode.nil?
42
+
43
+ resolved = mode.to_sym
44
+ unless %i[omit null].include?(resolved)
45
+ raise ArgumentError, "capability mode must be :omit or :null, got #{mode.inspect}"
46
+ end
47
+
48
+ names.flatten.each do |name|
49
+ field_capability_modes[name.to_sym] = resolved
50
+ end
51
+ end
52
+
53
+ def assign_query_capabilities!(capability_name, names)
54
+ assign_capabilities!(query_capabilities, capability_name, names, label: "query")
55
+ end
56
+
57
+ def assign_mutation_capabilities!(capability_name, names)
58
+ assign_capabilities!(mutation_capabilities, capability_name, names, label: "mutation")
59
+ end
60
+
61
+ private
62
+
63
+ def assign_capabilities!(map, capability_name, names, label:)
64
+ capability = capability_name.to_sym
65
+ names.flatten.each do |name|
66
+ key = name.to_sym
67
+ existing = map[key]
68
+ if existing && existing != capability
69
+ raise ArgumentError,
70
+ "GraphQL #{label} #{key.inspect} already requires capability #{existing.inspect}"
71
+ end
72
+ map[key] = capability
73
+ end
74
+ end
75
+ end
76
+ end
77
+ 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
- @enabled = true
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