activeadmin-graphql 0.3.1 → 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 +5 -0
- data/docs/changelogs/20261010101500_nonconventional-id-fields.md +16 -2
- data/docs/changelogs/20261010103800_release-0-3-1.md +9 -2
- data/docs/changelogs/20261010110600_graphql-identity-dsl.md +34 -0
- data/docs/changelogs/20261010111800_release-0-3-2.md +46 -0
- data/docs/graphql-api.md +42 -12
- 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/lib/active_admin/graphql/policy_set_cache.rb +8 -3
- 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 +4 -4
- data/lib/active_admin/graphql/resource_query_proxy.rb +5 -4
- 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 +9 -3
- data/lib/active_admin/graphql/version.rb +1 -1
- data/lib/active_admin/graphql.rb +1 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 95e56a00a0c78ea55f44c186012af72c1ba6ff80ada2adbf905ce9b2d190c47d
|
|
4
|
+
data.tar.gz: 1e1dc89e1ffd41d05c6f2962a83857188a239ba677e6f492dbc304a09bb123b9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 02533503de4c86355e28bc7212115b043a9d7da293db40d1134251f7892539fca8257d6c03140c110d12da8e4de1b4112668c7e6fc2d721273c990d9fff930e3
|
|
7
|
+
data.tar.gz: 6e13f9a69b6cc7df900cf9883ab60600be21bfea50196209c7324306030d74784cb3e66082ffc3ec225192c3c0c385fffb3f24840061e9f12415b60479de9d3c
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.2 (2026-10-10)
|
|
6
|
+
|
|
7
|
+
- Add `graphql { identity ... }` so keyless views can declare GraphQL id columns, optional `separator`, and `encode` / `decode` (RFC 0007).
|
|
8
|
+
- Fail schema build when a registered model has neither a primary key nor an `id` column and no `identity` declaration; set `self.primary_key`, use `identity`, or `graphql { disable! }`.
|
|
9
|
+
|
|
5
10
|
## 0.3.1 (2026-10-10)
|
|
6
11
|
|
|
7
12
|
- Fix GraphQL identity handling for database views and nonconventional primary keys: keep one `id` field, support inferred `id` lookups, reject ambiguous identifiers, and honor attribute filters.
|
|
@@ -11,7 +11,10 @@ primary key is blank. Object types skip emitting a column field named `id`
|
|
|
11
11
|
(interface owns GraphQL `id`) and expose a single non-`id` primary-key column
|
|
12
12
|
as a readable scalar unless the resource's attribute configuration omits it.
|
|
13
13
|
Models with neither a primary key nor an `id` column fail schema build with an
|
|
14
|
-
`ArgumentError` that asks for `self.primary_key`.
|
|
14
|
+
`ArgumentError` that asks for `self.primary_key`. On 0.3.0 those models could
|
|
15
|
+
still enter the schema with a broken GraphQL `id`. Apps that keep such a
|
|
16
|
+
resource registered must set `self.primary_key` or use `graphql { disable! }`
|
|
17
|
+
so the rest of the namespace schema still builds. Inferred `id` values must be
|
|
15
18
|
unique and non-null; ambiguous member and batched lookups fail closed.
|
|
16
19
|
|
|
17
20
|
Member lookup uses the inferred column while preserving an explicitly
|
|
@@ -21,9 +24,19 @@ entries for distinct records when ActiveRecord has no declared primary key.
|
|
|
21
24
|
Dummy coverage includes SQLite views that project `code AS id` and
|
|
22
25
|
`id AS row_key`, plus a table with an `id` column and no primary key.
|
|
23
26
|
|
|
27
|
+
## Later pass 2026-10-10
|
|
28
|
+
|
|
29
|
+
Downstream check: a database view with neither primary key nor `id` column
|
|
30
|
+
could stay GraphQL-disabled via `graphql { disable! }`. Spec covers that
|
|
31
|
+
escape hatch next to the ArgumentError path.
|
|
32
|
+
|
|
33
|
+
Later the same day: RFC 0007 adds `graphql { identity ... }` so those views
|
|
34
|
+
can stay enabled when columns (and optional separator) are declared. See
|
|
35
|
+
docs/changelogs/20261010110600_graphql-identity-dsl.md.
|
|
36
|
+
|
|
24
37
|
## Next
|
|
25
38
|
|
|
26
|
-
|
|
39
|
+
Core fix shipped in 0.3.1. Identity DSL follow-on is 0.3.2.
|
|
27
40
|
|
|
28
41
|
## Source
|
|
29
42
|
|
|
@@ -32,3 +45,4 @@ Folded into 0.3.1 release metadata. Tag and gem push wait on make release.
|
|
|
32
45
|
`spec/unit/resource_query_proxy_identity_spec.rb`,
|
|
33
46
|
`spec/unit/policy_set_cache_spec.rb`
|
|
34
47
|
- Views: `spec/dummy/db/schema.rb` (`string_id_records`, `alternate_key_records`)
|
|
48
|
+
- Follow-on: docs/changelogs/20261010110600_graphql-identity-dsl.md
|
|
@@ -6,7 +6,11 @@ Andrei Makarov
|
|
|
6
6
|
|
|
7
7
|
## Decisions
|
|
8
8
|
|
|
9
|
-
Cut 0.3.1 after published 0.3.0.
|
|
9
|
+
Cut 0.3.1 after published 0.3.0. User-visible work: GraphQL identity for
|
|
10
|
+
database views and nonconventional primary keys; fail-closed when a model has
|
|
11
|
+
neither a primary key nor an `id` column without configuration; and
|
|
12
|
+
`graphql { identity ... }` (RFC 0007) for keyless views with a virtual
|
|
13
|
+
composite id.
|
|
10
14
|
|
|
11
15
|
CHANGELOG 0.3.1 dated 2026-10-10. Version constant is 0.3.1. Path gem pins in Gemfile.lock and appraisal lockfiles match.
|
|
12
16
|
|
|
@@ -24,9 +28,12 @@ PATH specs for activeadmin-graphql in Gemfile.lock, gemfiles/rails72.gemfile.loc
|
|
|
24
28
|
|
|
25
29
|
## Next
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
0.3.1 tagged and published on RubyGems. Post-tag identity DSL work ships in
|
|
32
|
+
0.3.2; see docs/changelogs/20261010111800_release-0-3-2.md.
|
|
28
33
|
|
|
29
34
|
## Source
|
|
30
35
|
|
|
31
36
|
docs/changelogs/20261010101500_nonconventional-id-fields.md
|
|
37
|
+
docs/changelogs/20261010110600_graphql-identity-dsl.md
|
|
38
|
+
rfcs/0007-graphql-identity.md
|
|
32
39
|
usr/bin/release.rb
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# GraphQL identity DSL (RFC 0007)
|
|
2
|
+
|
|
3
|
+
## Decisions
|
|
4
|
+
|
|
5
|
+
Ship `graphql { identity *columns, separator:, encode:, decode: }` so resources
|
|
6
|
+
without an ActiveRecord primary key and without an `id` column can stay in the
|
|
7
|
+
schema. Prefer resource configuration over forcing `self.primary_key`.
|
|
8
|
+
|
|
9
|
+
Separator form matches HTML admins that use a virtual `id` method such as
|
|
10
|
+
`[entry_type, entry_id, event_type].join(":")`. Without separator, multi-column
|
|
11
|
+
identity keeps the existing composite JSON string.
|
|
12
|
+
|
|
13
|
+
## Effects
|
|
14
|
+
|
|
15
|
+
Schema build, member lookup, mutations that resolve by where/id, and policy
|
|
16
|
+
cache keys go through `ActiveAdmin::GraphQL::ResourceIdentity` when identity is
|
|
17
|
+
configured. Ambiguous matches still fail closed.
|
|
18
|
+
|
|
19
|
+
Dummy coverage: view `alert_events` / model `AlertEvent` with virtual composite
|
|
20
|
+
`id`.
|
|
21
|
+
|
|
22
|
+
## Next
|
|
23
|
+
|
|
24
|
+
Folded into 0.3.2 release metadata (0.3.1 already published without this DSL).
|
|
25
|
+
Tag and gem push wait on make release.
|
|
26
|
+
|
|
27
|
+
## Source
|
|
28
|
+
|
|
29
|
+
rfcs/0007-graphql-identity.md
|
|
30
|
+
docs/changelogs/20261010101500_nonconventional-id-fields.md
|
|
31
|
+
docs/issues/20261010111159_rails-composite-primary-keys-alignment.md
|
|
32
|
+
https://guides.rubyonrails.org/active_record_composite_primary_keys.html
|
|
33
|
+
spec/unit/resource_identity_spec.rb
|
|
34
|
+
spec/requests/graphql_identity_dsl_spec.rb
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Release 0.3.2 metadata
|
|
2
|
+
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
Andrei Makarov
|
|
6
|
+
|
|
7
|
+
## Decisions
|
|
8
|
+
|
|
9
|
+
Cut 0.3.2 after published 0.3.1. User-visible work since the 0.3.1 tag:
|
|
10
|
+
`graphql { identity ... }` (RFC 0007) for keyless views, and schema-build
|
|
11
|
+
error text that names `identity` alongside `self.primary_key` and `disable!`.
|
|
12
|
+
|
|
13
|
+
CHANGELOG 0.3.1 restored to the published one-bullet text. Identity DSL bullets
|
|
14
|
+
live under 0.3.2 dated 2026-10-10.
|
|
15
|
+
|
|
16
|
+
Version constant is 0.3.2. Path gem pins in Gemfile.lock and appraisal
|
|
17
|
+
lockfiles match.
|
|
18
|
+
|
|
19
|
+
Docs-only notes (Rails composite PK alignment, partitions/shards unsupported,
|
|
20
|
+
federation unsupported) stay out of CHANGELOG.md.
|
|
21
|
+
|
|
22
|
+
make release (usr/bin/release.rb) still owns gem build, gem push, git tag, and
|
|
23
|
+
GitHub release. This pass only prepares metadata.
|
|
24
|
+
|
|
25
|
+
## Effects
|
|
26
|
+
|
|
27
|
+
lib/active_admin/graphql/version.rb VERSION is 0.3.2.
|
|
28
|
+
|
|
29
|
+
CHANGELOG identity DSL bullets moved from the post-tag 0.3.1 draft under 0.3.2.
|
|
30
|
+
|
|
31
|
+
PATH specs for activeadmin-graphql in Gemfile.lock,
|
|
32
|
+
gemfiles/rails72.gemfile.lock, gemfiles/rails8ruby4.gemfile.lock, and
|
|
33
|
+
gemfiles/rails8ruby34.gemfile.lock are 0.3.2.
|
|
34
|
+
|
|
35
|
+
## Next
|
|
36
|
+
|
|
37
|
+
Run make release from a clean tree when ready to push the gem and tag 0.3.2.
|
|
38
|
+
|
|
39
|
+
## Source
|
|
40
|
+
|
|
41
|
+
docs/changelogs/20261010110600_graphql-identity-dsl.md
|
|
42
|
+
rfcs/0007-graphql-identity.md
|
|
43
|
+
docs/issues/20261010111159_rails-composite-primary-keys-alignment.md
|
|
44
|
+
docs/issues/20261010111300_partitions-and-shards-support.md
|
|
45
|
+
docs/issues/20261010111500_graphql-federation-support.md
|
|
46
|
+
usr/bin/release.rb
|
data/docs/graphql-api.md
CHANGED
|
@@ -229,12 +229,28 @@ GraphQL `id` always comes from `ActiveAdminResource` (`ID!`). An ActiveRecord
|
|
|
229
229
|
`id` column is never added as a second field. When `self.primary_key` is blank
|
|
230
230
|
but the model still has an `id` column (common for database views that project
|
|
231
231
|
`… AS id`), that column supplies the GraphQL `id` value. When the model has
|
|
232
|
-
neither a primary key nor an `id` column,
|
|
233
|
-
`self.primary_key` on the model
|
|
234
|
-
single-column primary key named something other
|
|
235
|
-
its own readable field beside `id`, unless `only`
|
|
236
|
-
inferred `id` column must contain a unique, non-null
|
|
237
|
-
ambiguous member and batched lookups fail instead of
|
|
232
|
+
neither a primary key nor an `id` column, declare GraphQL identity on the
|
|
233
|
+
resource (RFC 0007), set `self.primary_key` on the model, or omit the resource
|
|
234
|
+
with `graphql { disable! }`. A single-column primary key named something other
|
|
235
|
+
than `id` is also exposed as its own readable field beside `id`, unless `only`
|
|
236
|
+
or `exclude` omits it. An inferred `id` column must contain a unique, non-null
|
|
237
|
+
value for every row; ambiguous member and batched lookups fail instead of
|
|
238
|
+
selecting one row.
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
# View with no PK and no id column; virtual identity matches HTML admin.
|
|
242
|
+
ActiveAdmin.register AlertEvent do
|
|
243
|
+
graphql do
|
|
244
|
+
identity :entry_type, :entry_id, :event_type, separator: ":"
|
|
245
|
+
# optional: encode: ->(record) { record.id }, decode: ->(id) { ... }
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
With `separator: ":"`, GraphQL `id` is `entry_type:entry_id:event_type`. Keep
|
|
251
|
+
the model’s `def id` (if any) in the same shape so HTML and GraphQL share
|
|
252
|
+
member ids. Without `separator`, multi-column identity uses the same JSON
|
|
253
|
+
object string as composite ActiveRecord primary keys.
|
|
238
254
|
|
|
239
255
|
### Composite primary keys (Rails 7.1+)
|
|
240
256
|
|
|
@@ -242,8 +258,10 @@ For models with `self.primary_key = [:book_code, :seq]` (and no separate `id`
|
|
|
242
258
|
column), Active Admin treats GraphQL as follows:
|
|
243
259
|
|
|
244
260
|
* `id` on the object type — JSON object string with all key columns in
|
|
245
|
-
model order
|
|
246
|
-
|
|
261
|
+
model order, for example `{"book_code":"CPK","seq":7}`. This is the GraphQL
|
|
262
|
+
wire format. It is not the Rails form/URL underscore packing described in
|
|
263
|
+
the Composite Primary Keys guide (`/books/2_25` with
|
|
264
|
+
`params.extract_value(:id)`).
|
|
247
265
|
* Readable fields — each primary-key column is also exposed as its own field
|
|
248
266
|
(in addition to `id`) so clients can load scalars without parsing JSON.
|
|
249
267
|
* `LibraryEditionWhereInput` (and similar) — either optional `id` (that JSON
|
|
@@ -255,12 +273,20 @@ column), Active Admin treats GraphQL as follows:
|
|
|
255
273
|
inputs (unlike single `id` tables, where the key is omitted from assignable
|
|
256
274
|
attributes).
|
|
257
275
|
|
|
258
|
-
For REST/HTML member routes, `ResourceController#find_resource` must resolve
|
|
276
|
+
For REST/HTML member routes, `ResourceController#find_resource` must resolve
|
|
277
|
+
composite ids from `params[:id]` or from individual primary-key params. Stock
|
|
278
|
+
ActiveAdmin 3.x only passes a single `params[:id]` into `find`; an ActiveAdmin
|
|
279
|
+
fork (or patch) that implements composite-aware `find_resource` keeps the
|
|
280
|
+
admin UI aligned with GraphQL. After decoding the GraphQL JSON `id`, the gem
|
|
281
|
+
uses Rails’ array/tuple conventions for `Relation#find` and
|
|
282
|
+
`where(primary_key => [tuple, …])` as in the guide.
|
|
259
283
|
|
|
260
|
-
The gem ships `ActiveAdmin::PrimaryKey` when the core constant is missing
|
|
284
|
+
The gem ships `ActiveAdmin::PrimaryKey` when the core constant is missing
|
|
285
|
+
(upstream). A fork may define `PrimaryKey` and extended `find_resource` in
|
|
286
|
+
`ResourceController` instead.
|
|
261
287
|
|
|
262
|
-
|
|
263
|
-
|
|
288
|
+
Alignment notes versus the Rails guide:
|
|
289
|
+
`docs/issues/20261010111159_rails-composite-primary-keys-alignment.md`.
|
|
264
290
|
|
|
265
291
|
### Collection query arguments
|
|
266
292
|
|
|
@@ -359,6 +385,7 @@ Inside `ActiveAdmin.register` you can narrow the GraphQL surface:
|
|
|
359
385
|
ActiveAdmin.register Post do
|
|
360
386
|
graphql do
|
|
361
387
|
disable! # omit this resource from the schema
|
|
388
|
+
# identity :entry_type, :entry_id, :event_type, separator: ":" # RFC 0007
|
|
362
389
|
type_name "BlogPost" # GraphQL object / mutation type basename (`graphql_name` alias)
|
|
363
390
|
only :title, :body, :published_at # expose only these attributes on queries
|
|
364
391
|
exclude :internal_score # or `except` / `exclude`
|
|
@@ -379,6 +406,9 @@ end
|
|
|
379
406
|
```
|
|
380
407
|
|
|
381
408
|
* `disable!` — resource is not included in Query or Mutation.
|
|
409
|
+
* `identity` — declare GraphQL id columns (and optional `separator`, `encode`,
|
|
410
|
+
`decode`) when the model has no ActiveRecord primary key and no `id` column
|
|
411
|
+
(RFC 0007).
|
|
382
412
|
* `type_name` (or `graphql_name`) — overrides the default GraphQL type name
|
|
383
413
|
derived from the model (object types, mutations, and Rails enum GraphQL
|
|
384
414
|
types use this basename).
|
|
@@ -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
|
|
@@ -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,18 @@ 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
|
-
identity =
|
|
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
|
|
57
62
|
identity = "#{identity}:object:#{subject.object_id}" if subject.class.primary_key.blank?
|
|
58
63
|
"record:#{subject.class.name}:#{identity}"
|
|
59
64
|
else
|
|
@@ -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
|
|
@@ -6,11 +6,11 @@ module ActiveAdmin
|
|
|
6
6
|
module Identity
|
|
7
7
|
private
|
|
8
8
|
|
|
9
|
-
def
|
|
10
|
-
|
|
11
|
-
raise ActiveRecord::RecordNotFound
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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)
|
|
@@ -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,14 +82,20 @@ 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
86
|
if pk_cols.empty?
|
|
87
87
|
raise ArgumentError,
|
|
88
|
-
"#{model.name} has no primary key for GraphQL `id`; set self.primary_key on the model"
|
|
88
|
+
"#{model.name} has no primary key for GraphQL `id`; set self.primary_key on the model " \
|
|
89
|
+
"or graphql { identity ... }"
|
|
89
90
|
end
|
|
90
91
|
|
|
91
92
|
type_class.field :id, ::GraphQL::Types::ID, null: false, authorize: false
|
|
92
|
-
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
|
|
93
99
|
|
|
94
100
|
# ActiveAdmin omits a single-column primary key from resource_attributes; still expose
|
|
95
101
|
# non-id key columns as scalars (composite keys already remain in attributes).
|
data/lib/active_admin/graphql.rb
CHANGED
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
|
|
@@ -376,6 +376,8 @@ files:
|
|
|
376
376
|
- docs/changelogs/20260921120000_graphql-floor-and-activeadmin-4.md
|
|
377
377
|
- docs/changelogs/20261010101500_nonconventional-id-fields.md
|
|
378
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
|
|
379
381
|
- docs/dependencies/20260907141000_json-cve-2026-71847.md
|
|
380
382
|
- docs/dependencies/20260907141000_sqlite3-ghsa-mwm8-39rw-8826.md
|
|
381
383
|
- docs/dependencies/20260921114000_rails-json-parse-kwargs.md
|
|
@@ -388,6 +390,9 @@ files:
|
|
|
388
390
|
- docs/issues/20260921104400_convention-alignment-audit.md
|
|
389
391
|
- docs/issues/20260921114500_dependency-audit.md
|
|
390
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
|
|
391
396
|
- docs/plan/20260817201500_rfc-starter.md
|
|
392
397
|
- lib/active_admin/graphql.rb
|
|
393
398
|
- lib/active_admin/graphql/auth_context.rb
|
|
@@ -402,6 +407,7 @@ files:
|
|
|
402
407
|
- lib/active_admin/graphql/record_source.rb
|
|
403
408
|
- lib/active_admin/graphql/resource_config.rb
|
|
404
409
|
- lib/active_admin/graphql/resource_definition_dsl.rb
|
|
410
|
+
- lib/active_admin/graphql/resource_identity.rb
|
|
405
411
|
- lib/active_admin/graphql/resource_interface.rb
|
|
406
412
|
- lib/active_admin/graphql/resource_methods.rb
|
|
407
413
|
- lib/active_admin/graphql/resource_query_proxy.rb
|