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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  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/20261010101500_nonconventional-id-fields.md +16 -2
  6. data/docs/changelogs/20261010103800_release-0-3-1.md +9 -2
  7. data/docs/changelogs/20261010110600_graphql-identity-dsl.md +34 -0
  8. data/docs/changelogs/20261010111800_release-0-3-2.md +46 -0
  9. data/docs/changelogs/20261012050051_graphql-surface-toggles.md +40 -0
  10. data/docs/changelogs/20261012064500_graphql-capabilities.md +35 -0
  11. data/docs/changelogs/20261012113500_release-0-3-3.md +45 -0
  12. data/docs/graphql-api.md +166 -20
  13. data/docs/issues/20261010111159_rails-composite-primary-keys-alignment.md +81 -0
  14. data/docs/issues/20261010111300_partitions-and-shards-support.md +55 -0
  15. data/docs/issues/20261010111500_graphql-federation-support.md +46 -0
  16. data/lib/active_admin/graphql/capability.rb +35 -0
  17. data/lib/active_admin/graphql/capability_definition_dsl.rb +64 -0
  18. data/lib/active_admin/graphql/integration.rb +19 -1
  19. data/lib/active_admin/graphql/policy_adapter.rb +94 -0
  20. data/lib/active_admin/graphql/policy_set_cache.rb +18 -9
  21. data/lib/active_admin/graphql/resource_capability_dsl.rb +31 -0
  22. data/lib/active_admin/graphql/resource_capability_maps.rb +59 -0
  23. data/lib/active_admin/graphql/resource_config.rb +23 -2
  24. data/lib/active_admin/graphql/resource_definition_dsl.rb +12 -2
  25. data/lib/active_admin/graphql/resource_identity.rb +98 -0
  26. data/lib/active_admin/graphql/resource_methods.rb +8 -2
  27. data/lib/active_admin/graphql/resource_query_proxy/identity.rb +4 -4
  28. data/lib/active_admin/graphql/resource_query_proxy.rb +5 -4
  29. data/lib/active_admin/graphql/resource_surface_dsl.rb +48 -0
  30. data/lib/active_admin/graphql/schema_builder/build.rb +52 -3
  31. data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +7 -0
  32. data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +8 -0
  33. data/lib/active_admin/graphql/schema_builder/mutation_create.rb +1 -0
  34. data/lib/active_admin/graphql/schema_builder/mutation_member.rb +8 -0
  35. data/lib/active_admin/graphql/schema_builder/mutation_type_builder.rb +15 -10
  36. data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +4 -2
  37. data/lib/active_admin/graphql/schema_builder/query_type.rb +2 -1
  38. data/lib/active_admin/graphql/schema_builder/query_type_collection.rb +3 -0
  39. data/lib/active_admin/graphql/schema_builder/query_type_member.rb +5 -2
  40. data/lib/active_admin/graphql/schema_builder/query_type_policies.rb +9 -63
  41. data/lib/active_admin/graphql/schema_builder/resources.rb +34 -0
  42. data/lib/active_admin/graphql/schema_builder/types_object.rb +27 -11
  43. data/lib/active_admin/graphql/schema_builder/visibility.rb +22 -7
  44. data/lib/active_admin/graphql/schema_builder/wire.rb +3 -1
  45. data/lib/active_admin/graphql/schema_field.rb +18 -2
  46. data/lib/active_admin/graphql/version.rb +1 -1
  47. data/lib/active_admin/graphql.rb +3 -0
  48. 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::PrimaryKey.columns(resource_class)
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 find_inferred_member(controller, model, id)
10
- primary_key = ActiveAdmin::PrimaryKey.columns(model).first
11
- raise ActiveRecord::RecordNotFound unless primary_key
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(primary_key => id).limit(2).to_a
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
- find_inferred_member(controller, model, extra.fetch("id"))
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
- if ActiveAdmin::PrimaryKey.composite?(model)
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 = ActiveAdmin::PrimaryKey.columns(model).first&.to_sym
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
- union_members = @object_types.values.uniq
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
- actions = defined_actions_for(aa_res)
20
-
21
- if actions.include?(:create)
22
- mutations << mutation_create_field(sb, ns, aa_res, model, type_c)
23
- end
24
- if actions.include?(:update)
25
- mutations << mutation_update_field(sb, ns, aa_res, model, type_c)
26
- end
27
- if actions.include?(:destroy)
28
- mutations << mutation_destroy_field(sb, ns, aa_res, model)
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::PrimaryKey.member_param_hash(model, blob)
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::PrimaryKey.member_param_hash(model, blob)
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: 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::PrimaryKey.member_param_hash(model, blob)
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::PrimaryKey.field_kw_to_param_hash(model, id: id, **kw)
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).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
@@ -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)