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
@@ -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: 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).map do |aa_res|
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(name.to_sym, gql_t, null: true, camelize: false, authorize: false)
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
- type_class.field :activeadmin_policies, policy_set_type, null: false, camelize: false, authorize: false
122
- type_class.define_method(:activeadmin_policies) do
123
- ActiveAdmin::GraphQL::PolicySetCache.fetch(
124
- context,
125
- subject_owner: aa_res,
126
- subject: object
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
- if vis == true
14
- schema.use(::GraphQL::Schema::Visibility)
15
- elsif vis.is_a?(Hash)
16
- schema.use(::GraphQL::Schema::Visibility, **vis.symbolize_keys)
17
- else
18
- raise ActiveAdmin::DependencyError,
19
- "namespace graphql_visibility must be nil, true, or a Hash of GraphQL::Schema::Visibility options"
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
- # That hook runs from {#visible?}; it does not replace graphql-ruby's visibility system—it
10
- # composes with +super+ like any custom +Field+ class.
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
- def initialize(visibility: nil, authorize: nil, authorize_action: nil, **kwargs, &block)
13
- @visibility = visibility
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
- super(**kwargs, &block)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveAdmin
4
4
  module GraphQL
5
- VERSION = "0.3.2"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
@@ -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.3.2
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