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,81 @@
1
+ # Rails composite primary keys alignment
2
+
3
+ ## Participants
4
+
5
+ Andrei Makarov
6
+
7
+ ## Decisions
8
+
9
+ Treat ActiveRecord composite primary keys and GraphQL `identity` as related but
10
+ not identical contracts. Keep GraphQL `id` as a single `ID!` string. Do not
11
+ adopt Rails form URL underscore packing as the default GraphQL id encoding.
12
+
13
+ Record alignment against the Rails guide Composite Primary Keys
14
+ (https://guides.rubyonrails.org/active_record_composite_primary_keys.html)
15
+ after shipping inferred id columns and RFC 0007 identity DSL.
16
+
17
+ ## Effects
18
+
19
+ Compared gem behavior to the guide’s model, query, form, and parameter rules.
20
+
21
+ ActiveRecord composite primary key (self.primary_key = array):
22
+
23
+ - Aligns: model declares composite key the same way as the guide’s
24
+ `self.primary_key = [:store_id, :sku]` style. Dummy `LibraryEdition` uses
25
+ `[:book_code, :seq]`.
26
+ - Aligns: internal batch and find paths convert that string id into an ordered
27
+ value list and use Rails tuple/`Relation#find` style lookups
28
+ (`ActiveAdmin::PrimaryKey.dataloader_tuple`, `ResourceQueryProxy` composite
29
+ branch). That matches the guide’s `Product.find([3, "XYZ12345"])` and
30
+ `where(primary_key => [[...], [...]])` idea at the Ruby API layer.
31
+ - Diverges: GraphQL and the published docs encode composite `id` as a JSON
32
+ object string (`{"book_code":"CPK","seq":7}`), not as the guide’s form/URL
33
+ underscore packing (`/books/2_25` with `params.extract_value(:id)`). JSON
34
+ keeps column names explicit and avoids ambiguity when a key value contains
35
+ `_`. Underscore packing is not accepted as GraphQL `id` today.
36
+ - Diverges: GraphQL also accepts per-key arguments and `WhereInput` fields for
37
+ composite models. The guide’s controller example extracts one `:id` param
38
+ into an array; we keep named keys as a GraphQL-friendly alternate.
39
+ - Aligns with guide caution: `where(id: ...)` / `find_by(id:)` on composite
40
+ models can mean an `:id` attribute, not the full primary key. The gem’s
41
+ GraphQL `id` field is the ActiveAdminResource identity string, not
42
+ necessarily an ActiveRecord `:id` column. Member lookup uses primary-key
43
+ columns (or configured identity), not a bare `where(id: graphql_id)` on
44
+ composite models.
45
+
46
+ Keyless views and RFC 0007 `graphql { identity ... }`:
47
+
48
+ - Outside the guide’s scope. The guide assumes a declared composite primary
49
+ key on the table/model. AlertEvent-style views with `primary_key = nil` and
50
+ a virtual Ruby `id` are not Rails composite primary keys.
51
+ - `identity :a, :b, :c, separator: ":"` is an application-level encoding so
52
+ GraphQL can share the same string as a hand-rolled `def id; [...].join(":")`.
53
+ That separator form is closer in spirit to Rails’ delimited URL id than our
54
+ JSON composite encoding, but the delimiter and segment rules are app-chosen,
55
+ not `extract_value`’s underscore default.
56
+ - Without `separator`, configured multi-column identity reuses the gem’s JSON
57
+ object string so keyless and AR-composite GraphQL ids stay one family.
58
+
59
+ Stock ActiveAdmin HTML member routes still need a composite-aware
60
+ `find_resource` (or custom finder) for AR composite keys; GraphQL does not
61
+ make Rails form URLs use JSON.
62
+
63
+ ## Next
64
+
65
+ Leave GraphQL composite `id` as JSON for AR composite primary keys unless a
66
+ later RFC deliberately adds underscore (or `extract_value`-compatible) accept
67
+ and emit. Document that choice next to the Rails guide, not as “the same
68
+ format Rails uses for casting composite ids” without qualification.
69
+
70
+ No change required for RFC 0007 separator identity; it is the escape hatch
71
+ when the model is not a Rails composite primary key.
72
+
73
+ ## Source
74
+
75
+ https://guides.rubyonrails.org/active_record_composite_primary_keys.html
76
+ docs/graphql-api.md (Nonconventional primary keys, Composite primary keys)
77
+ rfcs/0007-graphql-identity.md
78
+ lib/active_admin/primary_key.rb
79
+ lib/active_admin/graphql/resource_identity.rb
80
+ spec/dummy/app/models/library_edition.rb
81
+ spec/dummy/app/models/alert_event.rb
@@ -0,0 +1,55 @@
1
+ # Table partitions and database shards support
2
+
3
+ ## Participants
4
+
5
+ Andrei Makarov
6
+
7
+ ## Decisions
8
+
9
+ activeadmin-graphql does not treat database table partitions or Rails
10
+ database shards as a GraphQL configuration surface. Identity DSL (RFC 0007)
11
+ and ActiveRecord composite primary keys only encode lookup columns into
12
+ GraphQL `id`. They do not select a shard, connection role, or physical
13
+ partition.
14
+
15
+ App-level Rails multi-database setup (`connects_to`, `connected_to`,
16
+ horizontal shards, tenant middleware) may still apply implicitly: GraphQL
17
+ resolvers reuse ActiveAdmin `scoped_collection` / `find_resource` on whatever
18
+ connection the request process already has. That inheritance is undocumented
19
+ as a product feature and untested in this gem.
20
+
21
+ ## Effects
22
+
23
+ Searched lib, docs, rfcs, and spec for partition, shard, connects_to,
24
+ multiple database, tenant routing, writing_role, and reading_role.
25
+
26
+ Product code has no shard or partition API. Hits for “shard” and “partition”
27
+ are Polyrun/CI test parallelism and agent IO-fault vocabulary, not databases.
28
+
29
+ Dummy app uses one SQLite database (`spec/dummy/config/database.yml`). No
30
+ multi-DB, no partitioned table fixtures, no `connected_to` specs.
31
+
32
+ `RecordSource`, `PrimaryKey`, and `ResourceIdentity` run `where` / encode on
33
+ the current model class only. A partition-key column can appear in
34
+ `graphql { identity ... }` as part of the id string; that still queries the
35
+ already-selected connection and does not prune or route partitions.
36
+
37
+ ## Next
38
+
39
+ If a host needs GraphQL across shards or partitioned tables, keep routing in
40
+ the Rails/ActiveAdmin request boundary (middleware, `connected_to`, custom
41
+ finder). Do not invent gem DSL until a concrete host failure and RFC name the
42
+ contract (shard key in GraphQL args, role switching, cross-shard batch limits).
43
+
44
+ Optional later: document “unsupported / inherited only” next to identity in
45
+ `docs/graphql-api.md` when a reader asks again.
46
+
47
+ ## Source
48
+
49
+ lib/active_admin/graphql/resource_identity.rb
50
+ lib/active_admin/graphql/record_source.rb
51
+ lib/active_admin/graphql/resource_query_proxy.rb
52
+ spec/dummy/config/database.yml
53
+ rfcs/0007-graphql-identity.md
54
+ docs/issues/20261010111159_rails-composite-primary-keys-alignment.md
55
+ polyrun.yml (test shards only)
@@ -0,0 +1,46 @@
1
+ # GraphQL Federation support
2
+
3
+ ## Participants
4
+
5
+ Andrei Makarov
6
+
7
+ ## Decisions
8
+
9
+ activeadmin-graphql does not implement GraphQL Federation (Apollo Federation
10
+ v1/v2, subgraph/supergraph, `@key` entities, `_entities` / `_service`). The
11
+ product is one graphql-ruby schema per ActiveAdmin namespace for that admin
12
+ HTTP endpoint.
13
+
14
+ RFC 0007 `graphql { identity ... }` and `ActiveAdminResource.id` are record
15
+ lookup identity inside that schema. They are not federation entity keys and
16
+ do not emit federation SDL directives.
17
+
18
+ ## Effects
19
+
20
+ Searched lib, docs, rfcs, spec, and the gemspec for federation, subgraph,
21
+ supergraph, Apollo, `@key`, shareable, apollo-federation, `_entities`, and
22
+ `_service`. No product hits.
23
+
24
+ Runtime dependencies are `activeadmin` and `graphql` only. Schema build is
25
+ `Class.new(::GraphQL::Schema)` with query, mutation, dataloader, and optional
26
+ visibility (`lib/active_admin/graphql/schema_builder/build.rb`).
27
+ `ResourceInterface` is a local interface for fragment reuse, not a federation
28
+ entity type.
29
+
30
+ An app could in principle call `graphql_configure_schema` on the built schema
31
+ and layer a federation library itself. That path is neither documented nor
32
+ tested here.
33
+
34
+ ## Next
35
+
36
+ Keep federation out of scope until a host names a concrete subgraph contract
37
+ and an RFC. Prefer documenting “single-schema admin API; federation
38
+ unsupported” if readers keep asking.
39
+
40
+ ## Source
41
+
42
+ activeadmin-graphql.gemspec
43
+ lib/active_admin/graphql/schema_builder/build.rb
44
+ lib/active_admin/graphql/resource_interface.rb
45
+ rfcs/0007-graphql-identity.md
46
+ docs/issues/20261010111300_partitions-and-shards-support.md
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Named grants carried in GraphQL +context[:capabilities]+ (RFC 0008).
6
+ module Capability
7
+ module_function
8
+
9
+ def normalize_set(raw)
10
+ return nil if raw.nil?
11
+
12
+ Set.new(
13
+ Array(raw).filter_map do |item|
14
+ next if item.nil?
15
+
16
+ name = item.to_s.strip
17
+ next if name.empty?
18
+
19
+ name.to_sym
20
+ end
21
+ )
22
+ end
23
+
24
+ def granted?(context, capability_name)
25
+ return true if capability_name.nil?
26
+
27
+ caps = context[:capabilities]
28
+ return false if caps.nil?
29
+
30
+ caps = normalize_set(caps) unless caps.is_a?(Set)
31
+ caps.include?(capability_name.to_sym)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Block body for +capability :name do ... end+ (RFC 0008).
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)
18
+ @config = config
19
+ @capability_name = capability_name.to_sym
20
+ end
21
+
22
+ def fields(*names)
23
+ @config.assign_field_capabilities!(@capability_name, names)
24
+ end
25
+
26
+ def queries(*names)
27
+ keys = names.flatten.map { |name| normalize_query_key(name) }
28
+ @config.assign_query_capabilities!(@capability_name, keys)
29
+ end
30
+
31
+ def mutations(*names)
32
+ keys = names.flatten.map { |name| normalize_mutation_key(name) }
33
+ @config.assign_mutation_capabilities!(@capability_name, keys)
34
+ end
35
+
36
+ def member_actions(*names)
37
+ keys = names.flatten.map { |name| :"member_#{name.to_s.tr("-", "_")}" }
38
+ @config.assign_mutation_capabilities!(@capability_name, keys)
39
+ end
40
+
41
+ def batch_actions(*names)
42
+ keys = names.flatten.map { |name| :"batch_#{name.to_s.tr("-", "_")}" }
43
+ @config.assign_mutation_capabilities!(@capability_name, keys)
44
+ end
45
+
46
+ def collection_actions(*names)
47
+ keys = names.flatten.map { |name| :"collection_#{name.to_s.tr("-", "_")}" }
48
+ @config.assign_mutation_capabilities!(@capability_name, keys)
49
+ end
50
+
51
+ private
52
+
53
+ def normalize_query_key(name)
54
+ key = name.to_sym
55
+ QUERY_ALIASES.fetch(key, key)
56
+ end
57
+
58
+ def normalize_mutation_key(name)
59
+ key = name.to_sym
60
+ MUTATION_ALIASES.fetch(key, key)
61
+ end
62
+ end
63
+ end
64
+ 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
 
@@ -26,12 +27,26 @@ module ActiveAdmin
26
27
  def register_namespace_settings!
27
28
  ns = ActiveAdmin::NamespaceSettings
28
29
  ns.register :graphql, false
30
+ # true preserves 0.3.x: every ActiveRecord resource is in the schema once the namespace endpoint is on.
31
+ # Set false for opt-in per resource (empty graphql block or enable!).
32
+ ns.register :graphql_resource_enabled_default, true
33
+ ns.register :graphql_queries_enabled_default, true
34
+ ns.register :graphql_mutations_enabled_default, true
35
+ ns.register :graphql_run_actions_enabled_default, true
36
+ ns.register :graphql_policies_enabled, true
37
+ ns.register :graphql_policies_enabled_default, true
38
+ # Payload builder for policy fields when graphql_policies_enabled is true.
39
+ ns.register :graphql_policy_adapter, ActiveAdmin::GraphQL::PolicyAdapter
40
+ ns.register :graphql_registered_resource_enabled, true
41
+ ns.register :graphql_belongs_to_associations_enabled, true
29
42
  ns.register :graphql_path, "graphql"
30
43
  ns.register :graphql_multiplex_max, 20
31
44
  ns.register :graphql_dataloader, nil
32
45
  ns.register :graphql_visibility, nil
33
46
  ns.register :graphql_visibility_profile, nil
34
47
  ns.register :graphql_schema_visible, nil
48
+ # Optional Proc +(context_hash) -> Enumerable+ of capability names (RFC 0008).
49
+ ns.register :graphql_capabilities, nil
35
50
  ns.register :graphql_max_complexity, 300
36
51
  ns.register :graphql_max_depth, 18
37
52
  ns.register :graphql_batch_action_max_ids, 5000
@@ -64,7 +79,10 @@ module ActiveAdmin
64
79
  module ResourceDSLMethods
65
80
  def graphql(&block)
66
81
  if block
67
- ActiveAdmin::GraphQL::ResourceDefinitionDSL.new(config.graphql_config).instance_exec(&block)
82
+ cfg = config.graphql_config
83
+ # Presence of a graphql block is an intentional opt-in when the namespace default is false.
84
+ cfg.enabled = true
85
+ ActiveAdmin::GraphQL::ResourceDefinitionDSL.new(cfg).instance_exec(&block)
68
86
  end
69
87
  config.graphql_config
70
88
  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,13 +28,17 @@ 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
34
38
 
35
39
  def cache_key(subject_owner, subject)
36
40
  owner_key = subject_owner_key(subject_owner)
37
- "#{owner_key}:#{subject_key(subject)}"
41
+ "#{owner_key}:#{subject_key(subject_owner, subject)}"
38
42
  end
39
43
 
40
44
  private
@@ -47,13 +51,18 @@ module ActiveAdmin
47
51
  end
48
52
  end
49
53
 
50
- def subject_key(subject)
54
+ def subject_key(subject_owner, subject)
51
55
  if subject.is_a?(Class)
52
56
  "class:#{subject.name}"
53
57
  elsif subject.is_a?(ActiveAdmin::Page)
54
58
  "page:#{subject.name}"
55
59
  elsif subject.is_a?(ActiveRecord::Base)
56
- identity = PrimaryKey.graphql_id_value(subject)
60
+ identity =
61
+ if subject_owner.respond_to?(:graphql_config)
62
+ ResourceIdentity.graphql_id_value(subject_owner, subject)
63
+ else
64
+ PrimaryKey.graphql_id_value(subject)
65
+ end
57
66
  identity = "#{identity}:object:#{subject.object_id}" if subject.class.primary_key.blank?
58
67
  "record:#{subject.class.name}:#{identity}"
59
68
  else
@@ -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,18 +2,29 @@
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
14
21
  attr_accessor :permit_params_attributes
15
22
  attr_accessor :html_permit_params_attributes
16
23
  attr_accessor :extension_block
24
+ attr_accessor :identity_columns
25
+ attr_accessor :identity_separator
26
+ attr_accessor :identity_encode_proc
27
+ attr_accessor :identity_decode_proc
17
28
 
18
29
  # Optional resolver overrides (set from +graphql do+). SchemaBuilder still owns field names,
19
30
  # arguments, and types; procs replace only the Ruby resolution body.
@@ -27,8 +38,18 @@ module ActiveAdmin
27
38
  # {RunActionMutationConfig#payload_type} is not set. Falls back to {RunActionPayload}.
28
39
  attr_accessor :run_action_payload_type
29
40
 
30
- def initialize
31
- @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
32
53
  @exclude_attributes = []
33
54
  end
34
55
 
@@ -3,17 +3,27 @@
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
20
+ # Declare GraphQL identity columns when ActiveRecord has no primary key / id column.
21
+ # See RFC 0007.
22
+ def identity(*columns, separator: nil, encode: nil, decode: nil)
23
+ @config.identity_columns = columns.flatten.map(&:to_s)
24
+ @config.identity_separator = separator
25
+ @config.identity_encode_proc = encode
26
+ @config.identity_decode_proc = decode
17
27
  end
18
28
 
19
29
  def type_name(name)