activeadmin-graphql 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/docs/changelogs/20260818111600_rfc-0002-problems.md +25 -0
- data/docs/changelogs/20260818114000_rfc-as-built-coverage.md +27 -0
- data/docs/changelogs/20260818114700_rfc-design-not-version.md +27 -0
- data/docs/changelogs/20260907141000_advisory-lockfile-refresh.md +21 -0
- data/docs/changelogs/20260921103000_mutation-attribute-allowlists.md +24 -0
- data/docs/changelogs/20260921111000_release-0-3-0.md +42 -0
- data/docs/changelogs/20260921112800_drop-standard-rubocop-direct.md +35 -0
- data/docs/changelogs/20260921120000_graphql-floor-and-activeadmin-4.md +41 -0
- data/docs/changelogs/20261010101500_nonconventional-id-fields.md +48 -0
- data/docs/changelogs/20261010103800_release-0-3-1.md +39 -0
- data/docs/changelogs/20261010110600_graphql-identity-dsl.md +34 -0
- data/docs/changelogs/20261010111800_release-0-3-2.md +46 -0
- data/docs/dependencies/20260907141000_json-cve-2026-71847.md +68 -0
- data/docs/dependencies/20260907141000_sqlite3-ghsa-mwm8-39rw-8826.md +62 -0
- data/docs/dependencies/20260921114000_rails-json-parse-kwargs.md +61 -0
- data/docs/dependencies/20260921114500_graphql-ghsa-rmxg-5p3r-j6hh.md +69 -0
- data/docs/dependencies/20261007161308_association-list-combobox.md +23 -0
- data/docs/graphql-api.md +49 -6
- data/docs/issues/20260907141000_engineering-and-dependency-audit.md +30 -0
- data/docs/issues/20260917123253_ci-drop-automatic-dependency-audit.md +29 -0
- data/docs/issues/20260921103000_mutation-attribute-allowlists.md +40 -0
- data/docs/issues/20260921104400_convention-alignment-audit.md +60 -0
- data/docs/issues/20260921114500_dependency-audit.md +90 -0
- data/docs/issues/20260921115500_file-length-metrics.md +31 -0
- data/docs/issues/20261010111159_rails-composite-primary-keys-alignment.md +81 -0
- data/docs/issues/20261010111300_partitions-and-shards-support.md +55 -0
- data/docs/issues/20261010111500_graphql-federation-support.md +46 -0
- data/docs/plan/20260817201500_rfc-starter.md +31 -0
- data/lib/active_admin/graphql/policy_set_cache.rb +10 -3
- data/lib/active_admin/graphql/record_source.rb +3 -2
- data/lib/active_admin/graphql/resource_config.rb +4 -0
- data/lib/active_admin/graphql/resource_definition_dsl.rb +9 -0
- data/lib/active_admin/graphql/resource_identity.rb +98 -0
- data/lib/active_admin/graphql/resource_methods.rb +1 -1
- data/lib/active_admin/graphql/resource_query_proxy/identity.rb +38 -0
- data/lib/active_admin/graphql/resource_query_proxy.rb +16 -10
- data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +2 -2
- data/lib/active_admin/graphql/schema_builder/query_type_member.rb +4 -2
- data/lib/active_admin/graphql/schema_builder/types_object.rb +23 -4
- data/lib/active_admin/graphql/version.rb +1 -1
- data/lib/active_admin/graphql.rb +1 -0
- data/lib/active_admin/primary_key.rb +13 -4
- metadata +30 -1
|
@@ -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,31 @@
|
|
|
1
|
+
# RFC starter pack
|
|
2
|
+
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
Andrei Makarov
|
|
6
|
+
|
|
7
|
+
## Decisions
|
|
8
|
+
|
|
9
|
+
Pilot the portable RFC form: README, template, RFC 0001 Proposed, RFC 0002 Informational positioning, ids/0001 and ids/0002, CONTRIBUTING and GOVERNANCE pointers.
|
|
10
|
+
|
|
11
|
+
Isolation is off. Sequential numbering. An RFC specifies a design. Version numbers belong in changelogs. Shipped design specified after the fact is Stable.
|
|
12
|
+
|
|
13
|
+
## Effects
|
|
14
|
+
|
|
15
|
+
Starter files written on plan/rfc-process. Process RFC remains Proposed until 2026-08-31.
|
|
16
|
+
|
|
17
|
+
Later pass 2026-08-18: RFC 0002 marked as positioning. Types and statuses cite rfcs/README.md. Relates dropped from RFC 0002. Process text stays aligned with kiskolabs/pray RFC 0001 until that RFC is Stable.
|
|
18
|
+
|
|
19
|
+
Later pass 2026-08-18: RFC 0002 rewritten with concrete problems, shipped solutions, and which surfaces need a later Standards Track RFC. RFC 0001 Motivation names those surfaces.
|
|
20
|
+
|
|
21
|
+
Later pass 2026-08-18: as-built Informational RFCs added for each public contract named in RFC 0002. Describes stays on the current version. Status is Stable. RFC 0002 Relates those documents.
|
|
22
|
+
|
|
23
|
+
Later pass 2026-08-18: product RFCs specify a design. Suggestion, motivation, specification, effects, alternatives, and prior art. Version numbers belong in changelogs. RFC 0002 stays Informational. RFC 0003 and later are Standards Track Stable. Describes dropped.
|
|
24
|
+
|
|
25
|
+
## Next
|
|
26
|
+
|
|
27
|
+
Discuss RFC 0001. Extract amkisko/rfc-process after kiskolabs/pray RFC 0001 is Stable. Replace this hand-copied pack if the prayer differs.
|
|
28
|
+
|
|
29
|
+
## Source
|
|
30
|
+
|
|
31
|
+
Upstream: kiskolabs/pray rfcs/0001-rfc-process.md and usr/docs/plan/20260817200400_library-rfc-adoption.md.
|
|
@@ -34,7 +34,7 @@ module ActiveAdmin
|
|
|
34
34
|
|
|
35
35
|
def cache_key(subject_owner, subject)
|
|
36
36
|
owner_key = subject_owner_key(subject_owner)
|
|
37
|
-
"#{owner_key}:#{subject_key(subject)}"
|
|
37
|
+
"#{owner_key}:#{subject_key(subject_owner, subject)}"
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
private
|
|
@@ -47,13 +47,20 @@ module ActiveAdmin
|
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
def subject_key(subject)
|
|
50
|
+
def subject_key(subject_owner, subject)
|
|
51
51
|
if subject.is_a?(Class)
|
|
52
52
|
"class:#{subject.name}"
|
|
53
53
|
elsif subject.is_a?(ActiveAdmin::Page)
|
|
54
54
|
"page:#{subject.name}"
|
|
55
55
|
elsif subject.is_a?(ActiveRecord::Base)
|
|
56
|
-
|
|
56
|
+
identity =
|
|
57
|
+
if subject_owner.respond_to?(:graphql_config)
|
|
58
|
+
ResourceIdentity.graphql_id_value(subject_owner, subject)
|
|
59
|
+
else
|
|
60
|
+
PrimaryKey.graphql_id_value(subject)
|
|
61
|
+
end
|
|
62
|
+
identity = "#{identity}:object:#{subject.object_id}" if subject.class.primary_key.blank?
|
|
63
|
+
"record:#{subject.class.name}:#{identity}"
|
|
57
64
|
else
|
|
58
65
|
"object:#{subject.object_id}"
|
|
59
66
|
end
|
|
@@ -6,13 +6,14 @@ module ActiveAdmin
|
|
|
6
6
|
class RecordSource < ::GraphQL::Dataloader::Source
|
|
7
7
|
def initialize(model_class)
|
|
8
8
|
@model_class = model_class
|
|
9
|
-
@
|
|
9
|
+
@pk_columns = ActiveAdmin::PrimaryKey.columns(model_class)
|
|
10
|
+
@pk = ActiveAdmin::PrimaryKey.composite?(model_class) ? model_class.primary_key : @pk_columns.first
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def fetch(ids)
|
|
13
14
|
keys = normalized_keys(ids)
|
|
14
15
|
uniq = keys.compact.uniq
|
|
15
|
-
return Array.new(ids.size) { nil } if uniq.empty?
|
|
16
|
+
return Array.new(ids.size) { nil } if uniq.empty? || @pk.nil?
|
|
16
17
|
|
|
17
18
|
indexed = load_records(uniq)
|
|
18
19
|
keys.map { |key| key.nil? ? nil : indexed[key] }
|
|
@@ -14,6 +14,10 @@ module ActiveAdmin
|
|
|
14
14
|
attr_accessor :permit_params_attributes
|
|
15
15
|
attr_accessor :html_permit_params_attributes
|
|
16
16
|
attr_accessor :extension_block
|
|
17
|
+
attr_accessor :identity_columns
|
|
18
|
+
attr_accessor :identity_separator
|
|
19
|
+
attr_accessor :identity_encode_proc
|
|
20
|
+
attr_accessor :identity_decode_proc
|
|
17
21
|
|
|
18
22
|
# Optional resolver overrides (set from +graphql do+). SchemaBuilder still owns field names,
|
|
19
23
|
# arguments, and types; procs replace only the Ruby resolution body.
|
|
@@ -16,6 +16,15 @@ module ActiveAdmin
|
|
|
16
16
|
@config.enabled = false
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# Declare GraphQL identity columns when ActiveRecord has no primary key / id column.
|
|
20
|
+
# See RFC 0007.
|
|
21
|
+
def identity(*columns, separator: nil, encode: nil, decode: nil)
|
|
22
|
+
@config.identity_columns = columns.flatten.map(&:to_s)
|
|
23
|
+
@config.identity_separator = separator
|
|
24
|
+
@config.identity_encode_proc = encode
|
|
25
|
+
@config.identity_decode_proc = decode
|
|
26
|
+
end
|
|
27
|
+
|
|
19
28
|
def type_name(name)
|
|
20
29
|
@config.graphql_type_name = name.to_s
|
|
21
30
|
end
|
|
@@ -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
|
|
@@ -22,7 +22,7 @@ module ActiveAdmin
|
|
|
22
22
|
|
|
23
23
|
def graphql_assignable_attribute_names
|
|
24
24
|
names = attributes_for_graphql.map(&:to_s)
|
|
25
|
-
pk_cols = ActiveAdmin::
|
|
25
|
+
pk_cols = ActiveAdmin::GraphQL::ResourceIdentity.columns(self)
|
|
26
26
|
return names if pk_cols.size > 1
|
|
27
27
|
|
|
28
28
|
names - pk_cols
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
class ResourceQueryProxy
|
|
6
|
+
module Identity
|
|
7
|
+
private
|
|
8
|
+
|
|
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
|
+
|
|
13
|
+
records = controller.send(:scoped_collection).where(attrs).limit(2).to_a
|
|
14
|
+
raise_ambiguous_id!(model) if records.size > 1
|
|
15
|
+
|
|
16
|
+
records.first || raise(ActiveRecord::RecordNotFound)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def index_records_by_id(relation, model, primary_key)
|
|
20
|
+
if model.primary_key.present?
|
|
21
|
+
return relation.index_by { |record| record.public_send(primary_key).to_s }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
grouped = relation.to_a.group_by { |record| record.public_send(primary_key).to_s }
|
|
25
|
+
if grouped.any? { |_id, matches| matches.size > 1 }
|
|
26
|
+
raise_ambiguous_id!(model)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
grouped.transform_values(&:first)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def raise_ambiguous_id!(model)
|
|
33
|
+
raise ::GraphQL::ExecutionError, "ambiguous GraphQL id for #{model.name}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -4,14 +4,14 @@ require "rack/mock"
|
|
|
4
4
|
require "stringio"
|
|
5
5
|
|
|
6
6
|
require_relative "resource_query_proxy/controller"
|
|
7
|
+
require_relative "resource_query_proxy/identity"
|
|
7
8
|
|
|
8
9
|
module ActiveAdmin
|
|
9
10
|
module GraphQL
|
|
10
|
-
# Reuses
|
|
11
|
-
# +find_resource+, authorization scoping, Ransack, menu scopes, +sorting+, +includes+)
|
|
12
|
-
# so GraphQL list/detail mutations align with the HTML/JSON list and member REST endpoints.
|
|
11
|
+
# Reuses ResourceController data access so GraphQL operations align with the REST endpoints.
|
|
13
12
|
class ResourceQueryProxy
|
|
14
13
|
include Controller
|
|
14
|
+
include Identity
|
|
15
15
|
|
|
16
16
|
def initialize(aa_resource:, user:, namespace:, graph_params: {})
|
|
17
17
|
@aa_resource = aa_resource
|
|
@@ -26,8 +26,13 @@ module ActiveAdmin
|
|
|
26
26
|
|
|
27
27
|
def find_member(id)
|
|
28
28
|
extra = member_route_params_for_find(id)
|
|
29
|
-
controller_for("show", extra)
|
|
30
|
-
|
|
29
|
+
controller = controller_for("show", extra)
|
|
30
|
+
model = @aa_resource.resource_class
|
|
31
|
+
finder = controller.send(:method_for_find)
|
|
32
|
+
return controller.send(:find_resource) if model.primary_key.present? || finder != :find
|
|
33
|
+
|
|
34
|
+
find_identity_member(controller, model, extra.fetch("id"))
|
|
35
|
+
rescue ActiveRecord::RecordNotFound, ArgumentError
|
|
31
36
|
nil
|
|
32
37
|
end
|
|
33
38
|
|
|
@@ -36,16 +41,17 @@ module ActiveAdmin
|
|
|
36
41
|
return {} if string_ids.empty?
|
|
37
42
|
|
|
38
43
|
model = @aa_resource.resource_class
|
|
39
|
-
|
|
44
|
+
identity_cols = ActiveAdmin::GraphQL::ResourceIdentity.columns(@aa_resource)
|
|
45
|
+
if ActiveAdmin::PrimaryKey.composite?(model) || identity_cols.size > 1
|
|
40
46
|
return string_ids.index_with { |identifier| find_member(identifier) }
|
|
41
47
|
end
|
|
42
48
|
|
|
43
49
|
controller = controller_for("index")
|
|
44
50
|
relation = controller.send(:apply_authorization_scope, controller.send(:scoped_collection))
|
|
45
|
-
primary_key =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
primary_key = identity_cols.first&.to_sym
|
|
52
|
+
return string_ids.index_with { nil } if primary_key.nil?
|
|
53
|
+
|
|
54
|
+
indexed = index_records_by_id(relation.where(primary_key => string_ids), model, primary_key)
|
|
49
55
|
string_ids.index_with { |identifier| indexed[identifier] }
|
|
50
56
|
end
|
|
51
57
|
|
|
@@ -24,7 +24,7 @@ module ActiveAdmin
|
|
|
24
24
|
blob = where.to_h.stringify_keys
|
|
25
25
|
graph = sb.graph_params_from_find_blob(aa_res, blob)
|
|
26
26
|
begin
|
|
27
|
-
rid = ActiveAdmin::
|
|
27
|
+
rid = ActiveAdmin::GraphQL::ResourceIdentity.member_param_hash(aa_res, blob)
|
|
28
28
|
rescue ArgumentError => e
|
|
29
29
|
raise ::GraphQL::ExecutionError, e.message
|
|
30
30
|
end
|
|
@@ -90,7 +90,7 @@ module ActiveAdmin
|
|
|
90
90
|
blob = where.to_h.stringify_keys
|
|
91
91
|
graph = sb.graph_params_from_find_blob(aa_res, blob)
|
|
92
92
|
begin
|
|
93
|
-
rid = ActiveAdmin::
|
|
93
|
+
rid = ActiveAdmin::GraphQL::ResourceIdentity.member_param_hash(aa_res, blob)
|
|
94
94
|
rescue ArgumentError => e
|
|
95
95
|
raise ::GraphQL::ExecutionError, e.message
|
|
96
96
|
end
|
|
@@ -51,10 +51,12 @@ module ActiveAdmin
|
|
|
51
51
|
if where
|
|
52
52
|
blob = where.to_h.stringify_keys
|
|
53
53
|
graph = builder.graph_params_from_find_blob(aa_res, blob)
|
|
54
|
-
rid = ActiveAdmin::
|
|
54
|
+
rid = ActiveAdmin::GraphQL::ResourceIdentity.member_param_hash(aa_res, blob)
|
|
55
55
|
else
|
|
56
56
|
graph = builder.graph_params_for_mutation(aa_res, kw)
|
|
57
|
-
rid = ActiveAdmin::
|
|
57
|
+
rid = ActiveAdmin::GraphQL::ResourceIdentity.field_kw_to_param_hash(
|
|
58
|
+
aa_res, id: id, **kw
|
|
59
|
+
)
|
|
58
60
|
end
|
|
59
61
|
rescue ArgumentError => e
|
|
60
62
|
raise ::GraphQL::ExecutionError, e.message
|
|
@@ -82,13 +82,32 @@ module ActiveAdmin
|
|
|
82
82
|
define_singleton_method(:activeadmin_graphql_resource) { aa_res }
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
pk_cols = ActiveAdmin::
|
|
85
|
+
pk_cols = ActiveAdmin::GraphQL::ResourceIdentity.columns(aa_res)
|
|
86
|
+
if pk_cols.empty?
|
|
87
|
+
raise ArgumentError,
|
|
88
|
+
"#{model.name} has no primary key for GraphQL `id`; set self.primary_key on the model " \
|
|
89
|
+
"or graphql { identity ... }"
|
|
90
|
+
end
|
|
86
91
|
|
|
87
92
|
type_class.field :id, ::GraphQL::Types::ID, null: false, authorize: false
|
|
88
|
-
type_class.define_method(:id)
|
|
93
|
+
type_class.define_method(:id) do
|
|
94
|
+
ActiveAdmin::GraphQL::ResourceIdentity.graphql_id_value(
|
|
95
|
+
self.class.activeadmin_graphql_resource,
|
|
96
|
+
object
|
|
97
|
+
)
|
|
98
|
+
end
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
# ActiveAdmin omits a single-column primary key from resource_attributes; still expose
|
|
101
|
+
# non-id key columns as scalars (composite keys already remain in attributes).
|
|
102
|
+
# ActiveAdminResource owns GraphQL `id`; never emit a second field from an `id` column.
|
|
103
|
+
configured_pk_cols = pk_cols
|
|
104
|
+
if (only = aa_res.graphql_config.only_attributes)
|
|
105
|
+
configured_pk_cols &= only.map(&:to_s)
|
|
106
|
+
end
|
|
107
|
+
configured_pk_cols -= aa_res.graphql_config.exclude_attributes.map(&:to_s)
|
|
108
|
+
readable_names = (attr_names + configured_pk_cols).uniq
|
|
109
|
+
readable_names.each do |name|
|
|
110
|
+
next if name == "id"
|
|
92
111
|
|
|
93
112
|
col = cols_by_name[name]
|
|
94
113
|
next unless col
|
data/lib/active_admin/graphql.rb
CHANGED
|
@@ -14,13 +14,22 @@ unless defined?(ActiveAdmin::PrimaryKey)
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def ordered_columns(model)
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
cols = case (pk = model.primary_key)
|
|
18
|
+
when nil, false then []
|
|
19
|
+
when Array then pk.map(&:to_s).reject(&:empty?)
|
|
20
|
+
else
|
|
21
|
+
(name = pk.to_s).empty? ? [] : [name]
|
|
22
|
+
end
|
|
23
|
+
return cols if cols.any?
|
|
24
|
+
return %w[id] if model.respond_to?(:columns_hash) && model.columns_hash.key?("id")
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
[]
|
|
27
|
+
rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished
|
|
28
|
+
[]
|
|
22
29
|
end
|
|
23
30
|
|
|
31
|
+
def columns(model) = ordered_columns(model)
|
|
32
|
+
|
|
24
33
|
def composite_attribute_hash(model, id_param)
|
|
25
34
|
cols = ordered_columns(model)
|
|
26
35
|
hash = normalize_composite_input(cols, id_param)
|
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.
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -366,7 +366,34 @@ files:
|
|
|
366
366
|
- README.md
|
|
367
367
|
- activeadmin-graphql.gemspec
|
|
368
368
|
- app/controllers/active_admin/graphql_controller.rb
|
|
369
|
+
- docs/changelogs/20260818111600_rfc-0002-problems.md
|
|
370
|
+
- docs/changelogs/20260818114000_rfc-as-built-coverage.md
|
|
371
|
+
- docs/changelogs/20260818114700_rfc-design-not-version.md
|
|
372
|
+
- docs/changelogs/20260907141000_advisory-lockfile-refresh.md
|
|
373
|
+
- docs/changelogs/20260921103000_mutation-attribute-allowlists.md
|
|
374
|
+
- docs/changelogs/20260921111000_release-0-3-0.md
|
|
375
|
+
- docs/changelogs/20260921112800_drop-standard-rubocop-direct.md
|
|
376
|
+
- docs/changelogs/20260921120000_graphql-floor-and-activeadmin-4.md
|
|
377
|
+
- docs/changelogs/20261010101500_nonconventional-id-fields.md
|
|
378
|
+
- docs/changelogs/20261010103800_release-0-3-1.md
|
|
379
|
+
- docs/changelogs/20261010110600_graphql-identity-dsl.md
|
|
380
|
+
- docs/changelogs/20261010111800_release-0-3-2.md
|
|
381
|
+
- docs/dependencies/20260907141000_json-cve-2026-71847.md
|
|
382
|
+
- docs/dependencies/20260907141000_sqlite3-ghsa-mwm8-39rw-8826.md
|
|
383
|
+
- docs/dependencies/20260921114000_rails-json-parse-kwargs.md
|
|
384
|
+
- docs/dependencies/20260921114500_graphql-ghsa-rmxg-5p3r-j6hh.md
|
|
385
|
+
- docs/dependencies/20261007161308_association-list-combobox.md
|
|
369
386
|
- docs/graphql-api.md
|
|
387
|
+
- docs/issues/20260907141000_engineering-and-dependency-audit.md
|
|
388
|
+
- docs/issues/20260917123253_ci-drop-automatic-dependency-audit.md
|
|
389
|
+
- docs/issues/20260921103000_mutation-attribute-allowlists.md
|
|
390
|
+
- docs/issues/20260921104400_convention-alignment-audit.md
|
|
391
|
+
- docs/issues/20260921114500_dependency-audit.md
|
|
392
|
+
- docs/issues/20260921115500_file-length-metrics.md
|
|
393
|
+
- docs/issues/20261010111159_rails-composite-primary-keys-alignment.md
|
|
394
|
+
- docs/issues/20261010111300_partitions-and-shards-support.md
|
|
395
|
+
- docs/issues/20261010111500_graphql-federation-support.md
|
|
396
|
+
- docs/plan/20260817201500_rfc-starter.md
|
|
370
397
|
- lib/active_admin/graphql.rb
|
|
371
398
|
- lib/active_admin/graphql/auth_context.rb
|
|
372
399
|
- lib/active_admin/graphql/engine.rb
|
|
@@ -380,10 +407,12 @@ files:
|
|
|
380
407
|
- lib/active_admin/graphql/record_source.rb
|
|
381
408
|
- lib/active_admin/graphql/resource_config.rb
|
|
382
409
|
- lib/active_admin/graphql/resource_definition_dsl.rb
|
|
410
|
+
- lib/active_admin/graphql/resource_identity.rb
|
|
383
411
|
- lib/active_admin/graphql/resource_interface.rb
|
|
384
412
|
- lib/active_admin/graphql/resource_methods.rb
|
|
385
413
|
- lib/active_admin/graphql/resource_query_proxy.rb
|
|
386
414
|
- lib/active_admin/graphql/resource_query_proxy/controller.rb
|
|
415
|
+
- lib/active_admin/graphql/resource_query_proxy/identity.rb
|
|
387
416
|
- lib/active_admin/graphql/run_action_mutation_config.rb
|
|
388
417
|
- lib/active_admin/graphql/run_action_mutation_dsl.rb
|
|
389
418
|
- lib/active_admin/graphql/run_action_payload.rb
|