activeadmin-graphql 0.3.3 → 0.4.1
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 +31 -3
- data/README.md +2 -2
- data/docs/changelogs/20260818111600_rfc-0002-problems.md +1 -1
- data/docs/changelogs/20260818114000_rfc-as-built-coverage.md +1 -1
- data/docs/changelogs/20260818114700_rfc-design-not-version.md +1 -1
- data/docs/changelogs/20260907141000_advisory-lockfile-refresh.md +1 -1
- data/docs/changelogs/20260921103000_mutation-attribute-allowlists.md +1 -1
- data/docs/changelogs/20260921111000_release-0-3-0.md +1 -1
- data/docs/changelogs/20260921112800_drop-standard-rubocop-direct.md +1 -1
- data/docs/changelogs/20260921120000_graphql-floor-and-activeadmin-4.md +1 -1
- data/docs/changelogs/20261010091415_graphql-resource-enabled-default.md +1 -1
- data/docs/changelogs/20261010103800_release-0-3-1.md +1 -1
- data/docs/changelogs/20261010110600_graphql-identity-dsl.md +4 -0
- data/docs/changelogs/20261010111800_release-0-3-2.md +1 -1
- data/docs/changelogs/20261012050051_graphql-surface-toggles.md +1 -1
- data/docs/changelogs/20261012064500_graphql-capabilities.md +1 -1
- data/docs/changelogs/20261012113500_release-0-3-3.md +1 -1
- data/docs/changelogs/20261012200000_host-customization-hooks.md +31 -0
- data/docs/changelogs/20261013080000_release-0-4-0.md +48 -0
- data/docs/changelogs/20261103145000_release-0-4-1.md +26 -0
- data/docs/graphql-api.md +154 -102
- data/docs/issues/20260907141000_engineering-and-dependency-audit.md +1 -1
- data/docs/issues/20260917123253_ci-drop-automatic-dependency-audit.md +1 -1
- data/docs/issues/20260921103000_mutation-attribute-allowlists.md +1 -1
- data/docs/issues/20260921104400_convention-alignment-audit.md +7 -7
- data/docs/issues/20260921114500_dependency-audit.md +13 -13
- data/docs/issues/20260921115500_file-length-metrics.md +1 -1
- data/docs/issues/20261010111159_rails-composite-primary-keys-alignment.md +13 -13
- data/docs/issues/20261010111300_partitions-and-shards-support.md +3 -3
- data/docs/issues/20261010111500_graphql-federation-support.md +3 -3
- data/docs/issues/20261012200000_host-customization-gaps.md +30 -0
- data/docs/issues/20261103145000_engineering-prose-release-audit.md +39 -0
- data/docs/plan/20260817201500_rfc-starter.md +1 -1
- data/docs/reviews/20261028_engineering-audit-release-preparation.md +24 -0
- data/docs/reviews/20261103145000_engineering-prose-release-audit.md +38 -0
- data/lib/active_admin/graphql/capability_definition_dsl.rb +9 -4
- data/lib/active_admin/graphql/collection_query_policy.rb +103 -0
- data/lib/active_admin/graphql/integration.rb +24 -0
- data/lib/active_admin/graphql/member_action_capability.rb +14 -0
- data/lib/active_admin/graphql/member_action_dry_run.rb +121 -0
- data/lib/active_admin/graphql/resource_capability_dsl.rb +4 -3
- data/lib/active_admin/graphql/resource_capability_maps.rb +20 -2
- data/lib/active_admin/graphql/resource_definition_dsl.rb +17 -6
- data/lib/active_admin/graphql/run_action_capability.rb +62 -0
- data/lib/active_admin/graphql/run_action_control_extension.rb +72 -0
- data/lib/active_admin/graphql/run_action_field.rb +103 -0
- data/lib/active_admin/graphql/run_action_mutation_config.rb +8 -0
- data/lib/active_admin/graphql/run_action_mutation_dsl.rb +18 -0
- data/lib/active_admin/graphql/run_action_preview.rb +20 -0
- data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +2 -2
- data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +4 -3
- data/lib/active_admin/graphql/schema_builder/mutation_create.rb +20 -20
- data/lib/active_admin/graphql/schema_builder/mutation_member.rb +8 -3
- data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +2 -4
- data/lib/active_admin/graphql/schema_builder/query_type_member.rb +1 -4
- data/lib/active_admin/graphql/schema_builder/query_type_registered.rb +1 -4
- data/lib/active_admin/graphql/schema_builder/resolvers.rb +46 -7
- data/lib/active_admin/graphql/schema_builder/types_object.rb +2 -1
- data/lib/active_admin/graphql/schema_builder/wire.rb +2 -1
- data/lib/active_admin/graphql/schema_field.rb +85 -10
- data/lib/active_admin/graphql/version.rb +1 -1
- data/lib/active_admin/graphql.rb +7 -0
- metadata +15 -1
|
@@ -4,7 +4,7 @@ Full audit: recon, security, freshness, ecosystem. Depth reached: full for hot-p
|
|
|
4
4
|
|
|
5
5
|
## Participants
|
|
6
6
|
|
|
7
|
-
Andrei Makarov
|
|
7
|
+
Andrei Makarov (@amkisko)
|
|
8
8
|
|
|
9
9
|
## Decisions
|
|
10
10
|
|
|
@@ -20,18 +20,6 @@ Stay on ActiveAdmin 3.5.2. It is the latest stable on RubyGems. 4.0.0.beta23 (20
|
|
|
20
20
|
|
|
21
21
|
Do not reverse the 2026-09-17 choice to keep bundle-audit off test.yml. The on-demand workflow remains the gate. Makefile still has no audit target that mirrors that workflow.
|
|
22
22
|
|
|
23
|
-
## Effects
|
|
24
|
-
|
|
25
|
-
ruby-advisory-db update: 1245 advisories, last updated 2026-09-16 10:35:37 -0400, commit 44784c295391577f25d198a9205eae4ba73ec4da.
|
|
26
|
-
|
|
27
|
-
bundle-audit check failed on all four lockfiles with GHSA-rmxg-5p3r-j6hh (graphql 2.6.7, solution >= 2.6.9). No other scanner matches.
|
|
28
|
-
|
|
29
|
-
bundle outdated --strict on the root Gemfile listed 16 outdated specs. graphql 2.6.7 vs 2.6.10 is the only outdated hot-path direct runtime gem.
|
|
30
|
-
|
|
31
|
-
Libyears from Gemfile.lock vs RubyGems stable (compact-graph band: about 10 low, about 100 medium): 124 locked packages, 27 outdated, total lag 17.94 years, average 0.145 years per package, 6 packages one major behind (arbre, inherited_resources, json, marcel, diff-lcs, rubocop-capybara). Coverage floor in config/polyrun_coverage.yml is 85 percent line. No upgrade spike was run this pass.
|
|
32
|
-
|
|
33
|
-
No person-facing surface in this pass.
|
|
34
|
-
|
|
35
23
|
## Findings
|
|
36
24
|
|
|
37
25
|
Tier hot path direct. Kind security. Severity high. Confidence high. Package graphql 2.6.7. Why it matters: Marshal.load of unauthenticated parser cache files is RCE when parse_file runs. Evidence kind observed (bundle-audit, GHSA text, repo grep with no parse_file). Sources: GHSA-rmxg-5p3r-j6hh, ruby-advisory-db. Smallest fix: lock 2.6.10 and gemspec >= 2.6.9. Deeper fix: none. Status: CI graphs affected; this library execute path not_affected. Disposition: upgrade. Next: apply when confirmed.
|
|
@@ -52,6 +40,18 @@ Tier dev/test. Kind freshness. Severity low. Confidence high. json 2.21.2 vs 3.0
|
|
|
52
40
|
|
|
53
41
|
Tier automation. Kind automation gap. Severity low. Confidence high. Why it matters: Dependabot bundler directory / does not refresh gemfiles/*.gemfile.lock. scorecard.yml still pins actions/checkout 9c091bb (v7.0.0) while test.yml uses 3d3c42e5 (v7.0.1). ossf/scorecard-action 4eaacf05 is v2.4.3; v2.4.4 exists. mschilde/auto-label-merge-conflicts is commented as master. Makefile has no audit target. Evidence kind observed (workflow files, dependabot.yml). Smallest fix: optional action SHA refresh via Dependabot; keep appraisal locks in the on-demand audit. Do not put advisory scanners back on test.yml.
|
|
54
42
|
|
|
43
|
+
## Effects
|
|
44
|
+
|
|
45
|
+
ruby-advisory-db update: 1245 advisories, last updated 2026-09-16 10:35:37 -0400, commit 44784c295391577f25d198a9205eae4ba73ec4da.
|
|
46
|
+
|
|
47
|
+
bundle-audit check failed on all four lockfiles with GHSA-rmxg-5p3r-j6hh (graphql 2.6.7, solution >= 2.6.9). No other scanner matches.
|
|
48
|
+
|
|
49
|
+
bundle outdated --strict on the root Gemfile listed 16 outdated specs. graphql 2.6.7 vs 2.6.10 is the only outdated hot-path direct runtime gem.
|
|
50
|
+
|
|
51
|
+
Libyears from Gemfile.lock vs RubyGems stable (compact-graph band: about 10 low, about 100 medium): 124 locked packages, 27 outdated, total lag 17.94 years, average 0.145 years per package, 6 packages one major behind (arbre, inherited_resources, json, marcel, diff-lcs, rubocop-capybara). Coverage floor in config/polyrun_coverage.yml is 85 percent line. No upgrade spike was run this pass.
|
|
52
|
+
|
|
53
|
+
No person-facing surface in this pass.
|
|
54
|
+
|
|
55
55
|
## Next
|
|
56
56
|
|
|
57
57
|
Wait for a Rails release that includes rails/rails#58601. Then drop the development json < 3 pin and bundle update json. Leave marcel 2 and diff-lcs 2 alone until those parents move.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Participants
|
|
4
4
|
|
|
5
|
-
Andrei Makarov
|
|
5
|
+
Andrei Makarov (@amkisko)
|
|
6
6
|
|
|
7
7
|
## Decisions
|
|
8
8
|
|
|
@@ -16,28 +16,28 @@ after shipping inferred id columns and RFC 0007 identity DSL.
|
|
|
16
16
|
|
|
17
17
|
## Effects
|
|
18
18
|
|
|
19
|
-
Compared gem behavior to the guide
|
|
19
|
+
Compared gem behavior to the guide's model, query, form, and parameter rules.
|
|
20
20
|
|
|
21
21
|
ActiveRecord composite primary key (self.primary_key = array):
|
|
22
22
|
|
|
23
|
-
- Aligns: model declares composite key the same way as the guide
|
|
23
|
+
- Aligns: model declares composite key the same way as the guide's
|
|
24
24
|
`self.primary_key = [:store_id, :sku]` style. Dummy `LibraryEdition` uses
|
|
25
25
|
`[:book_code, :seq]`.
|
|
26
26
|
- Aligns: internal batch and find paths convert that string id into an ordered
|
|
27
27
|
value list and use Rails tuple/`Relation#find` style lookups
|
|
28
28
|
(`ActiveAdmin::PrimaryKey.dataloader_tuple`, `ResourceQueryProxy` composite
|
|
29
|
-
branch). That matches the guide
|
|
29
|
+
branch). That matches the guide's `Product.find([3, "XYZ12345"])` and
|
|
30
30
|
`where(primary_key => [[...], [...]])` idea at the Ruby API layer.
|
|
31
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
|
|
32
|
+
object string (`{"book_code":"CPK","seq":7}`), not as the guide's form/URL
|
|
33
33
|
underscore packing (`/books/2_25` with `params.extract_value(:id)`). JSON
|
|
34
34
|
keeps column names explicit and avoids ambiguity when a key value contains
|
|
35
35
|
`_`. Underscore packing is not accepted as GraphQL `id` today.
|
|
36
36
|
- Diverges: GraphQL also accepts per-key arguments and `WhereInput` fields for
|
|
37
|
-
composite models. The guide
|
|
37
|
+
composite models. The guide's controller example extracts one `:id` param
|
|
38
38
|
into an array; we keep named keys as a GraphQL-friendly alternate.
|
|
39
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
|
|
40
|
+
models can mean an `:id` attribute, not the full primary key. The gem's
|
|
41
41
|
GraphQL `id` field is the ActiveAdminResource identity string, not
|
|
42
42
|
necessarily an ActiveRecord `:id` column. Member lookup uses primary-key
|
|
43
43
|
columns (or configured identity), not a bare `where(id: graphql_id)` on
|
|
@@ -45,15 +45,15 @@ ActiveRecord composite primary key (self.primary_key = array):
|
|
|
45
45
|
|
|
46
46
|
Keyless views and RFC 0007 `graphql { identity ... }`:
|
|
47
47
|
|
|
48
|
-
- Outside the guide
|
|
48
|
+
- Outside the guide's scope. The guide assumes a declared composite primary
|
|
49
49
|
key on the table/model. AlertEvent-style views with `primary_key = nil` and
|
|
50
50
|
a virtual Ruby `id` are not Rails composite primary keys.
|
|
51
51
|
- `identity :a, :b, :c, separator: ":"` is an application-level encoding so
|
|
52
52
|
GraphQL can share the same string as a hand-rolled `def id; [...].join(":")`.
|
|
53
|
-
That separator form is closer in spirit to Rails
|
|
53
|
+
That separator form is closer in spirit to Rails' delimited URL id than our
|
|
54
54
|
JSON composite encoding, but the delimiter and segment rules are app-chosen,
|
|
55
|
-
not `extract_value
|
|
56
|
-
- Without `separator`, configured multi-column identity reuses the gem
|
|
55
|
+
not `extract_value`'s underscore default.
|
|
56
|
+
- Without `separator`, configured multi-column identity reuses the gem's JSON
|
|
57
57
|
object string so keyless and AR-composite GraphQL ids stay one family.
|
|
58
58
|
|
|
59
59
|
Stock ActiveAdmin HTML member routes still need a composite-aware
|
|
@@ -64,8 +64,8 @@ make Rails form URLs use JSON.
|
|
|
64
64
|
|
|
65
65
|
Leave GraphQL composite `id` as JSON for AR composite primary keys unless a
|
|
66
66
|
later RFC deliberately adds underscore (or `extract_value`-compatible) accept
|
|
67
|
-
and emit. Document that choice next to the Rails guide, not as
|
|
68
|
-
format Rails uses for casting composite ids
|
|
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
69
|
|
|
70
70
|
No change required for RFC 0007 separator identity; it is the escape hatch
|
|
71
71
|
when the model is not a Rails composite primary key.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Participants
|
|
4
4
|
|
|
5
|
-
Andrei Makarov
|
|
5
|
+
Andrei Makarov (@amkisko)
|
|
6
6
|
|
|
7
7
|
## Decisions
|
|
8
8
|
|
|
@@ -23,7 +23,7 @@ as a product feature and untested in this gem.
|
|
|
23
23
|
Searched lib, docs, rfcs, and spec for partition, shard, connects_to,
|
|
24
24
|
multiple database, tenant routing, writing_role, and reading_role.
|
|
25
25
|
|
|
26
|
-
Product code has no shard or partition API. Hits for
|
|
26
|
+
Product code has no shard or partition API. Hits for "shard" and "partition"
|
|
27
27
|
are Polyrun/CI test parallelism and agent IO-fault vocabulary, not databases.
|
|
28
28
|
|
|
29
29
|
Dummy app uses one SQLite database (`spec/dummy/config/database.yml`). No
|
|
@@ -41,7 +41,7 @@ the Rails/ActiveAdmin request boundary (middleware, `connected_to`, custom
|
|
|
41
41
|
finder). Do not invent gem DSL until a concrete host failure and RFC name the
|
|
42
42
|
contract (shard key in GraphQL args, role switching, cross-shard batch limits).
|
|
43
43
|
|
|
44
|
-
Optional later: document
|
|
44
|
+
Optional later: document "unsupported / inherited only" next to identity in
|
|
45
45
|
`docs/graphql-api.md` when a reader asks again.
|
|
46
46
|
|
|
47
47
|
## Source
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Participants
|
|
4
4
|
|
|
5
|
-
Andrei Makarov
|
|
5
|
+
Andrei Makarov (@amkisko)
|
|
6
6
|
|
|
7
7
|
## Decisions
|
|
8
8
|
|
|
@@ -34,8 +34,8 @@ tested here.
|
|
|
34
34
|
## Next
|
|
35
35
|
|
|
36
36
|
Keep federation out of scope until a host names a concrete subgraph contract
|
|
37
|
-
and an RFC. Prefer documenting
|
|
38
|
-
unsupported
|
|
37
|
+
and an RFC. Prefer documenting "single-schema admin API; federation
|
|
38
|
+
unsupported" if readers keep asking.
|
|
39
39
|
|
|
40
40
|
## Source
|
|
41
41
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Host customization gaps
|
|
2
|
+
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
Andrei Makarov (@amkisko)
|
|
6
|
+
|
|
7
|
+
## Decisions
|
|
8
|
+
|
|
9
|
+
The gem owns mechanics: when a field is visible, null, or omitted; dry-run
|
|
10
|
+
argument wiring; filter-key policy hooks; default member-action capability;
|
|
11
|
+
confirm/dry_run injection on custom fields. Hosts own product grants, domain
|
|
12
|
+
preview bodies, and audit storage.
|
|
13
|
+
|
|
14
|
+
Capabilities remain grant names in context[:capabilities]. Named visibility
|
|
15
|
+
profiles stay incompatible with graphql_capabilities.
|
|
16
|
+
|
|
17
|
+
## Effects
|
|
18
|
+
|
|
19
|
+
RFCs 0009-0013 specify the contract. Implementation landed on the feature
|
|
20
|
+
branch with request coverage for each hook.
|
|
21
|
+
|
|
22
|
+
## Next
|
|
23
|
+
|
|
24
|
+
Publish a release. Hosts migrate off local prepends in the order: capability
|
|
25
|
+
null mode and dry-run DSL, then collection policy, then field_visible and
|
|
26
|
+
capability defaults, then confirm helpers.
|
|
27
|
+
|
|
28
|
+
## Source
|
|
29
|
+
|
|
30
|
+
RFCs 0009-0013. docs/changelogs/20261012200000_host-customization-hooks.md.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Engineering, prose, and release audit
|
|
2
|
+
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
Andrei Makarov (@amkisko)
|
|
6
|
+
|
|
7
|
+
## Decisions
|
|
8
|
+
|
|
9
|
+
- Keep version 0.4.1. Fold audit fixes into the unpublished cut and date the product changelog 2026-11-03.
|
|
10
|
+
- Treat member and registered_resource instance-read denials as missing (null), and update/destroy instance denials as not found, so clients cannot enumerate ids from the error message.
|
|
11
|
+
- Wrap resolve_create hooks in a nested transaction and run instance create authorization before commit.
|
|
12
|
+
- Keep release confirmation EOF as rejection, require a CHANGELOG version heading, and run tests on pull requests to main.
|
|
13
|
+
- Leave policy run-action over-reporting, composite find_members N+1, RecordSource unscoped load, and default-open collection filter policy as follow-up work (medium severity, not cut into 0.4.1).
|
|
14
|
+
|
|
15
|
+
## Effects
|
|
16
|
+
|
|
17
|
+
Pipeline: GraphQL HTTP ingress -> schema -> resolve -> auth/policy -> ActiveRecord -> JSON egress. Queue, worker, learned-system, and physical plant modes skipped (absent). Resource and identification: gem execute log keeps namespace, operation name, and error count only.
|
|
18
|
+
|
|
19
|
+
Ranked fixes applied:
|
|
20
|
+
|
|
21
|
+
1. Member and registered_resource instance-read deny returns null (high, observed).
|
|
22
|
+
2. Update and destroy instance deny returns not found (high, observed).
|
|
23
|
+
3. resolve_create hook deny rolls back nested transaction (high, observed).
|
|
24
|
+
4. Release changelog heading specs and EOF confirmation coverage (medium, observed).
|
|
25
|
+
5. ASCII punctuation in README, CHANGELOG, and graphql-api guide (house prose).
|
|
26
|
+
6. Docs timestamp trees: Participants handle, Source after Later pass sections.
|
|
27
|
+
|
|
28
|
+
Checks: rubocop clean on touched files; rspec 197 examples, 0 failures; gem 0.4.1 builds and verifies.
|
|
29
|
+
|
|
30
|
+
## Next
|
|
31
|
+
|
|
32
|
+
- Commit the worktree, then run make release for tag and gem push.
|
|
33
|
+
- Follow-up: filter PolicyAdapter run-action lists by per-action authorized?; batch composite find_members; scope RecordSource loads; optional default collection query policy.
|
|
34
|
+
|
|
35
|
+
## Source
|
|
36
|
+
|
|
37
|
+
docs/reviews/20261103145000_engineering-prose-release-audit.md
|
|
38
|
+
CHANGELOG.md section 0.4.1 (2026-11-03)
|
|
39
|
+
docs/reviews/20261028_engineering-audit-release-preparation.md
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Engineering audit and release preparation
|
|
2
|
+
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
Andrei Makarov (@amkisko)
|
|
6
|
+
|
|
7
|
+
## Decisions
|
|
8
|
+
|
|
9
|
+
- Treat end-of-input at the release confirmation prompt as rejection.
|
|
10
|
+
- Run tests for pull requests targeting `main`.
|
|
11
|
+
|
|
12
|
+
## Effects
|
|
13
|
+
|
|
14
|
+
- `make test`, RBS validation, RuboCop, and the sharded suite pass.
|
|
15
|
+
- The 0.4.1 gem builds and passes `Gem::Package#verify`.
|
|
16
|
+
- A local Rails and ActiveAdmin host cold load used 1.063 CPU seconds and 92,422,144 bytes peak RSS. These are single-machine observations, not release budgets.
|
|
17
|
+
|
|
18
|
+
## Next
|
|
19
|
+
|
|
20
|
+
- Review the worktree, commit it, and let the release workflow create the tag and publish the gem.
|
|
21
|
+
|
|
22
|
+
## Source
|
|
23
|
+
|
|
24
|
+
Iterative engineering audit on 2026-10-28.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Engineering and prose audit before 0.4.1 publish
|
|
2
|
+
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
Andrei Makarov (@amkisko)
|
|
6
|
+
|
|
7
|
+
## Decisions
|
|
8
|
+
|
|
9
|
+
Re-audit main for the unpublished 0.4.1 cut. Include member auth oracle shape, resolve_create transaction order, release confirmation and changelog gates, pull_request CI, and ASCII docs punctuation. Leave tag and RubyGems publish to make release after commit.
|
|
10
|
+
|
|
11
|
+
## Effects
|
|
12
|
+
|
|
13
|
+
Pipeline: POST graphql -> GraphqlController#execute -> schema_for -> resolve -> AuthContext / ResourceQueryProxy -> ActiveRecord -> JSON. Boundary mode covered class vs instance authorization messages. Resource and identification modes: no new person identifiers in gem logs. Queue, worker, learned-system, and physical plant modes skipped (absent).
|
|
14
|
+
|
|
15
|
+
Ranked fixes applied:
|
|
16
|
+
|
|
17
|
+
1. Null for denied member and registered_resource reads (high confidence, observed).
|
|
18
|
+
2. not found for denied update and destroy (high confidence, observed).
|
|
19
|
+
3. Nested transaction around resolve_create with instance authorize before commit (high confidence, observed).
|
|
20
|
+
4. Changelog heading and confirmation specs; CHANGELOG dated 2026-11-03.
|
|
21
|
+
5. ASCII punctuation pass on README, CHANGELOG, and docs/graphql-api.md.
|
|
22
|
+
|
|
23
|
+
Deferred (documented in the issue): PolicyAdapter run-action over-report, composite find_members N+1, RecordSource unscoped PK load, default-open collection q policy.
|
|
24
|
+
|
|
25
|
+
Checks observed in this session:
|
|
26
|
+
|
|
27
|
+
- bundle exec rubocop on touched Ruby files: 9 files, no offenses.
|
|
28
|
+
- bundle exec rspec: 197 examples, 0 failures (includes release_version_check and new auth examples).
|
|
29
|
+
- gem build activeadmin-graphql.gemspec and Gem::Package#verify for 0.4.1: ok.
|
|
30
|
+
|
|
31
|
+
## Next
|
|
32
|
+
|
|
33
|
+
Commit, then make release.
|
|
34
|
+
|
|
35
|
+
## Source
|
|
36
|
+
|
|
37
|
+
- docs/issues/20261103145000_engineering-prose-release-audit.md
|
|
38
|
+
- CHANGELOG.md section 0.4.1 (2026-11-03)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module ActiveAdmin
|
|
4
4
|
module GraphQL
|
|
5
|
-
# Block body for +capability :name do ... end+ (RFC 0008).
|
|
5
|
+
# Block body for +capability :name do ... end+ (RFC 0008, RFC 0009).
|
|
6
6
|
class CapabilityDefinitionDSL
|
|
7
7
|
QUERY_ALIASES = {
|
|
8
8
|
index: :collection,
|
|
@@ -14,13 +14,18 @@ module ActiveAdmin
|
|
|
14
14
|
delete: :destroy
|
|
15
15
|
}.freeze
|
|
16
16
|
|
|
17
|
-
def initialize(config, capability_name)
|
|
17
|
+
def initialize(config, capability_name, mode: nil)
|
|
18
18
|
@config = config
|
|
19
19
|
@capability_name = capability_name.to_sym
|
|
20
|
+
@mode = mode
|
|
20
21
|
end
|
|
21
22
|
|
|
22
|
-
def fields(*names)
|
|
23
|
-
@config.assign_field_capabilities!(
|
|
23
|
+
def fields(*names, mode: nil)
|
|
24
|
+
@config.assign_field_capabilities!(
|
|
25
|
+
@capability_name,
|
|
26
|
+
names,
|
|
27
|
+
mode: mode.nil? ? @mode : mode
|
|
28
|
+
)
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
def queries(*names)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Built-in helpers for +graphql_collection_query_policy+ (RFC 0011).
|
|
6
|
+
module CollectionQueryPolicy
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def require_visible_fields!(
|
|
10
|
+
context:,
|
|
11
|
+
aa_resource:,
|
|
12
|
+
q: nil,
|
|
13
|
+
order: nil,
|
|
14
|
+
allow_q_keys: [],
|
|
15
|
+
field_grants: {},
|
|
16
|
+
infer_field_grants: true
|
|
17
|
+
)
|
|
18
|
+
allow = Array(allow_q_keys).map(&:to_s)
|
|
19
|
+
grants = merge_field_grants(aa_resource, field_grants, infer: infer_field_grants)
|
|
20
|
+
|
|
21
|
+
normalize_q(q).each_key do |raw_key|
|
|
22
|
+
key = raw_key.to_s
|
|
23
|
+
next if allow.include?(key)
|
|
24
|
+
|
|
25
|
+
field_name = field_from_predicate_key(key)
|
|
26
|
+
unless field_name
|
|
27
|
+
raise ::GraphQL::ExecutionError, "unsupported collection filter key #{key.inspect}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
assert_field_filterable!(context, aa_resource, field_name, grants)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
assert_order_filterable!(context, aa_resource, order, grants) if order.present?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def merge_field_grants(aa_resource, field_grants, infer: true)
|
|
37
|
+
explicit = field_grants.to_h.transform_keys(&:to_s).transform_values(&:to_sym)
|
|
38
|
+
return explicit unless infer
|
|
39
|
+
|
|
40
|
+
inferred = aa_resource.graphql_config.field_capabilities.to_h do |name, capability|
|
|
41
|
+
[name.to_s, capability.to_sym]
|
|
42
|
+
end
|
|
43
|
+
inferred.merge(explicit)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def normalize_q(q)
|
|
47
|
+
return {} if q.nil?
|
|
48
|
+
return q.to_unsafe_h if q.respond_to?(:to_unsafe_h)
|
|
49
|
+
|
|
50
|
+
q.respond_to?(:to_h) ? q.to_h : {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def field_from_predicate_key(key)
|
|
54
|
+
predicates = ransack_predicate_names
|
|
55
|
+
predicates.sort_by(&:length).reverse_each do |predicate|
|
|
56
|
+
suffix = "_#{predicate}"
|
|
57
|
+
return key[0...-suffix.length] if key.end_with?(suffix) && key.length > suffix.length
|
|
58
|
+
end
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def ransack_predicate_names
|
|
63
|
+
if defined?(Ransack) && Ransack.respond_to?(:predicates)
|
|
64
|
+
Ransack.predicates.keys.map(&:to_s)
|
|
65
|
+
else
|
|
66
|
+
%w[eq cont start end gt lt gteq lteq in not_in null present blank matches does_not_match]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def assert_order_filterable!(context, aa_resource, order, grants)
|
|
71
|
+
column = order.to_s.sub(/_(asc|desc|ascs|descs)\z/i, "")
|
|
72
|
+
return if column.blank?
|
|
73
|
+
|
|
74
|
+
assert_field_filterable!(context, aa_resource, column, grants)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def assert_field_filterable!(context, aa_resource, field_name, grants)
|
|
78
|
+
unless field_schema_visible?(context, aa_resource, field_name)
|
|
79
|
+
raise ::GraphQL::ExecutionError, "not authorized to filter on #{field_name}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
capability = grants[field_name.to_s]
|
|
83
|
+
return if capability.nil?
|
|
84
|
+
return if Capability.granted?(context, capability)
|
|
85
|
+
|
|
86
|
+
raise ::GraphQL::ExecutionError, "not authorized to filter on #{field_name}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def field_schema_visible?(context, aa_resource, field_name)
|
|
90
|
+
type_name = graphql_type_name_for(aa_resource)
|
|
91
|
+
type = context.schema.get_type(type_name, context)
|
|
92
|
+
return false unless type
|
|
93
|
+
|
|
94
|
+
!context.schema.get_field(type, field_name.to_s, context).nil?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def graphql_type_name_for(aa_resource)
|
|
98
|
+
aa_resource.graphql_config.graphql_type_name.presence ||
|
|
99
|
+
aa_resource.resource_class.name.delete_prefix("::").gsub("::", "__")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -26,6 +26,12 @@ module ActiveAdmin
|
|
|
26
26
|
|
|
27
27
|
def register_namespace_settings!
|
|
28
28
|
ns = ActiveAdmin::NamespaceSettings
|
|
29
|
+
register_surface_namespace_settings!(ns)
|
|
30
|
+
register_capability_namespace_settings!(ns)
|
|
31
|
+
register_limit_namespace_settings!(ns)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def register_surface_namespace_settings!(ns)
|
|
29
35
|
ns.register :graphql, false
|
|
30
36
|
# true preserves 0.3.x: every ActiveRecord resource is in the schema once the namespace endpoint is on.
|
|
31
37
|
# Set false for opt-in per resource (empty graphql block or enable!).
|
|
@@ -45,8 +51,26 @@ module ActiveAdmin
|
|
|
45
51
|
ns.register :graphql_visibility, nil
|
|
46
52
|
ns.register :graphql_visibility_profile, nil
|
|
47
53
|
ns.register :graphql_schema_visible, nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def register_capability_namespace_settings!(ns)
|
|
48
57
|
# Optional Proc +(context_hash) -> Enumerable+ of capability names (RFC 0008).
|
|
49
58
|
ns.register :graphql_capabilities, nil
|
|
59
|
+
# RFC 0009: default field capability mode (:omit or :null).
|
|
60
|
+
ns.register :graphql_field_capability_mode_default, :omit
|
|
61
|
+
# RFC 0009: optional Proc +(context, meta) -> Boolean+ to omit object/root fields.
|
|
62
|
+
ns.register :graphql_field_visible, nil
|
|
63
|
+
# RFC 0010: optional Proc +(context, meta) -> void+ after run-action outcomes.
|
|
64
|
+
ns.register :graphql_run_action_audit, nil
|
|
65
|
+
# RFC 0011: optional Proc +(context, meta) -> void+ at collection resolve start.
|
|
66
|
+
ns.register :graphql_collection_query_policy, nil
|
|
67
|
+
# RFC 0012: optional default capabilities for run-action mutation fields.
|
|
68
|
+
ns.register :graphql_member_action_capability_default, nil
|
|
69
|
+
ns.register :graphql_batch_action_capability_default, nil
|
|
70
|
+
ns.register :graphql_collection_action_capability_default, nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def register_limit_namespace_settings!(ns)
|
|
50
74
|
ns.register :graphql_max_complexity, 300
|
|
51
75
|
ns.register :graphql_max_depth, 18
|
|
52
76
|
ns.register :graphql_batch_action_max_ids, 5000
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Compatibility wrapper around {RunActionCapability} for member actions (RFC 0012).
|
|
6
|
+
module MemberActionCapability
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def resolve(namespace, aa_res, action_name = nil)
|
|
10
|
+
RunActionCapability.resolve(namespace, aa_res, :member, action_name)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Preview context and resolve helpers for +member_action_mutation dry_run:+ (RFC 0010).
|
|
6
|
+
class MemberActionDryRun
|
|
7
|
+
PreviewContext = RunActionPreview
|
|
8
|
+
|
|
9
|
+
def self.resolve(
|
|
10
|
+
aa_res,
|
|
11
|
+
proxy:,
|
|
12
|
+
context:,
|
|
13
|
+
action:,
|
|
14
|
+
id:,
|
|
15
|
+
params_h:,
|
|
16
|
+
extras:,
|
|
17
|
+
per:,
|
|
18
|
+
dry_run:
|
|
19
|
+
)
|
|
20
|
+
new(aa_res, proxy:, context:, action:, id:, params_h:, extras:, per:, dry_run:).call
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(aa_res, proxy:, context:, action:, id:, params_h:, extras:, per:, dry_run:)
|
|
24
|
+
@aa_res = aa_res
|
|
25
|
+
@proxy = proxy
|
|
26
|
+
@context = context
|
|
27
|
+
@action = action
|
|
28
|
+
@id = id
|
|
29
|
+
@params_h = params_h
|
|
30
|
+
@extras = extras
|
|
31
|
+
@per = per
|
|
32
|
+
@dry_run = dry_run
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def call
|
|
36
|
+
record = @proxy.find_member(@id)
|
|
37
|
+
preview_result = run_preview(record)
|
|
38
|
+
|
|
39
|
+
if @dry_run
|
|
40
|
+
result = preview_result || default_dry_run_result(record)
|
|
41
|
+
audit!(result, dry_run: true)
|
|
42
|
+
return result
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if preview_result && !preview_result.ok
|
|
46
|
+
audit!(preview_result, dry_run: false)
|
|
47
|
+
return preview_result
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
result = run_action
|
|
51
|
+
audit!(result, dry_run: false)
|
|
52
|
+
result
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def run_preview(record)
|
|
58
|
+
return nil unless @per.preview_proc
|
|
59
|
+
|
|
60
|
+
PreviewContext.new.instance_exec(
|
|
61
|
+
record,
|
|
62
|
+
proxy: @proxy,
|
|
63
|
+
context: @context,
|
|
64
|
+
aa_resource: @aa_res,
|
|
65
|
+
auth: @context[:auth],
|
|
66
|
+
action: @action,
|
|
67
|
+
id: @id,
|
|
68
|
+
params: @params_h,
|
|
69
|
+
**@extras,
|
|
70
|
+
&@per.preview_proc
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def default_dry_run_result(record)
|
|
75
|
+
body = if record
|
|
76
|
+
"would_run:#{@action}:#{record.id}"
|
|
77
|
+
else
|
|
78
|
+
"would_run:#{@action}"
|
|
79
|
+
end
|
|
80
|
+
RunActionPayload::Result.new(ok: true, status: 200, location: nil, body: body)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def run_action
|
|
84
|
+
resolve = @per.resolve_proc || @aa_res.graphql_config.member_run_action.resolve_proc
|
|
85
|
+
if resolve
|
|
86
|
+
resolve.call(
|
|
87
|
+
proxy: @proxy,
|
|
88
|
+
context: @context,
|
|
89
|
+
aa_resource: @aa_res,
|
|
90
|
+
auth: @context[:auth],
|
|
91
|
+
action: @action,
|
|
92
|
+
id: @id,
|
|
93
|
+
params: @params_h,
|
|
94
|
+
**@extras
|
|
95
|
+
)
|
|
96
|
+
else
|
|
97
|
+
@proxy.run_member_action(@action, @id, extra_params: @params_h)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def audit!(result, dry_run:)
|
|
102
|
+
hook = @context[:namespace]&.graphql_run_action_audit
|
|
103
|
+
return if hook.nil?
|
|
104
|
+
|
|
105
|
+
hook.call(
|
|
106
|
+
@context,
|
|
107
|
+
{
|
|
108
|
+
action: @action,
|
|
109
|
+
id: @id,
|
|
110
|
+
status: result.respond_to?(:status) ? result.status : nil,
|
|
111
|
+
dry_run: dry_run,
|
|
112
|
+
ok: result.respond_to?(:ok) ? result.ok : nil,
|
|
113
|
+
reason: result.respond_to?(:body) ? result.body : nil
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
rescue
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -4,7 +4,7 @@ require_relative "capability_definition_dsl"
|
|
|
4
4
|
|
|
5
5
|
module ActiveAdmin
|
|
6
6
|
module GraphQL
|
|
7
|
-
# +capability+ entry on {ResourceDefinitionDSL} (RFC 0008).
|
|
7
|
+
# +capability+ entry on {ResourceDefinitionDSL} (RFC 0008, RFC 0009).
|
|
8
8
|
module ResourceCapabilityDSL
|
|
9
9
|
def capability(
|
|
10
10
|
name,
|
|
@@ -14,10 +14,11 @@ module ActiveAdmin
|
|
|
14
14
|
member_actions: nil,
|
|
15
15
|
batch_actions: nil,
|
|
16
16
|
collection_actions: nil,
|
|
17
|
+
mode: nil,
|
|
17
18
|
&block
|
|
18
19
|
)
|
|
19
|
-
body = CapabilityDefinitionDSL.new(@config, name)
|
|
20
|
-
body.fields(*fields) if fields
|
|
20
|
+
body = CapabilityDefinitionDSL.new(@config, name, mode: mode)
|
|
21
|
+
body.fields(*fields, mode: mode) if fields
|
|
21
22
|
body.queries(*queries) if queries
|
|
22
23
|
body.mutations(*mutations) if mutations
|
|
23
24
|
body.member_actions(*member_actions) if member_actions
|