activeadmin-graphql 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/app/controllers/active_admin/graphql_controller.rb +19 -4
- data/docs/changelogs/20261010091415_graphql-resource-enabled-default.md +34 -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/changelogs/20261012050051_graphql-surface-toggles.md +40 -0
- data/docs/changelogs/20261012064500_graphql-capabilities.md +35 -0
- data/docs/changelogs/20261012113500_release-0-3-3.md +45 -0
- data/docs/graphql-api.md +166 -20
- 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/capability.rb +35 -0
- data/lib/active_admin/graphql/capability_definition_dsl.rb +64 -0
- data/lib/active_admin/graphql/integration.rb +19 -1
- data/lib/active_admin/graphql/policy_adapter.rb +94 -0
- data/lib/active_admin/graphql/policy_set_cache.rb +18 -9
- data/lib/active_admin/graphql/resource_capability_dsl.rb +31 -0
- data/lib/active_admin/graphql/resource_capability_maps.rb +59 -0
- data/lib/active_admin/graphql/resource_config.rb +23 -2
- data/lib/active_admin/graphql/resource_definition_dsl.rb +12 -2
- data/lib/active_admin/graphql/resource_identity.rb +98 -0
- data/lib/active_admin/graphql/resource_methods.rb +8 -2
- 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/resource_surface_dsl.rb +48 -0
- data/lib/active_admin/graphql/schema_builder/build.rb +52 -3
- data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +7 -0
- data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +8 -0
- data/lib/active_admin/graphql/schema_builder/mutation_create.rb +1 -0
- data/lib/active_admin/graphql/schema_builder/mutation_member.rb +8 -0
- data/lib/active_admin/graphql/schema_builder/mutation_type_builder.rb +15 -10
- data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +4 -2
- data/lib/active_admin/graphql/schema_builder/query_type.rb +2 -1
- data/lib/active_admin/graphql/schema_builder/query_type_collection.rb +3 -0
- data/lib/active_admin/graphql/schema_builder/query_type_member.rb +5 -2
- data/lib/active_admin/graphql/schema_builder/query_type_policies.rb +9 -63
- data/lib/active_admin/graphql/schema_builder/resources.rb +34 -0
- data/lib/active_admin/graphql/schema_builder/types_object.rb +27 -11
- data/lib/active_admin/graphql/schema_builder/visibility.rb +22 -7
- data/lib/active_admin/graphql/schema_builder/wire.rb +3 -1
- data/lib/active_admin/graphql/schema_field.rb +18 -2
- data/lib/active_admin/graphql/version.rb +1 -1
- data/lib/active_admin/graphql.rb +3 -0
- metadata +17 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Rails composite primary keys alignment
|
|
2
|
+
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
Andrei Makarov
|
|
6
|
+
|
|
7
|
+
## Decisions
|
|
8
|
+
|
|
9
|
+
Treat ActiveRecord composite primary keys and GraphQL `identity` as related but
|
|
10
|
+
not identical contracts. Keep GraphQL `id` as a single `ID!` string. Do not
|
|
11
|
+
adopt Rails form URL underscore packing as the default GraphQL id encoding.
|
|
12
|
+
|
|
13
|
+
Record alignment against the Rails guide Composite Primary Keys
|
|
14
|
+
(https://guides.rubyonrails.org/active_record_composite_primary_keys.html)
|
|
15
|
+
after shipping inferred id columns and RFC 0007 identity DSL.
|
|
16
|
+
|
|
17
|
+
## Effects
|
|
18
|
+
|
|
19
|
+
Compared gem behavior to the guide’s model, query, form, and parameter rules.
|
|
20
|
+
|
|
21
|
+
ActiveRecord composite primary key (self.primary_key = array):
|
|
22
|
+
|
|
23
|
+
- Aligns: model declares composite key the same way as the guide’s
|
|
24
|
+
`self.primary_key = [:store_id, :sku]` style. Dummy `LibraryEdition` uses
|
|
25
|
+
`[:book_code, :seq]`.
|
|
26
|
+
- Aligns: internal batch and find paths convert that string id into an ordered
|
|
27
|
+
value list and use Rails tuple/`Relation#find` style lookups
|
|
28
|
+
(`ActiveAdmin::PrimaryKey.dataloader_tuple`, `ResourceQueryProxy` composite
|
|
29
|
+
branch). That matches the guide’s `Product.find([3, "XYZ12345"])` and
|
|
30
|
+
`where(primary_key => [[...], [...]])` idea at the Ruby API layer.
|
|
31
|
+
- Diverges: GraphQL and the published docs encode composite `id` as a JSON
|
|
32
|
+
object string (`{"book_code":"CPK","seq":7}`), not as the guide’s form/URL
|
|
33
|
+
underscore packing (`/books/2_25` with `params.extract_value(:id)`). JSON
|
|
34
|
+
keeps column names explicit and avoids ambiguity when a key value contains
|
|
35
|
+
`_`. Underscore packing is not accepted as GraphQL `id` today.
|
|
36
|
+
- Diverges: GraphQL also accepts per-key arguments and `WhereInput` fields for
|
|
37
|
+
composite models. The guide’s controller example extracts one `:id` param
|
|
38
|
+
into an array; we keep named keys as a GraphQL-friendly alternate.
|
|
39
|
+
- Aligns with guide caution: `where(id: ...)` / `find_by(id:)` on composite
|
|
40
|
+
models can mean an `:id` attribute, not the full primary key. The gem’s
|
|
41
|
+
GraphQL `id` field is the ActiveAdminResource identity string, not
|
|
42
|
+
necessarily an ActiveRecord `:id` column. Member lookup uses primary-key
|
|
43
|
+
columns (or configured identity), not a bare `where(id: graphql_id)` on
|
|
44
|
+
composite models.
|
|
45
|
+
|
|
46
|
+
Keyless views and RFC 0007 `graphql { identity ... }`:
|
|
47
|
+
|
|
48
|
+
- Outside the guide’s scope. The guide assumes a declared composite primary
|
|
49
|
+
key on the table/model. AlertEvent-style views with `primary_key = nil` and
|
|
50
|
+
a virtual Ruby `id` are not Rails composite primary keys.
|
|
51
|
+
- `identity :a, :b, :c, separator: ":"` is an application-level encoding so
|
|
52
|
+
GraphQL can share the same string as a hand-rolled `def id; [...].join(":")`.
|
|
53
|
+
That separator form is closer in spirit to Rails’ delimited URL id than our
|
|
54
|
+
JSON composite encoding, but the delimiter and segment rules are app-chosen,
|
|
55
|
+
not `extract_value`’s underscore default.
|
|
56
|
+
- Without `separator`, configured multi-column identity reuses the gem’s JSON
|
|
57
|
+
object string so keyless and AR-composite GraphQL ids stay one family.
|
|
58
|
+
|
|
59
|
+
Stock ActiveAdmin HTML member routes still need a composite-aware
|
|
60
|
+
`find_resource` (or custom finder) for AR composite keys; GraphQL does not
|
|
61
|
+
make Rails form URLs use JSON.
|
|
62
|
+
|
|
63
|
+
## Next
|
|
64
|
+
|
|
65
|
+
Leave GraphQL composite `id` as JSON for AR composite primary keys unless a
|
|
66
|
+
later RFC deliberately adds underscore (or `extract_value`-compatible) accept
|
|
67
|
+
and emit. Document that choice next to the Rails guide, not as “the same
|
|
68
|
+
format Rails uses for casting composite ids” without qualification.
|
|
69
|
+
|
|
70
|
+
No change required for RFC 0007 separator identity; it is the escape hatch
|
|
71
|
+
when the model is not a Rails composite primary key.
|
|
72
|
+
|
|
73
|
+
## Source
|
|
74
|
+
|
|
75
|
+
https://guides.rubyonrails.org/active_record_composite_primary_keys.html
|
|
76
|
+
docs/graphql-api.md (Nonconventional primary keys, Composite primary keys)
|
|
77
|
+
rfcs/0007-graphql-identity.md
|
|
78
|
+
lib/active_admin/primary_key.rb
|
|
79
|
+
lib/active_admin/graphql/resource_identity.rb
|
|
80
|
+
spec/dummy/app/models/library_edition.rb
|
|
81
|
+
spec/dummy/app/models/alert_event.rb
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Table partitions and database shards support
|
|
2
|
+
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
Andrei Makarov
|
|
6
|
+
|
|
7
|
+
## Decisions
|
|
8
|
+
|
|
9
|
+
activeadmin-graphql does not treat database table partitions or Rails
|
|
10
|
+
database shards as a GraphQL configuration surface. Identity DSL (RFC 0007)
|
|
11
|
+
and ActiveRecord composite primary keys only encode lookup columns into
|
|
12
|
+
GraphQL `id`. They do not select a shard, connection role, or physical
|
|
13
|
+
partition.
|
|
14
|
+
|
|
15
|
+
App-level Rails multi-database setup (`connects_to`, `connected_to`,
|
|
16
|
+
horizontal shards, tenant middleware) may still apply implicitly: GraphQL
|
|
17
|
+
resolvers reuse ActiveAdmin `scoped_collection` / `find_resource` on whatever
|
|
18
|
+
connection the request process already has. That inheritance is undocumented
|
|
19
|
+
as a product feature and untested in this gem.
|
|
20
|
+
|
|
21
|
+
## Effects
|
|
22
|
+
|
|
23
|
+
Searched lib, docs, rfcs, and spec for partition, shard, connects_to,
|
|
24
|
+
multiple database, tenant routing, writing_role, and reading_role.
|
|
25
|
+
|
|
26
|
+
Product code has no shard or partition API. Hits for “shard” and “partition”
|
|
27
|
+
are Polyrun/CI test parallelism and agent IO-fault vocabulary, not databases.
|
|
28
|
+
|
|
29
|
+
Dummy app uses one SQLite database (`spec/dummy/config/database.yml`). No
|
|
30
|
+
multi-DB, no partitioned table fixtures, no `connected_to` specs.
|
|
31
|
+
|
|
32
|
+
`RecordSource`, `PrimaryKey`, and `ResourceIdentity` run `where` / encode on
|
|
33
|
+
the current model class only. A partition-key column can appear in
|
|
34
|
+
`graphql { identity ... }` as part of the id string; that still queries the
|
|
35
|
+
already-selected connection and does not prune or route partitions.
|
|
36
|
+
|
|
37
|
+
## Next
|
|
38
|
+
|
|
39
|
+
If a host needs GraphQL across shards or partitioned tables, keep routing in
|
|
40
|
+
the Rails/ActiveAdmin request boundary (middleware, `connected_to`, custom
|
|
41
|
+
finder). Do not invent gem DSL until a concrete host failure and RFC name the
|
|
42
|
+
contract (shard key in GraphQL args, role switching, cross-shard batch limits).
|
|
43
|
+
|
|
44
|
+
Optional later: document “unsupported / inherited only” next to identity in
|
|
45
|
+
`docs/graphql-api.md` when a reader asks again.
|
|
46
|
+
|
|
47
|
+
## Source
|
|
48
|
+
|
|
49
|
+
lib/active_admin/graphql/resource_identity.rb
|
|
50
|
+
lib/active_admin/graphql/record_source.rb
|
|
51
|
+
lib/active_admin/graphql/resource_query_proxy.rb
|
|
52
|
+
spec/dummy/config/database.yml
|
|
53
|
+
rfcs/0007-graphql-identity.md
|
|
54
|
+
docs/issues/20261010111159_rails-composite-primary-keys-alignment.md
|
|
55
|
+
polyrun.yml (test shards only)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# GraphQL Federation support
|
|
2
|
+
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
Andrei Makarov
|
|
6
|
+
|
|
7
|
+
## Decisions
|
|
8
|
+
|
|
9
|
+
activeadmin-graphql does not implement GraphQL Federation (Apollo Federation
|
|
10
|
+
v1/v2, subgraph/supergraph, `@key` entities, `_entities` / `_service`). The
|
|
11
|
+
product is one graphql-ruby schema per ActiveAdmin namespace for that admin
|
|
12
|
+
HTTP endpoint.
|
|
13
|
+
|
|
14
|
+
RFC 0007 `graphql { identity ... }` and `ActiveAdminResource.id` are record
|
|
15
|
+
lookup identity inside that schema. They are not federation entity keys and
|
|
16
|
+
do not emit federation SDL directives.
|
|
17
|
+
|
|
18
|
+
## Effects
|
|
19
|
+
|
|
20
|
+
Searched lib, docs, rfcs, spec, and the gemspec for federation, subgraph,
|
|
21
|
+
supergraph, Apollo, `@key`, shareable, apollo-federation, `_entities`, and
|
|
22
|
+
`_service`. No product hits.
|
|
23
|
+
|
|
24
|
+
Runtime dependencies are `activeadmin` and `graphql` only. Schema build is
|
|
25
|
+
`Class.new(::GraphQL::Schema)` with query, mutation, dataloader, and optional
|
|
26
|
+
visibility (`lib/active_admin/graphql/schema_builder/build.rb`).
|
|
27
|
+
`ResourceInterface` is a local interface for fragment reuse, not a federation
|
|
28
|
+
entity type.
|
|
29
|
+
|
|
30
|
+
An app could in principle call `graphql_configure_schema` on the built schema
|
|
31
|
+
and layer a federation library itself. That path is neither documented nor
|
|
32
|
+
tested here.
|
|
33
|
+
|
|
34
|
+
## Next
|
|
35
|
+
|
|
36
|
+
Keep federation out of scope until a host names a concrete subgraph contract
|
|
37
|
+
and an RFC. Prefer documenting “single-schema admin API; federation
|
|
38
|
+
unsupported” if readers keep asking.
|
|
39
|
+
|
|
40
|
+
## Source
|
|
41
|
+
|
|
42
|
+
activeadmin-graphql.gemspec
|
|
43
|
+
lib/active_admin/graphql/schema_builder/build.rb
|
|
44
|
+
lib/active_admin/graphql/resource_interface.rb
|
|
45
|
+
rfcs/0007-graphql-identity.md
|
|
46
|
+
docs/issues/20261010111300_partitions-and-shards-support.md
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Named grants carried in GraphQL +context[:capabilities]+ (RFC 0008).
|
|
6
|
+
module Capability
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def normalize_set(raw)
|
|
10
|
+
return nil if raw.nil?
|
|
11
|
+
|
|
12
|
+
Set.new(
|
|
13
|
+
Array(raw).filter_map do |item|
|
|
14
|
+
next if item.nil?
|
|
15
|
+
|
|
16
|
+
name = item.to_s.strip
|
|
17
|
+
next if name.empty?
|
|
18
|
+
|
|
19
|
+
name.to_sym
|
|
20
|
+
end
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def granted?(context, capability_name)
|
|
25
|
+
return true if capability_name.nil?
|
|
26
|
+
|
|
27
|
+
caps = context[:capabilities]
|
|
28
|
+
return false if caps.nil?
|
|
29
|
+
|
|
30
|
+
caps = normalize_set(caps) unless caps.is_a?(Set)
|
|
31
|
+
caps.include?(capability_name.to_sym)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Block body for +capability :name do ... end+ (RFC 0008).
|
|
6
|
+
class CapabilityDefinitionDSL
|
|
7
|
+
QUERY_ALIASES = {
|
|
8
|
+
index: :collection,
|
|
9
|
+
list: :collection,
|
|
10
|
+
show: :member
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
MUTATION_ALIASES = {
|
|
14
|
+
delete: :destroy
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def initialize(config, capability_name)
|
|
18
|
+
@config = config
|
|
19
|
+
@capability_name = capability_name.to_sym
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def fields(*names)
|
|
23
|
+
@config.assign_field_capabilities!(@capability_name, names)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def queries(*names)
|
|
27
|
+
keys = names.flatten.map { |name| normalize_query_key(name) }
|
|
28
|
+
@config.assign_query_capabilities!(@capability_name, keys)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def mutations(*names)
|
|
32
|
+
keys = names.flatten.map { |name| normalize_mutation_key(name) }
|
|
33
|
+
@config.assign_mutation_capabilities!(@capability_name, keys)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def member_actions(*names)
|
|
37
|
+
keys = names.flatten.map { |name| :"member_#{name.to_s.tr("-", "_")}" }
|
|
38
|
+
@config.assign_mutation_capabilities!(@capability_name, keys)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def batch_actions(*names)
|
|
42
|
+
keys = names.flatten.map { |name| :"batch_#{name.to_s.tr("-", "_")}" }
|
|
43
|
+
@config.assign_mutation_capabilities!(@capability_name, keys)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def collection_actions(*names)
|
|
47
|
+
keys = names.flatten.map { |name| :"collection_#{name.to_s.tr("-", "_")}" }
|
|
48
|
+
@config.assign_mutation_capabilities!(@capability_name, keys)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def normalize_query_key(name)
|
|
54
|
+
key = name.to_sym
|
|
55
|
+
QUERY_ALIASES.fetch(key, key)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def normalize_mutation_key(name)
|
|
59
|
+
key = name.to_sym
|
|
60
|
+
MUTATION_ALIASES.fetch(key, key)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "policy_adapter"
|
|
3
4
|
require_relative "resource_definition_dsl"
|
|
4
5
|
require_relative "resource_methods"
|
|
5
6
|
|
|
@@ -26,12 +27,26 @@ module ActiveAdmin
|
|
|
26
27
|
def register_namespace_settings!
|
|
27
28
|
ns = ActiveAdmin::NamespaceSettings
|
|
28
29
|
ns.register :graphql, false
|
|
30
|
+
# true preserves 0.3.x: every ActiveRecord resource is in the schema once the namespace endpoint is on.
|
|
31
|
+
# Set false for opt-in per resource (empty graphql block or enable!).
|
|
32
|
+
ns.register :graphql_resource_enabled_default, true
|
|
33
|
+
ns.register :graphql_queries_enabled_default, true
|
|
34
|
+
ns.register :graphql_mutations_enabled_default, true
|
|
35
|
+
ns.register :graphql_run_actions_enabled_default, true
|
|
36
|
+
ns.register :graphql_policies_enabled, true
|
|
37
|
+
ns.register :graphql_policies_enabled_default, true
|
|
38
|
+
# Payload builder for policy fields when graphql_policies_enabled is true.
|
|
39
|
+
ns.register :graphql_policy_adapter, ActiveAdmin::GraphQL::PolicyAdapter
|
|
40
|
+
ns.register :graphql_registered_resource_enabled, true
|
|
41
|
+
ns.register :graphql_belongs_to_associations_enabled, true
|
|
29
42
|
ns.register :graphql_path, "graphql"
|
|
30
43
|
ns.register :graphql_multiplex_max, 20
|
|
31
44
|
ns.register :graphql_dataloader, nil
|
|
32
45
|
ns.register :graphql_visibility, nil
|
|
33
46
|
ns.register :graphql_visibility_profile, nil
|
|
34
47
|
ns.register :graphql_schema_visible, nil
|
|
48
|
+
# Optional Proc +(context_hash) -> Enumerable+ of capability names (RFC 0008).
|
|
49
|
+
ns.register :graphql_capabilities, nil
|
|
35
50
|
ns.register :graphql_max_complexity, 300
|
|
36
51
|
ns.register :graphql_max_depth, 18
|
|
37
52
|
ns.register :graphql_batch_action_max_ids, 5000
|
|
@@ -64,7 +79,10 @@ module ActiveAdmin
|
|
|
64
79
|
module ResourceDSLMethods
|
|
65
80
|
def graphql(&block)
|
|
66
81
|
if block
|
|
67
|
-
|
|
82
|
+
cfg = config.graphql_config
|
|
83
|
+
# Presence of a graphql block is an intentional opt-in when the namespace default is false.
|
|
84
|
+
cfg.enabled = true
|
|
85
|
+
ActiveAdmin::GraphQL::ResourceDefinitionDSL.new(cfg).instance_exec(&block)
|
|
68
86
|
end
|
|
69
87
|
config.graphql_config
|
|
70
88
|
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Builds +activeadmin_policies+ payloads for a namespace.
|
|
6
|
+
#
|
|
7
|
+
# Set +graphql_policy_adapter+ to this class (default), another subclass, or a
|
|
8
|
+
# constant name +String+. Omit policy fields with +graphql_policies_enabled = false+.
|
|
9
|
+
class PolicyAdapter
|
|
10
|
+
POLICY_ACTIONS = {
|
|
11
|
+
read: ActiveAdmin::Authorization::READ,
|
|
12
|
+
create: ActiveAdmin::Authorization::CREATE,
|
|
13
|
+
update: ActiveAdmin::Authorization::UPDATE,
|
|
14
|
+
destroy: ActiveAdmin::Authorization::DESTROY
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :namespace
|
|
18
|
+
|
|
19
|
+
def initialize(namespace:)
|
|
20
|
+
@namespace = namespace
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Whether this resource gets an object +activeadmin_policies+ field and a
|
|
24
|
+
# row in the +activeadmin_policies+ resources matrix.
|
|
25
|
+
def include_resource?(aa_resource)
|
|
26
|
+
aa_resource.graphql_config.policies_enabled
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def policy_set(auth:, subject_owner:, subject:, context:)
|
|
30
|
+
base = {
|
|
31
|
+
allowed_actions: allowed_base_actions(
|
|
32
|
+
auth: auth,
|
|
33
|
+
subject_owner: subject_owner,
|
|
34
|
+
subject: subject,
|
|
35
|
+
context: context
|
|
36
|
+
),
|
|
37
|
+
allowed_member_actions: [],
|
|
38
|
+
allowed_collection_actions: [],
|
|
39
|
+
allowed_batch_actions: []
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
append_resource_action_names!(base, auth: auth, subject_owner: subject_owner, subject: subject)
|
|
43
|
+
|
|
44
|
+
if (extra = namespace.graphql_policy_extra).respond_to?(:call)
|
|
45
|
+
out = extra.call(context, subject_owner, subject, base)
|
|
46
|
+
base = out if out.is_a?(Hash)
|
|
47
|
+
end
|
|
48
|
+
if (transform = namespace.graphql_policy_transform).respond_to?(:call)
|
|
49
|
+
out = transform.call(context, subject_owner, subject, base)
|
|
50
|
+
base = out if out.is_a?(Hash)
|
|
51
|
+
end
|
|
52
|
+
base
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def policy_action_pairs
|
|
58
|
+
hook = namespace.graphql_policy_actions
|
|
59
|
+
if hook.respond_to?(:call)
|
|
60
|
+
raw = hook.call(namespace)
|
|
61
|
+
return raw.to_h.transform_keys(&:to_sym) if raw.respond_to?(:to_h)
|
|
62
|
+
end
|
|
63
|
+
POLICY_ACTIONS
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def allowed_base_actions(auth:, subject_owner:, subject:, context:)
|
|
67
|
+
action_mapper = namespace.graphql_policy_action_mapper
|
|
68
|
+
policy_action_pairs.each_with_object([]) do |(name, default_action), out|
|
|
69
|
+
allowed = if action_mapper.respond_to?(:call)
|
|
70
|
+
action_mapper.call(context, subject_owner, subject, name, default_action)
|
|
71
|
+
else
|
|
72
|
+
auth.authorized?(subject_owner, default_action, subject)
|
|
73
|
+
end
|
|
74
|
+
out << name.to_s if allowed
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def append_resource_action_names!(base, auth:, subject_owner:, subject:)
|
|
79
|
+
return unless subject_owner.is_a?(ActiveAdmin::Resource)
|
|
80
|
+
|
|
81
|
+
class_subject = subject_owner.resource_class
|
|
82
|
+
if subject.is_a?(Class)
|
|
83
|
+
return unless auth.authorized?(subject_owner, ActiveAdmin::Authorization::READ, class_subject)
|
|
84
|
+
|
|
85
|
+
base[:allowed_member_actions] = subject_owner.member_actions.map { |a| a.name.to_s }
|
|
86
|
+
base[:allowed_collection_actions] = subject_owner.collection_actions.map { |a| a.name.to_s }
|
|
87
|
+
base[:allowed_batch_actions] = subject_owner.batch_actions.map { |a| a.sym.to_s }
|
|
88
|
+
elsif auth.authorized?(subject_owner, ActiveAdmin::Authorization::READ, subject)
|
|
89
|
+
base[:allowed_member_actions] = subject_owner.member_actions.map { |a| a.name.to_s }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -6,10 +6,11 @@ module ActiveAdmin
|
|
|
6
6
|
class PolicySetCache
|
|
7
7
|
class << self
|
|
8
8
|
def fetch(context, subject_owner:, subject:)
|
|
9
|
-
|
|
10
|
-
unless
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
adapter = policy_adapter(context)
|
|
10
|
+
raise ActiveAdmin::DependencyError, "graphql_policy_adapter is disabled" unless adapter
|
|
11
|
+
|
|
12
|
+
unless context[:namespace]
|
|
13
|
+
return adapter.policy_set(
|
|
13
14
|
auth: context[:auth],
|
|
14
15
|
subject_owner: subject_owner,
|
|
15
16
|
subject: subject,
|
|
@@ -19,8 +20,7 @@ module ActiveAdmin
|
|
|
19
20
|
|
|
20
21
|
cache = context[:policy_set_cache] ||= {}
|
|
21
22
|
key = cache_key(subject_owner, subject)
|
|
22
|
-
cache[key] ||=
|
|
23
|
-
:build_policy_set,
|
|
23
|
+
cache[key] ||= adapter.policy_set(
|
|
24
24
|
auth: context[:auth],
|
|
25
25
|
subject_owner: subject_owner,
|
|
26
26
|
subject: subject,
|
|
@@ -28,13 +28,17 @@ module ActiveAdmin
|
|
|
28
28
|
)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
def policy_adapter(context)
|
|
32
|
+
policy_builder(context).send(:policy_adapter)
|
|
33
|
+
end
|
|
34
|
+
|
|
31
35
|
def policy_builder(context)
|
|
32
36
|
context[:policy_schema_builder] ||= SchemaBuilder.new(context[:namespace])
|
|
33
37
|
end
|
|
34
38
|
|
|
35
39
|
def cache_key(subject_owner, subject)
|
|
36
40
|
owner_key = subject_owner_key(subject_owner)
|
|
37
|
-
"#{owner_key}:#{subject_key(subject)}"
|
|
41
|
+
"#{owner_key}:#{subject_key(subject_owner, subject)}"
|
|
38
42
|
end
|
|
39
43
|
|
|
40
44
|
private
|
|
@@ -47,13 +51,18 @@ module ActiveAdmin
|
|
|
47
51
|
end
|
|
48
52
|
end
|
|
49
53
|
|
|
50
|
-
def subject_key(subject)
|
|
54
|
+
def subject_key(subject_owner, subject)
|
|
51
55
|
if subject.is_a?(Class)
|
|
52
56
|
"class:#{subject.name}"
|
|
53
57
|
elsif subject.is_a?(ActiveAdmin::Page)
|
|
54
58
|
"page:#{subject.name}"
|
|
55
59
|
elsif subject.is_a?(ActiveRecord::Base)
|
|
56
|
-
identity =
|
|
60
|
+
identity =
|
|
61
|
+
if subject_owner.respond_to?(:graphql_config)
|
|
62
|
+
ResourceIdentity.graphql_id_value(subject_owner, subject)
|
|
63
|
+
else
|
|
64
|
+
PrimaryKey.graphql_id_value(subject)
|
|
65
|
+
end
|
|
57
66
|
identity = "#{identity}:object:#{subject.object_id}" if subject.class.primary_key.blank?
|
|
58
67
|
"record:#{subject.class.name}:#{identity}"
|
|
59
68
|
else
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "capability_definition_dsl"
|
|
4
|
+
|
|
5
|
+
module ActiveAdmin
|
|
6
|
+
module GraphQL
|
|
7
|
+
# +capability+ entry on {ResourceDefinitionDSL} (RFC 0008).
|
|
8
|
+
module ResourceCapabilityDSL
|
|
9
|
+
def capability(
|
|
10
|
+
name,
|
|
11
|
+
fields: nil,
|
|
12
|
+
queries: nil,
|
|
13
|
+
mutations: nil,
|
|
14
|
+
member_actions: nil,
|
|
15
|
+
batch_actions: nil,
|
|
16
|
+
collection_actions: nil,
|
|
17
|
+
&block
|
|
18
|
+
)
|
|
19
|
+
body = CapabilityDefinitionDSL.new(@config, name)
|
|
20
|
+
body.fields(*fields) if fields
|
|
21
|
+
body.queries(*queries) if queries
|
|
22
|
+
body.mutations(*mutations) if mutations
|
|
23
|
+
body.member_actions(*member_actions) if member_actions
|
|
24
|
+
body.batch_actions(*batch_actions) if batch_actions
|
|
25
|
+
body.collection_actions(*collection_actions) if collection_actions
|
|
26
|
+
body.instance_exec(&block) if block
|
|
27
|
+
name.to_sym
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Capability → member maps on {ResourceConfig} (RFC 0008).
|
|
6
|
+
module ResourceCapabilityMaps
|
|
7
|
+
def field_capabilities
|
|
8
|
+
@field_capabilities ||= {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def query_capabilities
|
|
12
|
+
@query_capabilities ||= {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def mutation_capabilities
|
|
16
|
+
@mutation_capabilities ||= {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def capability_for_field(name)
|
|
20
|
+
field_capabilities[name.to_sym]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def capability_for_query(kind)
|
|
24
|
+
query_capabilities[kind.to_sym]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def capability_for_mutation(kind)
|
|
28
|
+
mutation_capabilities[kind.to_sym]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def assign_field_capabilities!(capability_name, names)
|
|
32
|
+
assign_capabilities!(field_capabilities, capability_name, names, label: "field")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def assign_query_capabilities!(capability_name, names)
|
|
36
|
+
assign_capabilities!(query_capabilities, capability_name, names, label: "query")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def assign_mutation_capabilities!(capability_name, names)
|
|
40
|
+
assign_capabilities!(mutation_capabilities, capability_name, names, label: "mutation")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def assign_capabilities!(map, capability_name, names, label:)
|
|
46
|
+
capability = capability_name.to_sym
|
|
47
|
+
names.flatten.each do |name|
|
|
48
|
+
key = name.to_sym
|
|
49
|
+
existing = map[key]
|
|
50
|
+
if existing && existing != capability
|
|
51
|
+
raise ArgumentError,
|
|
52
|
+
"GraphQL #{label} #{key.inspect} already requires capability #{existing.inspect}"
|
|
53
|
+
end
|
|
54
|
+
map[key] = capability
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -2,18 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "run_action_mutation_config"
|
|
4
4
|
require_relative "mutation_input_config"
|
|
5
|
+
require_relative "resource_capability_maps"
|
|
5
6
|
|
|
6
7
|
module ActiveAdmin
|
|
7
8
|
module GraphQL
|
|
8
9
|
# Per-resource GraphQL options set via +graphql+ in +ActiveAdmin.register+.
|
|
9
10
|
class ResourceConfig
|
|
11
|
+
include ResourceCapabilityMaps
|
|
12
|
+
|
|
10
13
|
attr_accessor :enabled
|
|
14
|
+
attr_accessor :queries_enabled
|
|
15
|
+
attr_accessor :mutations_enabled
|
|
16
|
+
attr_accessor :run_actions_enabled
|
|
17
|
+
attr_accessor :policies_enabled
|
|
11
18
|
attr_accessor :graphql_type_name
|
|
12
19
|
attr_accessor :only_attributes
|
|
13
20
|
attr_accessor :exclude_attributes
|
|
14
21
|
attr_accessor :permit_params_attributes
|
|
15
22
|
attr_accessor :html_permit_params_attributes
|
|
16
23
|
attr_accessor :extension_block
|
|
24
|
+
attr_accessor :identity_columns
|
|
25
|
+
attr_accessor :identity_separator
|
|
26
|
+
attr_accessor :identity_encode_proc
|
|
27
|
+
attr_accessor :identity_decode_proc
|
|
17
28
|
|
|
18
29
|
# Optional resolver overrides (set from +graphql do+). SchemaBuilder still owns field names,
|
|
19
30
|
# arguments, and types; procs replace only the Ruby resolution body.
|
|
@@ -27,8 +38,18 @@ module ActiveAdmin
|
|
|
27
38
|
# {RunActionMutationConfig#payload_type} is not set. Falls back to {RunActionPayload}.
|
|
28
39
|
attr_accessor :run_action_payload_type
|
|
29
40
|
|
|
30
|
-
def initialize
|
|
31
|
-
|
|
41
|
+
def initialize(
|
|
42
|
+
enabled: true,
|
|
43
|
+
queries_enabled: true,
|
|
44
|
+
mutations_enabled: true,
|
|
45
|
+
run_actions_enabled: true,
|
|
46
|
+
policies_enabled: true
|
|
47
|
+
)
|
|
48
|
+
@enabled = enabled
|
|
49
|
+
@queries_enabled = queries_enabled
|
|
50
|
+
@mutations_enabled = mutations_enabled
|
|
51
|
+
@run_actions_enabled = run_actions_enabled
|
|
52
|
+
@policies_enabled = policies_enabled
|
|
32
53
|
@exclude_attributes = []
|
|
33
54
|
end
|
|
34
55
|
|
|
@@ -3,17 +3,27 @@
|
|
|
3
3
|
require_relative "run_action_mutation_config"
|
|
4
4
|
require_relative "run_action_mutation_dsl"
|
|
5
5
|
require_relative "mutation_input_definition_dsl"
|
|
6
|
+
require_relative "resource_surface_dsl"
|
|
7
|
+
require_relative "resource_capability_dsl"
|
|
6
8
|
|
|
7
9
|
module ActiveAdmin
|
|
8
10
|
module GraphQL
|
|
9
11
|
# DSL for +graphql do ... end+ inside +ActiveAdmin.register+.
|
|
10
12
|
class ResourceDefinitionDSL
|
|
13
|
+
include ResourceSurfaceDSL
|
|
14
|
+
include ResourceCapabilityDSL
|
|
15
|
+
|
|
11
16
|
def initialize(config)
|
|
12
17
|
@config = config
|
|
13
18
|
end
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
# Declare GraphQL identity columns when ActiveRecord has no primary key / id column.
|
|
21
|
+
# See RFC 0007.
|
|
22
|
+
def identity(*columns, separator: nil, encode: nil, decode: nil)
|
|
23
|
+
@config.identity_columns = columns.flatten.map(&:to_s)
|
|
24
|
+
@config.identity_separator = separator
|
|
25
|
+
@config.identity_encode_proc = encode
|
|
26
|
+
@config.identity_decode_proc = decode
|
|
17
27
|
end
|
|
18
28
|
|
|
19
29
|
def type_name(name)
|