activeadmin-graphql 0.3.3 → 0.4.0
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 +17 -1
- data/docs/changelogs/20261012200000_host-customization-hooks.md +27 -0
- data/docs/changelogs/20261013080000_release-0-4-0.md +48 -0
- data/docs/graphql-api.md +53 -6
- data/docs/issues/20261012200000_host-customization-gaps.md +26 -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_member.rb +8 -3
- 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 +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c03a6abe474dea238c1dec40f75bb03b2f3203e4874d4639e2327a0562c9a9ce
|
|
4
|
+
data.tar.gz: 3bb9bd9cf8b55e796cf87cf8f51dca817b41b5ae9a515132ff0e56a218afe6db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9e0a6930217123a83432a072410be640d618371c062eb83e27c86786461218f4d48d524edd9a7c00749914ac3c216d015d565fbc60a7f5fcdf7fc0d2f57d48b7
|
|
7
|
+
data.tar.gz: 639363956ab8b19349df04e7a5c5f9f6610705206b3d7d8f82838cde74b4dc10ab5e7b53f993def3c2de016885d1a1ecd267698252b877f655c87b49ef1a8f9f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.4.0 (2026-10-13)
|
|
4
|
+
|
|
5
|
+
- Add field `capability_mode` (`:omit` / `:null`) and namespace hooks
|
|
6
|
+
`graphql_field_capability_mode_default` and `graphql_field_visible` (RFC 0009).
|
|
7
|
+
- Add `member_action_mutation … dry_run: true` with `preview`, `extra_params`,
|
|
8
|
+
`ok` / `refuse`, and optional `graphql_run_action_audit` (RFC 0010).
|
|
9
|
+
- Add `graphql_collection_query_policy` and
|
|
10
|
+
`CollectionQueryPolicy.require_visible_fields!`, inferring filter grants from
|
|
11
|
+
resource field capabilities by default (RFC 0011).
|
|
12
|
+
- Add `graphql_member_action_capability_default`,
|
|
13
|
+
`graphql_batch_action_capability_default`,
|
|
14
|
+
`graphql_collection_action_capability_default`, and `capability:` on
|
|
15
|
+
`batch_action_mutation` / `member_action_mutation` /
|
|
16
|
+
`collection_action_mutation` (RFC 0012).
|
|
17
|
+
- Add `dry_run:` / `confirm:` / `preview:` on `SchemaField`, preview
|
|
18
|
+
short-circuit via `RunActionControlExtension`, and
|
|
19
|
+
`RunActionField.define` DSL sugar for custom run-action fields (RFC 0013).
|
|
4
20
|
|
|
5
21
|
## 0.3.3 (2026-10-12)
|
|
6
22
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Host customization hooks
|
|
2
|
+
|
|
3
|
+
## Decisions
|
|
4
|
+
|
|
5
|
+
Ship generic namespace hooks and DSL for field capability null mode, member-action
|
|
6
|
+
dry-run with preview, collection query policy, default run-action capabilities,
|
|
7
|
+
and confirm/dry-run/preview plus RunActionField.define for custom mutations.
|
|
8
|
+
Product allowlists and auth grants stay in the host application.
|
|
9
|
+
|
|
10
|
+
## Effects
|
|
11
|
+
|
|
12
|
+
RFCs 0009 through 0013 claimed and drafted. Implementation under 0.4.0 in
|
|
13
|
+
CHANGELOG.md. Request specs cover null-mode introspection, dry-run short-circuit,
|
|
14
|
+
collection filter refusal, default capability omit, and confirm_required.
|
|
15
|
+
|
|
16
|
+
## Next
|
|
17
|
+
|
|
18
|
+
Metadata prepared in docs/changelogs/20261013080000_release-0-4-0.md. Run make
|
|
19
|
+
release when ready to push the gem and tag. Host apps can drop prepends that
|
|
20
|
+
duplicated these mechanics after upgrading.
|
|
21
|
+
|
|
22
|
+
## Source
|
|
23
|
+
|
|
24
|
+
RFCs 0009–0013. Specs under spec/requests/graphql_capability_mode_spec.rb,
|
|
25
|
+
graphql_member_action_dry_run_spec.rb, graphql_collection_query_policy_spec.rb,
|
|
26
|
+
graphql_run_action_capability_default_spec.rb,
|
|
27
|
+
graphql_run_action_confirm_spec.rb.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Release 0.4.0 metadata
|
|
2
|
+
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
Andrei Makarov
|
|
6
|
+
|
|
7
|
+
## Decisions
|
|
8
|
+
|
|
9
|
+
Cut 0.4.0 after published 0.3.3. User-visible work since the 0.3.3 tag:
|
|
10
|
+
field capability null mode and visibility hooks (RFC 0009), member-action
|
|
11
|
+
dry-run with preview and optional audit (RFC 0010), collection query policy
|
|
12
|
+
with filter grants inferred from field capabilities (RFC 0011), default
|
|
13
|
+
run-action capabilities and per-mutation `capability:` (RFC 0012), and
|
|
14
|
+
confirm / dry-run / preview plus `RunActionField.define` for custom
|
|
15
|
+
run-action fields (RFC 0013). Product allowlists and auth grants stay in the
|
|
16
|
+
host application.
|
|
17
|
+
|
|
18
|
+
CHANGELOG Unreleased bullets moved under 0.4.0 dated 2026-10-13.
|
|
19
|
+
|
|
20
|
+
Version constant is 0.4.0. Path gem pins in Gemfile.lock and appraisal
|
|
21
|
+
lockfiles match.
|
|
22
|
+
|
|
23
|
+
make release (usr/bin/release.rb) still owns gem build, gem push, git tag, and
|
|
24
|
+
GitHub release. This pass only prepares metadata.
|
|
25
|
+
|
|
26
|
+
## Effects
|
|
27
|
+
|
|
28
|
+
lib/active_admin/graphql/version.rb VERSION is 0.4.0.
|
|
29
|
+
|
|
30
|
+
CHANGELOG host-customization bullets live under 0.4.0.
|
|
31
|
+
|
|
32
|
+
PATH specs for activeadmin-graphql in Gemfile.lock,
|
|
33
|
+
gemfiles/rails72.gemfile.lock, gemfiles/rails8ruby4.gemfile.lock, and
|
|
34
|
+
gemfiles/rails8ruby34.gemfile.lock are 0.4.0.
|
|
35
|
+
|
|
36
|
+
## Next
|
|
37
|
+
|
|
38
|
+
Run make release from a clean tree when ready to push the gem and tag 0.4.0.
|
|
39
|
+
|
|
40
|
+
## Source
|
|
41
|
+
|
|
42
|
+
docs/changelogs/20261012200000_host-customization-hooks.md
|
|
43
|
+
rfcs/0009-field-capability-mode.md
|
|
44
|
+
rfcs/0010-member-action-dry-run.md
|
|
45
|
+
rfcs/0011-collection-query-policy.md
|
|
46
|
+
rfcs/0012-run-action-capability-defaults.md
|
|
47
|
+
rfcs/0013-run-action-confirm-dry-run.md
|
|
48
|
+
usr/bin/release.rb
|
data/docs/graphql-api.md
CHANGED
|
@@ -108,6 +108,22 @@ Each namespace inherits these options (defaults in parentheses):
|
|
|
108
108
|
Cannot be combined with named `graphql_visibility` `profiles:`; schema build
|
|
109
109
|
forces `dynamic: true` and omits `visibility_profile` from the request context
|
|
110
110
|
so introspection uses the live capability set.
|
|
111
|
+
* `graphql_field_capability_mode_default` — `:omit` or `:null` when a field has a
|
|
112
|
+
capability but no per-field mode (RFC 0009). Default `:omit`.
|
|
113
|
+
* `graphql_field_visible` — optional `Proc` `(context, meta) -> Boolean` to omit
|
|
114
|
+
fields after capability checks (RFC 0009). Meta includes `graphql_type_name`,
|
|
115
|
+
`field_name`, and `resource` when known.
|
|
116
|
+
* `graphql_run_action_audit` — optional `Proc` `(context, meta) -> void` after
|
|
117
|
+
member-action dry-run, refused preview, or completed run (RFC 0010).
|
|
118
|
+
* `graphql_collection_query_policy` — optional `Proc` `(context, meta) -> void`
|
|
119
|
+
at the start of collection resolve (RFC 0011). Meta includes `aa_resource`,
|
|
120
|
+
`q`, `scope`, `order`, and `filter`.
|
|
121
|
+
* `graphql_member_action_capability_default` — optional capability symbol applied
|
|
122
|
+
to member-action mutation fields when the resource map has no entry (RFC 0012).
|
|
123
|
+
* `graphql_batch_action_capability_default` — same for the aggregate batch-action
|
|
124
|
+
field and per-action authorize checks (RFC 0012).
|
|
125
|
+
* `graphql_collection_action_capability_default` — same for collection-action
|
|
126
|
+
mutation fields (RFC 0012).
|
|
111
127
|
* `graphql_configure_schema` — `Proc` receiving the schema class after build
|
|
112
128
|
(extensions, extra types, and so on).
|
|
113
129
|
|
|
@@ -284,14 +300,27 @@ admin.graphql_capabilities = lambda { |ctx|
|
|
|
284
300
|
|
|
285
301
|
A gated member fails closed when the capability is missing from that set:
|
|
286
302
|
resolve returns null for the field (graphql-ruby unauthorized field). Enable
|
|
287
|
-
`graphql_visibility` so introspection and validation also omit ungated members
|
|
303
|
+
`graphql_visibility` so introspection and validation also omit ungated members
|
|
304
|
+
when the field mode is `:omit` (default). Use `capability_mode: :null` (or
|
|
305
|
+
`capability :pii, fields: %i[body], mode: :null`) so the field stays in the
|
|
306
|
+
schema and resolves to `null` without the grant (RFC 0009).
|
|
307
|
+
|
|
288
308
|
`fields:` applies to generated attributes and wired `belongs_to` associations.
|
|
289
309
|
Unknown field names raise at schema build. Capabilities do not replace the
|
|
290
310
|
authorization adapter.
|
|
291
311
|
|
|
292
|
-
Custom `SchemaField` definitions may pass `capability: :name`
|
|
293
|
-
|
|
294
|
-
(
|
|
312
|
+
Custom `SchemaField` definitions may pass `capability: :name` and
|
|
313
|
+
`capability_mode: :null` the same way. Set `graphql_field_visible` for a second
|
|
314
|
+
omit gate (for example a request allowlist). Do not use named visibility
|
|
315
|
+
`profiles:` together with `graphql_capabilities` (cached profile context ignores
|
|
316
|
+
per-request grants).
|
|
317
|
+
|
|
318
|
+
Collection list fields may call a host `graphql_collection_query_policy`. The
|
|
319
|
+
helper `ActiveAdmin::GraphQL::CollectionQueryPolicy.require_visible_fields!`
|
|
320
|
+
rejects Ransack `q` keys for hidden fields and requires capabilities from the
|
|
321
|
+
resource field map by default (needed for `capability_mode: :null` columns).
|
|
322
|
+
Optional `field_grants` overrides inference; `infer_field_grants: false` turns
|
|
323
|
+
it off (RFC 0011).
|
|
295
324
|
|
|
296
325
|
## Schema overview
|
|
297
326
|
|
|
@@ -572,8 +601,12 @@ graphql do
|
|
|
572
601
|
type(Members::MemberRunPayload)
|
|
573
602
|
end
|
|
574
603
|
|
|
575
|
-
member_action_mutation(:send_invoice) do
|
|
604
|
+
member_action_mutation(:send_invoice, dry_run: true, capability: :support_act) do
|
|
576
605
|
type(Members::SendInvoicePayload)
|
|
606
|
+
preview do |record, **|
|
|
607
|
+
record.may_send_invoice? ? ok("would_run:send_invoice:#{record.id}") : refuse("not_ready")
|
|
608
|
+
end
|
|
609
|
+
extra_params :note
|
|
577
610
|
arguments do
|
|
578
611
|
argument :note, GraphQL::Types::String, required: false, camelize: false
|
|
579
612
|
end
|
|
@@ -583,7 +616,21 @@ end
|
|
|
583
616
|
|
|
584
617
|
Call the named field as `posts_member_send_invoice(id: "...", note: "…")`. Extra
|
|
585
618
|
arguments are merged with `params` for the controller and passed as keyword args
|
|
586
|
-
to a custom `resolve` block.
|
|
619
|
+
to a custom `resolve` block. With `dry_run: true`, the field gains a Boolean
|
|
620
|
+
`dry_run` argument: preview runs first, dry-run returns without
|
|
621
|
+
`run_member_action`, and a refused preview never runs the action (RFC 0010).
|
|
622
|
+
`extra_params` allowlists which GraphQL kwargs forward into the AA request
|
|
623
|
+
(`id` cannot be rebound). `capability:` on batch / member / collection mutation
|
|
624
|
+
DSL overrides the resource map; otherwise the matching
|
|
625
|
+
`graphql_*_action_capability_default` applies (RFC 0012).
|
|
626
|
+
|
|
627
|
+
Custom mutations in `graphql_configure_schema` may use
|
|
628
|
+
`ActiveAdmin::GraphQL::RunActionField.define` (or `SchemaField` keywords
|
|
629
|
+
`dry_run:`, `confirm:`, `preview:`) for confirm/dry-run wiring (RFC 0013). When
|
|
630
|
+
`confirm` is required and both flags are false, the field returns
|
|
631
|
+
`body: "confirm_required"`. When `preview` is set, dry-run and refused preview
|
|
632
|
+
return without calling `resolve`; without `preview`, dry-run still calls
|
|
633
|
+
`resolve` with `dry_run: true`.
|
|
587
634
|
|
|
588
635
|
With no symbol argument, `member_action_mutation { … }` configures only the
|
|
589
636
|
aggregate `posts_member_action` field. With a symbol, it configures the
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Host customization gaps
|
|
2
|
+
|
|
3
|
+
## Decisions
|
|
4
|
+
|
|
5
|
+
The gem owns mechanics: when a field is visible, null, or omitted; dry-run
|
|
6
|
+
argument wiring; filter-key policy hooks; default member-action capability;
|
|
7
|
+
confirm/dry_run injection on custom fields. Hosts own product grants, domain
|
|
8
|
+
preview bodies, and audit storage.
|
|
9
|
+
|
|
10
|
+
Capabilities remain grant names in context[:capabilities]. Named visibility
|
|
11
|
+
profiles stay incompatible with graphql_capabilities.
|
|
12
|
+
|
|
13
|
+
## Effects
|
|
14
|
+
|
|
15
|
+
RFCs 0009–0013 specify the contract. Implementation landed on the feature
|
|
16
|
+
branch with request coverage for each hook.
|
|
17
|
+
|
|
18
|
+
## Next
|
|
19
|
+
|
|
20
|
+
Publish a release. Hosts migrate off local prepends in the order: capability
|
|
21
|
+
null mode and dry-run DSL, then collection policy, then field_visible and
|
|
22
|
+
capability defaults, then confirm helpers.
|
|
23
|
+
|
|
24
|
+
## Source
|
|
25
|
+
|
|
26
|
+
RFCs 0009–0013. docs/changelogs/20261012200000_host-customization-hooks.md.
|
|
@@ -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
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module ActiveAdmin
|
|
4
4
|
module GraphQL
|
|
5
|
-
# Capability → member maps on {ResourceConfig} (RFC 0008).
|
|
5
|
+
# Capability → member maps on {ResourceConfig} (RFC 0008, RFC 0009).
|
|
6
6
|
module ResourceCapabilityMaps
|
|
7
7
|
def field_capabilities
|
|
8
8
|
@field_capabilities ||= {}
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
def field_capability_modes
|
|
12
|
+
@field_capability_modes ||= {}
|
|
13
|
+
end
|
|
14
|
+
|
|
11
15
|
def query_capabilities
|
|
12
16
|
@query_capabilities ||= {}
|
|
13
17
|
end
|
|
@@ -20,6 +24,10 @@ module ActiveAdmin
|
|
|
20
24
|
field_capabilities[name.to_sym]
|
|
21
25
|
end
|
|
22
26
|
|
|
27
|
+
def capability_mode_for_field(name)
|
|
28
|
+
field_capability_modes[name.to_sym]
|
|
29
|
+
end
|
|
30
|
+
|
|
23
31
|
def capability_for_query(kind)
|
|
24
32
|
query_capabilities[kind.to_sym]
|
|
25
33
|
end
|
|
@@ -28,8 +36,18 @@ module ActiveAdmin
|
|
|
28
36
|
mutation_capabilities[kind.to_sym]
|
|
29
37
|
end
|
|
30
38
|
|
|
31
|
-
def assign_field_capabilities!(capability_name, names)
|
|
39
|
+
def assign_field_capabilities!(capability_name, names, mode: nil)
|
|
32
40
|
assign_capabilities!(field_capabilities, capability_name, names, label: "field")
|
|
41
|
+
return if mode.nil?
|
|
42
|
+
|
|
43
|
+
resolved = mode.to_sym
|
|
44
|
+
unless %i[omit null].include?(resolved)
|
|
45
|
+
raise ArgumentError, "capability mode must be :omit or :null, got #{mode.inspect}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
names.flatten.each do |name|
|
|
49
|
+
field_capability_modes[name.to_sym] = resolved
|
|
50
|
+
end
|
|
33
51
|
end
|
|
34
52
|
|
|
35
53
|
def assign_query_capabilities!(capability_name, names)
|
|
@@ -103,21 +103,32 @@ module ActiveAdmin
|
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
# Pair +type+ and +resolve+ for +posts_batch_action+ (graphql-ruby-style block).
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
# +capability:+ overrides the resource map / namespace default for the aggregate field (RFC 0012).
|
|
107
|
+
def batch_action_mutation(capability: nil, &block)
|
|
108
|
+
cfg = @config.batch_run_action
|
|
109
|
+
cfg.capability = capability.to_sym if capability
|
|
110
|
+
RunActionMutationDSL.new(cfg).instance_exec(&block) if block
|
|
111
|
+
cfg
|
|
108
112
|
end
|
|
109
113
|
|
|
110
114
|
# With no name: configures the aggregate +posts_member_action(action: …)+ field.
|
|
111
115
|
# With a symbol/string: configures +posts_member_<action>+ (one field per +member_action+), so each
|
|
112
116
|
# action can use its own +type+, +resolve+, +arguments+, and GraphQL inputs.
|
|
113
|
-
|
|
117
|
+
# +dry_run:+ / +capability:+ apply to named per-action fields (RFC 0010, RFC 0012).
|
|
118
|
+
def member_action_mutation(name = nil, dry_run: false, capability: nil, &block)
|
|
114
119
|
cfg = name.nil? ? @config.member_run_action : @config.member_action_mutation_for(name)
|
|
115
|
-
|
|
120
|
+
cfg.dry_run = true if dry_run
|
|
121
|
+
cfg.capability = capability.to_sym if capability
|
|
122
|
+
RunActionMutationDSL.new(cfg).instance_exec(&block) if block
|
|
123
|
+
cfg
|
|
116
124
|
end
|
|
117
125
|
|
|
118
|
-
def collection_action_mutation(name = nil, &block)
|
|
126
|
+
def collection_action_mutation(name = nil, dry_run: false, capability: nil, &block)
|
|
119
127
|
cfg = name.nil? ? @config.collection_run_action : @config.collection_action_mutation_for(name)
|
|
120
|
-
|
|
128
|
+
cfg.dry_run = true if dry_run
|
|
129
|
+
cfg.capability = capability.to_sym if capability
|
|
130
|
+
RunActionMutationDSL.new(cfg).instance_exec(&block) if block
|
|
131
|
+
cfg
|
|
121
132
|
end
|
|
122
133
|
|
|
123
134
|
# Default return type for all run-action fields unless a kind-specific +type+ is set inside
|