activeadmin-graphql 0.1.1 → 0.2.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 +14 -0
- data/app/controllers/active_admin/graphql_controller.rb +47 -28
- data/docs/graphql-api.md +28 -4
- data/lib/active_admin/graphql/integration.rb +33 -13
- data/lib/active_admin/graphql/key_value_pair_input.rb +20 -12
- data/lib/active_admin/graphql/record_source.rb +24 -11
- data/lib/active_admin/graphql/resource_query_proxy/controller.rb +22 -6
- data/lib/active_admin/graphql/resource_query_proxy.rb +12 -1
- data/lib/active_admin/graphql/run_action_mutation_config.rb +2 -0
- data/lib/active_admin/graphql/run_action_mutation_dsl.rb +4 -0
- data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +9 -1
- data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +11 -2
- data/lib/active_admin/graphql/schema_builder/mutation_member.rb +11 -2
- data/lib/active_admin/graphql/schema_builder/query_type.rb +7 -0
- data/lib/active_admin/graphql/schema_builder/query_type_policies.rb +148 -0
- data/lib/active_admin/graphql/schema_builder/types_object.rb +24 -2
- data/lib/active_admin/graphql/schema_builder.rb +1 -0
- data/lib/active_admin/graphql/schema_field.rb +21 -1
- data/lib/active_admin/graphql/version.rb +1 -1
- data/lib/active_admin/graphql.rb +29 -15
- data/lib/active_admin/primary_key.rb +61 -39
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 15355b8548053d83cb751fc238ff2f9d4c910be902cf3aab25c2c91741a33fa9
|
|
4
|
+
data.tar.gz: 8d76e29dcfe43079b171d19b817d52f13327c286a234688dd32c7e04efcc1522
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a8a5472cfd32ac45a662733ee46fe1b0f918ea96e71db5bdf25ff9191a3c808c7b9ec047b4e4e2e0b52269a2a3aba20ff7d5cce39607bdf28448f85139beff39
|
|
7
|
+
data.tar.gz: 873a5814296d27af34ac8a6742ada804d61e4f855c33d0b5a092793ec7260625c68c54d8831977b88e59a7d7299a5cb22f77b502ab81b504e2d523b01e505cae
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 (2026-04-29)
|
|
4
|
+
|
|
5
|
+
- Add `activeadmin_policies` GraphQL policy surfaces:
|
|
6
|
+
- global `activeadmin_policies` for resources/pages
|
|
7
|
+
- per-object `activeadmin_policies` on resource objects
|
|
8
|
+
- preflight `activeadmin_policies_for(type_name:, ids:, path:)` for per-record checks before running queries/mutations/actions
|
|
9
|
+
- Switch policy payloads to allow-lists (`allowed_actions`, `allowed_member_actions`, `allowed_collection_actions`, `allowed_batch_actions`) and add namespace customization hooks (`graphql_policy_actions`, `graphql_policy_action_mapper`, `graphql_policy_extra`, `graphql_policy_transform`)
|
|
10
|
+
- Enforce authorization-by-default for custom GraphQL fields/mutations with explicit opt-out (`authorize: false` / mutation DSL `authorize false`)
|
|
11
|
+
- Add namespace defaults/settings and request specs covering policies, customization hooks, and auth-toggle behavior
|
|
12
|
+
|
|
13
|
+
## 0.1.2 (2026-03-30)
|
|
14
|
+
|
|
15
|
+
- Fix TruffleRuby compatibility issue with JSON.dump
|
|
16
|
+
|
|
3
17
|
## 0.1.1 (2026-03-30)
|
|
4
18
|
|
|
5
19
|
- Fix repository URL in gemspec
|
|
@@ -14,35 +14,9 @@ module ActiveAdmin
|
|
|
14
14
|
|
|
15
15
|
def execute
|
|
16
16
|
schema = ActiveAdmin::GraphQL.schema_for(active_admin_namespace)
|
|
17
|
+
return render_multiplex(schema) if multiplex_request?
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
operations = multiplex_operations
|
|
20
|
-
max_n = active_admin_namespace.graphql_multiplex_max || 20
|
|
21
|
-
if operations.size > max_n
|
|
22
|
-
return render json: {errors: [{message: "Multiplex exceeds maximum of #{max_n}"}]},
|
|
23
|
-
status: :content_too_large
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
results = schema.multiplex(
|
|
27
|
-
operations.map do |op|
|
|
28
|
-
{
|
|
29
|
-
query: op[:query],
|
|
30
|
-
variables: ensure_variables(op[:variables]),
|
|
31
|
-
operation_name: op[:operation_name],
|
|
32
|
-
context: graphql_context
|
|
33
|
-
}
|
|
34
|
-
end
|
|
35
|
-
)
|
|
36
|
-
render json: results.map(&:to_h), status: :ok
|
|
37
|
-
else
|
|
38
|
-
result = schema.execute(
|
|
39
|
-
query: query_string,
|
|
40
|
-
variables: ensure_variables(variables_hash),
|
|
41
|
-
operation_name: operation_name,
|
|
42
|
-
context: graphql_context
|
|
43
|
-
)
|
|
44
|
-
render json: result.to_h, status: :ok
|
|
45
|
-
end
|
|
19
|
+
render_single(schema)
|
|
46
20
|
end
|
|
47
21
|
|
|
48
22
|
def active_admin_namespace
|
|
@@ -164,5 +138,50 @@ module ActiveAdmin
|
|
|
164
138
|
rescue JSON::ParserError
|
|
165
139
|
{}
|
|
166
140
|
end
|
|
141
|
+
|
|
142
|
+
def render_multiplex(schema)
|
|
143
|
+
operations = multiplex_operations
|
|
144
|
+
return render_multiplex_limit_error!(operations.size) if exceeds_multiplex_limit?(operations)
|
|
145
|
+
|
|
146
|
+
payloads = build_multiplex_payloads(operations)
|
|
147
|
+
results = schema.multiplex(payloads)
|
|
148
|
+
render json: results.map(&:to_h), status: :ok
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def render_single(schema)
|
|
152
|
+
result = schema.execute(
|
|
153
|
+
query: query_string,
|
|
154
|
+
variables: ensure_variables(variables_hash),
|
|
155
|
+
operation_name: operation_name,
|
|
156
|
+
context: graphql_context
|
|
157
|
+
)
|
|
158
|
+
render json: result.to_h, status: :ok
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def exceeds_multiplex_limit?(operations)
|
|
162
|
+
operations.size > max_multiplex_operations
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def max_multiplex_operations
|
|
166
|
+
active_admin_namespace.graphql_multiplex_max || 20
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def render_multiplex_limit_error!(count)
|
|
170
|
+
max_n = max_multiplex_operations
|
|
171
|
+
msg = "Multiplex exceeds maximum of #{max_n} (received #{count})"
|
|
172
|
+
render json: {errors: [{message: msg}]}, status: :content_too_large
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def build_multiplex_payloads(operations)
|
|
176
|
+
context = graphql_context
|
|
177
|
+
operations.map do |op|
|
|
178
|
+
{
|
|
179
|
+
query: op[:query],
|
|
180
|
+
variables: ensure_variables(op[:variables]),
|
|
181
|
+
operation_name: op[:operation_name],
|
|
182
|
+
context: context
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
end
|
|
167
186
|
end
|
|
168
187
|
end
|
data/docs/graphql-api.md
CHANGED
|
@@ -46,10 +46,16 @@ Each namespace inherits these options (defaults in parentheses):
|
|
|
46
46
|
* `graphql_path` — path segment under the namespace mount (`"graphql"`).
|
|
47
47
|
* `graphql_multiplex_max` — maximum operations per multiplexed JSON array
|
|
48
48
|
request (`20`).
|
|
49
|
-
* `graphql_max_complexity` — passed to graphql-ruby `max_complexity`
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
* `graphql_max_complexity` — passed to graphql-ruby `max_complexity` (`300` by
|
|
50
|
+
default; set a higher value if legitimate queries are rejected). Set to
|
|
51
|
+
`nil` to disable the limit.
|
|
52
|
+
* `graphql_max_depth` — passed to `max_depth` (`18` by default). Set to `nil`
|
|
53
|
+
to disable.
|
|
54
|
+
* `graphql_batch_action_max_ids` — maximum length of the `ids` argument on
|
|
55
|
+
`*_batch_action` mutations (`5000` by default; set `0` to disable the cap).
|
|
56
|
+
* `graphql_default_page_size` / `graphql_default_max_page_size` — schema
|
|
57
|
+
connection defaults (`25` / `100`); needed for graphql-ruby complexity scoring
|
|
58
|
+
on connection fields when `max_complexity` is enabled.
|
|
53
59
|
* `graphql_dataloader` — graphql-ruby dataloader plugin (`nil` = use
|
|
54
60
|
`GraphQL::Dataloader`). Set to `GraphQL::Dataloader::AsyncDataloader` when
|
|
55
61
|
the [`async` gem](https://github.com/socketry/async) is loaded if you want
|
|
@@ -155,6 +161,24 @@ separate route with CSRF disabled. Treat this endpoint like any other
|
|
|
155
161
|
The namespace `authentication_method` runs before any GraphQL work, including
|
|
156
162
|
schema introspection. Unauthenticated clients should not receive a schema.
|
|
157
163
|
|
|
164
|
+
If you need to turn off schema introspection entirely (even for authenticated
|
|
165
|
+
sessions or specific environments), call
|
|
166
|
+
[`schema.disable_introspection_entry_points`](https://graphql-ruby.org/schema/definition.html#schema-configuration)
|
|
167
|
+
from `graphql_configure_schema`. The hook receives the generated schema class,
|
|
168
|
+
so you can toggle introspection based on Rails environment or other app
|
|
169
|
+
settings:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
ActiveAdmin.setup do |config|
|
|
173
|
+
config.namespace :admin do |admin|
|
|
174
|
+
admin.graphql = true
|
|
175
|
+
admin.graphql_configure_schema = lambda do |schema|
|
|
176
|
+
schema.disable_introspection_entry_points if Rails.env.production?
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
158
182
|
The GraphQL context exposes the same user as `current_active_admin_user` (from
|
|
159
183
|
the namespace `current_user_method`), wrapped for authorization checks.
|
|
160
184
|
|
|
@@ -31,11 +31,18 @@ module ActiveAdmin
|
|
|
31
31
|
ns.register :graphql_visibility, nil
|
|
32
32
|
ns.register :graphql_visibility_profile, nil
|
|
33
33
|
ns.register :graphql_schema_visible, nil
|
|
34
|
-
ns.register :graphql_max_complexity,
|
|
35
|
-
ns.register :graphql_max_depth,
|
|
36
|
-
ns.register :
|
|
37
|
-
ns.register :
|
|
34
|
+
ns.register :graphql_max_complexity, 300
|
|
35
|
+
ns.register :graphql_max_depth, 18
|
|
36
|
+
ns.register :graphql_batch_action_max_ids, 5000
|
|
37
|
+
ns.register :graphql_default_page_size, 25
|
|
38
|
+
ns.register :graphql_default_max_page_size, 100
|
|
38
39
|
ns.register :graphql_configure_schema, nil
|
|
40
|
+
ns.register :graphql_policy_actions, nil
|
|
41
|
+
ns.register :graphql_policy_action_mapper, nil
|
|
42
|
+
ns.register :graphql_policy_extra, nil
|
|
43
|
+
ns.register :graphql_policy_transform, nil
|
|
44
|
+
ns.register :graphql_custom_field_authorization_default, true
|
|
45
|
+
ns.register :graphql_custom_mutation_authorization_default, true
|
|
39
46
|
end
|
|
40
47
|
|
|
41
48
|
def register_railtie!
|
|
@@ -114,18 +121,31 @@ module ActiveAdmin
|
|
|
114
121
|
private
|
|
115
122
|
|
|
116
123
|
def define_graphql_routes
|
|
117
|
-
|
|
118
|
-
|
|
124
|
+
graphql_namespaces.each do |namespace|
|
|
125
|
+
segment = graphql_segment_for(namespace)
|
|
126
|
+
defaults = graphql_route_defaults(namespace)
|
|
127
|
+
mount_graphql_route(namespace, segment, defaults)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def graphql_namespaces
|
|
132
|
+
namespaces.select(&:graphql)
|
|
133
|
+
end
|
|
119
134
|
|
|
120
|
-
|
|
121
|
-
|
|
135
|
+
def graphql_segment_for(namespace)
|
|
136
|
+
namespace.graphql_path.to_s.delete_prefix("/").presence || "graphql"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def graphql_route_defaults(namespace)
|
|
140
|
+
{active_admin_namespace: namespace.name}
|
|
141
|
+
end
|
|
122
142
|
|
|
123
|
-
|
|
143
|
+
def mount_graphql_route(namespace, segment, defaults)
|
|
144
|
+
if namespace.root?
|
|
145
|
+
router.post segment, controller: "/active_admin/graphql", action: "execute", defaults: defaults
|
|
146
|
+
else
|
|
147
|
+
router.namespace namespace.name, **namespace.route_options.dup do
|
|
124
148
|
router.post segment, controller: "/active_admin/graphql", action: "execute", defaults: defaults
|
|
125
|
-
else
|
|
126
|
-
router.namespace namespace.name, **namespace.route_options.dup do
|
|
127
|
-
router.post segment, controller: "/active_admin/graphql", action: "execute", defaults: defaults
|
|
128
|
-
end
|
|
129
149
|
end
|
|
130
150
|
end
|
|
131
151
|
end
|
|
@@ -30,18 +30,26 @@ module ActiveAdmin
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def extract_pair(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
def extract_pair(entry)
|
|
34
|
+
return extract_from_struct(entry) if key_value_struct?(entry)
|
|
35
|
+
return extract_from_hash(entry) if entry.is_a?(Hash)
|
|
36
|
+
|
|
37
|
+
raise ::GraphQL::ExecutionError, "invalid key/value pair entry: #{entry.class}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def key_value_struct?(entry)
|
|
41
|
+
entry.respond_to?(:key) && entry.respond_to?(:value) && !entry.is_a?(Hash)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def extract_from_struct(entry)
|
|
45
|
+
[entry.key, entry.value]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def extract_from_hash(entry)
|
|
49
|
+
key = entry["key"] || entry[:key]
|
|
50
|
+
raise ::GraphQL::ExecutionError, "key/value pair missing key" if key.nil?
|
|
51
|
+
|
|
52
|
+
[key, entry["value"] || entry[:value]]
|
|
45
53
|
end
|
|
46
54
|
end
|
|
47
55
|
end
|
|
@@ -10,21 +10,34 @@ module ActiveAdmin
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def fetch(ids)
|
|
13
|
-
keys = ids
|
|
13
|
+
keys = normalized_keys(ids)
|
|
14
14
|
uniq = keys.compact.uniq
|
|
15
|
-
return ids.
|
|
15
|
+
return Array.new(ids.size) { nil } if uniq.empty?
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
by_id = records.index_by { |r| r.public_send(pk_sym) }
|
|
25
|
-
keys.map { |k| k.nil? ? nil : by_id[k] }
|
|
17
|
+
indexed = load_records(uniq)
|
|
18
|
+
keys.map { |key| key.nil? ? nil : indexed[key] }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def normalized_keys(ids)
|
|
22
|
+
ids.map do |raw|
|
|
23
|
+
raw.nil? ? nil : ActiveAdmin::PrimaryKey.dataloader_tuple(@model_class, raw)
|
|
26
24
|
end
|
|
27
25
|
end
|
|
26
|
+
|
|
27
|
+
def load_records(keys)
|
|
28
|
+
ActiveAdmin::PrimaryKey.composite?(@model_class) ? composite_index(keys) : single_column_index(keys)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def composite_index(keys)
|
|
32
|
+
records = @model_class.where(@pk => keys).to_a
|
|
33
|
+
records.index_by { |record| ActiveAdmin::PrimaryKey.dataloader_tuple_from_record(record) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def single_column_index(keys)
|
|
37
|
+
pk_sym = @pk.to_sym
|
|
38
|
+
records = @model_class.where(pk_sym => keys).to_a
|
|
39
|
+
records.index_by { |record| record.public_send(pk_sym) }
|
|
40
|
+
end
|
|
28
41
|
end
|
|
29
42
|
end
|
|
30
43
|
end
|
|
@@ -12,15 +12,24 @@ module ActiveAdmin
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def param_hash_for(action, extra)
|
|
15
|
+
base_graphql_params(action)
|
|
16
|
+
.merge(extra.stringify_keys)
|
|
17
|
+
.yield_self { |params| merge_graph_params!(params) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def base_graphql_params(action)
|
|
15
21
|
{
|
|
16
22
|
"action" => action,
|
|
17
23
|
"controller" => "active_admin/graphql"
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def merge_graph_params!(hash)
|
|
28
|
+
merge_ransack!(hash)
|
|
29
|
+
assign_if_present!(hash, "scope", @graph_params["scope"])
|
|
30
|
+
assign_if_present!(hash, "order", @graph_params["order"])
|
|
31
|
+
merge_belongs_to!(hash)
|
|
32
|
+
hash
|
|
24
33
|
end
|
|
25
34
|
|
|
26
35
|
def merge_ransack!(h)
|
|
@@ -50,6 +59,13 @@ module ActiveAdmin
|
|
|
50
59
|
h[key] = val.to_s if val.present?
|
|
51
60
|
end
|
|
52
61
|
|
|
62
|
+
def assign_if_present!(hash, key, value)
|
|
63
|
+
return hash if value.blank?
|
|
64
|
+
|
|
65
|
+
hash[key] = value.to_s
|
|
66
|
+
hash
|
|
67
|
+
end
|
|
68
|
+
|
|
53
69
|
def stub_controller!(controller, hash)
|
|
54
70
|
user = @user
|
|
55
71
|
params_obj = ActionController::Parameters.new(hash)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rack/mock"
|
|
4
|
+
require "stringio"
|
|
4
5
|
|
|
5
6
|
require_relative "resource_query_proxy/controller"
|
|
6
7
|
|
|
@@ -49,7 +50,9 @@ module ActiveAdmin
|
|
|
49
50
|
extra = {
|
|
50
51
|
"batch_action" => batch_sym.to_s,
|
|
51
52
|
"collection_selection" => Array(ids).map(&:to_s),
|
|
52
|
-
"
|
|
53
|
+
# TruffleRuby freezes JSON's internal "{}" buffer, so Hash#to_json raises FrozenError.
|
|
54
|
+
# Dumping into a dedicated StringIO avoids mutating the shared literal.
|
|
55
|
+
"batch_action_inputs" => dump_batch_inputs(inputs)
|
|
53
56
|
}
|
|
54
57
|
c = controller_for("batch_action", extra)
|
|
55
58
|
perform_controller_command!(c) { c.send(:batch_action) }
|
|
@@ -107,6 +110,14 @@ module ActiveAdmin
|
|
|
107
110
|
stub_controller!(c, h)
|
|
108
111
|
c
|
|
109
112
|
end
|
|
113
|
+
|
|
114
|
+
def dump_batch_inputs(hash)
|
|
115
|
+
buffer = StringIO.new
|
|
116
|
+
JSON.dump(hash, buffer)
|
|
117
|
+
buffer.string
|
|
118
|
+
rescue FrozenError
|
|
119
|
+
JSON.generate(hash)
|
|
120
|
+
end
|
|
110
121
|
end
|
|
111
122
|
end
|
|
112
123
|
end
|
|
@@ -12,6 +12,8 @@ module ActiveAdmin
|
|
|
12
12
|
attr_accessor :resolve_proc
|
|
13
13
|
# Optional block evaluated in the graphql-ruby +field+ DSL context (+argument+, …) for per-action fields.
|
|
14
14
|
attr_accessor :arguments_proc
|
|
15
|
+
# Optional authorization toggle for this mutation configuration. +nil+ means namespace default.
|
|
16
|
+
attr_accessor :authorize
|
|
15
17
|
|
|
16
18
|
def self.ensure_graphql_object_subclass!(type)
|
|
17
19
|
unless type.is_a?(Class) && type < ::GraphQL::Schema::Object
|
|
@@ -29,7 +29,9 @@ module ActiveAdmin
|
|
|
29
29
|
define_method(fname.to_sym) do |batch_action:, ids:, inputs: nil, **kw|
|
|
30
30
|
auth = context[:auth]
|
|
31
31
|
mdl = aa_res.resource_class
|
|
32
|
-
|
|
32
|
+
cfg = aa_res.graphql_config.batch_run_action
|
|
33
|
+
auth_enabled = cfg.authorize.nil? ? (ns.graphql_custom_mutation_authorization_default != false) : cfg.authorize
|
|
34
|
+
if auth_enabled && !auth.authorized?(aa_res, ActiveAdmin::Authorization::READ, mdl)
|
|
33
35
|
raise ::GraphQL::ExecutionError, "not authorized to read #{mdl.name}"
|
|
34
36
|
end
|
|
35
37
|
|
|
@@ -38,6 +40,12 @@ module ActiveAdmin
|
|
|
38
40
|
raise ::GraphQL::ExecutionError, "Unknown batch_action #{ba.inspect} for #{plural}"
|
|
39
41
|
end
|
|
40
42
|
|
|
43
|
+
max_ids = ns.graphql_batch_action_max_ids
|
|
44
|
+
if max_ids.is_a?(Integer) && max_ids.positive? && ids.size > max_ids
|
|
45
|
+
raise ::GraphQL::ExecutionError,
|
|
46
|
+
"ids cannot exceed #{max_ids} entries (received #{ids.size})"
|
|
47
|
+
end
|
|
48
|
+
|
|
41
49
|
proxy = ResourceQueryProxy.new(
|
|
42
50
|
aa_resource: aa_res,
|
|
43
51
|
user: context[:auth].user,
|
|
@@ -33,7 +33,9 @@ module ActiveAdmin
|
|
|
33
33
|
define_method(fname.to_sym) do |action:, params: nil, **kw|
|
|
34
34
|
auth = context[:auth]
|
|
35
35
|
mdl = aa_res.resource_class
|
|
36
|
-
|
|
36
|
+
cfg = aa_res.graphql_config.collection_run_action
|
|
37
|
+
auth_enabled = cfg.authorize.nil? ? (ns.graphql_custom_mutation_authorization_default != false) : cfg.authorize
|
|
38
|
+
if auth_enabled && !auth.authorized?(aa_res, ActiveAdmin::Authorization::READ, mdl)
|
|
37
39
|
raise ::GraphQL::ExecutionError, "not authorized to read #{mdl.name}"
|
|
38
40
|
end
|
|
39
41
|
|
|
@@ -91,7 +93,14 @@ module ActiveAdmin
|
|
|
91
93
|
define_method(fname) do |params: nil, **kw|
|
|
92
94
|
auth = context[:auth]
|
|
93
95
|
mdl = aa_res.resource_class
|
|
94
|
-
|
|
96
|
+
auth_enabled =
|
|
97
|
+
if per_cfg&.authorize.nil?
|
|
98
|
+
base_cfg = aa_res.graphql_config.collection_run_action
|
|
99
|
+
base_cfg.authorize.nil? ? (ns.graphql_custom_mutation_authorization_default != false) : base_cfg.authorize
|
|
100
|
+
else
|
|
101
|
+
per_cfg.authorize
|
|
102
|
+
end
|
|
103
|
+
if auth_enabled && !auth.authorized?(aa_res, ActiveAdmin::Authorization::READ, mdl)
|
|
95
104
|
raise ::GraphQL::ExecutionError, "not authorized to read #{mdl.name}"
|
|
96
105
|
end
|
|
97
106
|
|
|
@@ -34,7 +34,9 @@ module ActiveAdmin
|
|
|
34
34
|
define_method(fname.to_sym) do |action:, id:, params: nil, **kw|
|
|
35
35
|
auth = context[:auth]
|
|
36
36
|
mdl = aa_res.resource_class
|
|
37
|
-
|
|
37
|
+
cfg = aa_res.graphql_config.member_run_action
|
|
38
|
+
auth_enabled = cfg.authorize.nil? ? (ns.graphql_custom_mutation_authorization_default != false) : cfg.authorize
|
|
39
|
+
if auth_enabled && !auth.authorized?(aa_res, ActiveAdmin::Authorization::READ, mdl)
|
|
38
40
|
raise ::GraphQL::ExecutionError, "not authorized to read #{mdl.name}"
|
|
39
41
|
end
|
|
40
42
|
|
|
@@ -94,7 +96,14 @@ module ActiveAdmin
|
|
|
94
96
|
define_method(fname) do |id:, params: nil, **kw|
|
|
95
97
|
auth = context[:auth]
|
|
96
98
|
mdl = aa_res.resource_class
|
|
97
|
-
|
|
99
|
+
auth_enabled =
|
|
100
|
+
if per_cfg&.authorize.nil?
|
|
101
|
+
base_cfg = aa_res.graphql_config.member_run_action
|
|
102
|
+
base_cfg.authorize.nil? ? (ns.graphql_custom_mutation_authorization_default != false) : base_cfg.authorize
|
|
103
|
+
else
|
|
104
|
+
per_cfg.authorize
|
|
105
|
+
end
|
|
106
|
+
if auth_enabled && !auth.authorized?(aa_res, ActiveAdmin::Authorization::READ, mdl)
|
|
98
107
|
raise ::GraphQL::ExecutionError, "not authorized to read #{mdl.name}"
|
|
99
108
|
end
|
|
100
109
|
|
|
@@ -8,6 +8,7 @@ module ActiveAdmin
|
|
|
8
8
|
include QueryTypeCollection
|
|
9
9
|
include QueryTypeMember
|
|
10
10
|
include QueryTypePages
|
|
11
|
+
include QueryTypePolicies
|
|
11
12
|
|
|
12
13
|
def build_query_type(registered_resource_union:)
|
|
13
14
|
builder = self
|
|
@@ -45,6 +46,12 @@ module ActiveAdmin
|
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
builder.add_page_query_fields!(self, builder: builder, ns: ns)
|
|
49
|
+
builder.add_activeadmin_policies_query_field!(
|
|
50
|
+
self,
|
|
51
|
+
builder: builder,
|
|
52
|
+
ns: ns,
|
|
53
|
+
aa_by_graphql_type_name: aa_by_graphql_type_name
|
|
54
|
+
)
|
|
48
55
|
end
|
|
49
56
|
end
|
|
50
57
|
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
class SchemaBuilder
|
|
6
|
+
module QueryTypePolicies
|
|
7
|
+
POLICY_ACTIONS = {
|
|
8
|
+
read: ActiveAdmin::Authorization::READ,
|
|
9
|
+
create: ActiveAdmin::Authorization::CREATE,
|
|
10
|
+
update: ActiveAdmin::Authorization::UPDATE,
|
|
11
|
+
destroy: ActiveAdmin::Authorization::DESTROY
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def add_activeadmin_policies_query_field!(query_class, builder:, ns:, aa_by_graphql_type_name:)
|
|
15
|
+
resource_policy_type = Class.new(::GraphQL::Schema::Object) do
|
|
16
|
+
graphql_name "ActiveAdminResourcePolicies"
|
|
17
|
+
field :type_name, ::GraphQL::Types::String, null: false, camelize: false
|
|
18
|
+
field :activeadmin_policies, builder.send(:policy_set_type), null: false, camelize: false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
page_policy_type = Class.new(::GraphQL::Schema::Object) do
|
|
22
|
+
graphql_name "ActiveAdminPagePolicies"
|
|
23
|
+
field :name, ::GraphQL::Types::String, null: false, camelize: false
|
|
24
|
+
field :activeadmin_policies, builder.send(:policy_set_type), null: false, camelize: false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
record_policy_type = Class.new(::GraphQL::Schema::Object) do
|
|
28
|
+
graphql_name "ActiveAdminRecordPolicies"
|
|
29
|
+
field :id, ::GraphQL::Types::ID, null: false, camelize: false
|
|
30
|
+
field :activeadmin_policies, builder.send(:policy_set_type), null: false, camelize: false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
matrix_type = Class.new(::GraphQL::Schema::Object) do
|
|
34
|
+
graphql_name "ActiveAdminPolicies"
|
|
35
|
+
field :resources, [resource_policy_type], null: false, camelize: false
|
|
36
|
+
field :pages, [page_policy_type], null: false, camelize: false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
query_class.class_eval do
|
|
40
|
+
field :activeadmin_policies, matrix_type, null: false, camelize: false,
|
|
41
|
+
visibility: {kind: :query_activeadmin_policies}
|
|
42
|
+
|
|
43
|
+
field :activeadmin_policies_for, [record_policy_type], null: false, camelize: false,
|
|
44
|
+
visibility: {kind: :query_activeadmin_policies_for} do
|
|
45
|
+
argument :type_name, ::GraphQL::Types::String, required: true, camelize: false
|
|
46
|
+
argument :ids, [::GraphQL::Types::ID], required: true, camelize: false
|
|
47
|
+
argument :path, [KeyValuePairInput], required: false, camelize: false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
define_method(:activeadmin_policies) do
|
|
51
|
+
auth = context[:auth]
|
|
52
|
+
resources = builder.send(:active_resources).map do |aa_res|
|
|
53
|
+
{
|
|
54
|
+
type_name: builder.send(:graphql_type_name_for, aa_res),
|
|
55
|
+
activeadmin_policies: builder.send(:build_policy_set, auth: auth, subject_owner: aa_res, subject: aa_res.resource_class, context: context)
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
pages = ns.resources.select { |r| r.is_a?(ActiveAdmin::Page) }.map do |page|
|
|
59
|
+
{
|
|
60
|
+
name: page.name,
|
|
61
|
+
activeadmin_policies: builder.send(:build_policy_set, auth: auth, subject_owner: page, subject: page, context: context)
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
{resources: resources, pages: pages}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
define_method(:activeadmin_policies_for) do |type_name:, ids:, path: nil|
|
|
68
|
+
aa_res = aa_by_graphql_type_name[type_name.to_s]
|
|
69
|
+
raise ::GraphQL::ExecutionError, "unknown resource type_name #{type_name.inspect}" unless aa_res
|
|
70
|
+
|
|
71
|
+
proxy = ResourceQueryProxy.new(
|
|
72
|
+
aa_resource: aa_res,
|
|
73
|
+
user: context[:auth].user,
|
|
74
|
+
namespace: ns,
|
|
75
|
+
graph_params: KeyValuePairs.to_hash(path)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
ids.map do |id|
|
|
79
|
+
record = proxy.find_member(id.to_s)
|
|
80
|
+
raise ::GraphQL::ExecutionError, "not found" unless record
|
|
81
|
+
|
|
82
|
+
{
|
|
83
|
+
id: id.to_s,
|
|
84
|
+
activeadmin_policies: builder.send(:build_policy_set, auth: context[:auth], subject_owner: aa_res, subject: record, context: context)
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def policy_action_pairs
|
|
94
|
+
hook = @namespace.graphql_policy_actions
|
|
95
|
+
if hook.respond_to?(:call)
|
|
96
|
+
raw = hook.call(@namespace)
|
|
97
|
+
return raw.to_h.transform_keys(&:to_sym) if raw.respond_to?(:to_h)
|
|
98
|
+
end
|
|
99
|
+
POLICY_ACTIONS
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def allowed_base_actions(auth:, subject_owner:, subject:, context:)
|
|
103
|
+
action_mapper = @namespace.graphql_policy_action_mapper
|
|
104
|
+
policy_action_pairs.each_with_object([]) do |(name, default_action), out|
|
|
105
|
+
allowed = if action_mapper.respond_to?(:call)
|
|
106
|
+
action_mapper.call(context, subject_owner, subject, name, default_action)
|
|
107
|
+
else
|
|
108
|
+
auth.authorized?(subject_owner, default_action, subject)
|
|
109
|
+
end
|
|
110
|
+
out << name.to_s if allowed
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_policy_set(auth:, subject_owner:, subject:, context:)
|
|
115
|
+
base = {
|
|
116
|
+
allowed_actions: allowed_base_actions(auth: auth, subject_owner: subject_owner, subject: subject, context: context),
|
|
117
|
+
allowed_member_actions: [],
|
|
118
|
+
allowed_collection_actions: [],
|
|
119
|
+
allowed_batch_actions: []
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if subject_owner.is_a?(ActiveAdmin::Resource)
|
|
123
|
+
class_subject = subject_owner.resource_class
|
|
124
|
+
if subject.is_a?(Class)
|
|
125
|
+
if auth.authorized?(subject_owner, ActiveAdmin::Authorization::READ, class_subject)
|
|
126
|
+
base[:allowed_member_actions] = subject_owner.member_actions.map { |a| a.name.to_s }
|
|
127
|
+
base[:allowed_collection_actions] = subject_owner.collection_actions.map { |a| a.name.to_s }
|
|
128
|
+
base[:allowed_batch_actions] = subject_owner.batch_actions.map { |a| a.sym.to_s }
|
|
129
|
+
end
|
|
130
|
+
elsif auth.authorized?(subject_owner, ActiveAdmin::Authorization::READ, subject)
|
|
131
|
+
base[:allowed_member_actions] = subject_owner.member_actions.map { |a| a.name.to_s }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
if (extra = @namespace.graphql_policy_extra).respond_to?(:call)
|
|
136
|
+
out = extra.call(context, subject_owner, subject, base)
|
|
137
|
+
base = out if out.is_a?(Hash)
|
|
138
|
+
end
|
|
139
|
+
if (transform = @namespace.graphql_policy_transform).respond_to?(:call)
|
|
140
|
+
out = transform.call(context, subject_owner, subject, base)
|
|
141
|
+
base = out if out.is_a?(Hash)
|
|
142
|
+
end
|
|
143
|
+
base
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -6,6 +6,19 @@ module ActiveAdmin
|
|
|
6
6
|
module TypesObject
|
|
7
7
|
private
|
|
8
8
|
|
|
9
|
+
def policy_set_type
|
|
10
|
+
@policy_set_type ||= Class.new(::GraphQL::Schema::Object) do
|
|
11
|
+
graphql_name "ActiveAdminPolicySet"
|
|
12
|
+
description "Allowed ActiveAdmin actions for this subject."
|
|
13
|
+
|
|
14
|
+
field :allowed_actions, [::GraphQL::Types::String], null: false, camelize: false
|
|
15
|
+
field :allowed_member_actions, [::GraphQL::Types::String], null: false, camelize: false
|
|
16
|
+
field :allowed_collection_actions, [::GraphQL::Types::String], null: false, camelize: false
|
|
17
|
+
field :allowed_batch_actions, [::GraphQL::Types::String], null: false, camelize: false
|
|
18
|
+
field :extra, ::GraphQL::Types::JSON, null: true, camelize: false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
9
22
|
def build_enum_type(aa_res, column_name, mapping)
|
|
10
23
|
key = [aa_res.resource_class.name, column_name.to_s]
|
|
11
24
|
return @enum_types[key] if @enum_types[key]
|
|
@@ -61,14 +74,17 @@ module ActiveAdmin
|
|
|
61
74
|
attr_names = attributes_for(aa_res).map(&:to_s)
|
|
62
75
|
|
|
63
76
|
type_class = Class.new(::GraphQL::Schema::Object) do
|
|
77
|
+
field_class ::ActiveAdmin::GraphQL::SchemaField
|
|
64
78
|
graphql_name gname
|
|
65
79
|
description "ActiveAdmin resource `#{aa_res.resource_name}`"
|
|
66
80
|
implements ::ActiveAdmin::GraphQL::ResourceInterface
|
|
81
|
+
|
|
82
|
+
define_singleton_method(:activeadmin_graphql_resource) { aa_res }
|
|
67
83
|
end
|
|
68
84
|
|
|
69
85
|
pk_cols = ActiveAdmin::PrimaryKey.columns(model)
|
|
70
86
|
|
|
71
|
-
type_class.field :id, ::GraphQL::Types::ID, null: false
|
|
87
|
+
type_class.field :id, ::GraphQL::Types::ID, null: false, authorize: false
|
|
72
88
|
type_class.define_method(:id) { ActiveAdmin::PrimaryKey.graphql_id_value(object) }
|
|
73
89
|
|
|
74
90
|
attr_names.each do |name|
|
|
@@ -78,11 +94,17 @@ module ActiveAdmin
|
|
|
78
94
|
next unless col
|
|
79
95
|
|
|
80
96
|
gql_t = graphql_scalar_for_column(aa_res, model, col)
|
|
81
|
-
type_class.field(name.to_sym, gql_t, null: true, camelize: false)
|
|
97
|
+
type_class.field(name.to_sym, gql_t, null: true, camelize: false, authorize: false)
|
|
82
98
|
|
|
83
99
|
type_class.define_method(name.to_sym) { object.public_send(name) }
|
|
84
100
|
end
|
|
85
101
|
|
|
102
|
+
type_class.field :activeadmin_policies, policy_set_type, null: false, camelize: false, authorize: false
|
|
103
|
+
type_class.define_method(:activeadmin_policies) do
|
|
104
|
+
builder = context[:namespace] && ActiveAdmin::GraphQL::SchemaBuilder.new(context[:namespace])
|
|
105
|
+
builder.send(:build_policy_set, auth: context[:auth], subject_owner: aa_res, subject: object, context: context)
|
|
106
|
+
end
|
|
107
|
+
|
|
86
108
|
if (ext = aa_res.graphql_config.extension_block)
|
|
87
109
|
type_class.class_eval(&ext)
|
|
88
110
|
end
|
|
@@ -17,6 +17,7 @@ require_relative "schema_builder/query_type_registered"
|
|
|
17
17
|
require_relative "schema_builder/query_type_collection"
|
|
18
18
|
require_relative "schema_builder/query_type_member"
|
|
19
19
|
require_relative "schema_builder/query_type_pages"
|
|
20
|
+
require_relative "schema_builder/query_type_policies"
|
|
20
21
|
require_relative "schema_builder/query_type"
|
|
21
22
|
require_relative "schema_builder/resolvers"
|
|
22
23
|
require_relative "schema_builder/mutation_action_types"
|
|
@@ -9,8 +9,10 @@ module ActiveAdmin
|
|
|
9
9
|
# That hook runs from {#visible?}; it does not replace graphql-ruby's visibility system—it
|
|
10
10
|
# composes with +super+ like any custom +Field+ class.
|
|
11
11
|
class SchemaField < ::GraphQL::Schema::Field
|
|
12
|
-
def initialize(visibility: nil, **kwargs, &block)
|
|
12
|
+
def initialize(visibility: nil, authorize: nil, authorize_action: nil, **kwargs, &block)
|
|
13
13
|
@visibility = visibility
|
|
14
|
+
@authorize = authorize
|
|
15
|
+
@authorize_action = authorize_action
|
|
14
16
|
super(**kwargs, &block)
|
|
15
17
|
end
|
|
16
18
|
|
|
@@ -24,6 +26,24 @@ module ActiveAdmin
|
|
|
24
26
|
|
|
25
27
|
!!hook.call(ctx, @visibility)
|
|
26
28
|
end
|
|
29
|
+
|
|
30
|
+
def authorized?(object, args, ctx)
|
|
31
|
+
return false unless super
|
|
32
|
+
|
|
33
|
+
owner = self.owner
|
|
34
|
+
aa_resource = owner.respond_to?(:activeadmin_graphql_resource) ? owner.activeadmin_graphql_resource : nil
|
|
35
|
+
return true unless aa_resource
|
|
36
|
+
|
|
37
|
+
enabled = if @authorize.nil?
|
|
38
|
+
ctx[:namespace]&.graphql_custom_field_authorization_default != false
|
|
39
|
+
else
|
|
40
|
+
@authorize
|
|
41
|
+
end
|
|
42
|
+
return true unless enabled
|
|
43
|
+
|
|
44
|
+
action = @authorize_action || ActiveAdmin::Authorization::READ
|
|
45
|
+
!!ctx[:auth]&.authorized?(aa_resource, action, object)
|
|
46
|
+
end
|
|
27
47
|
end
|
|
28
48
|
end
|
|
29
49
|
end
|
data/lib/active_admin/graphql.rb
CHANGED
|
@@ -28,27 +28,19 @@ module ActiveAdmin
|
|
|
28
28
|
|
|
29
29
|
@graphql_features_loaded = true
|
|
30
30
|
ActiveAdmin::Dependency["graphql"].spec!
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
require "graphql/types/iso_8601_date_time"
|
|
34
|
-
require "graphql/types/iso_8601_date"
|
|
35
|
-
require_relative "graphql/resource_interface"
|
|
36
|
-
require_relative "graphql/schema_field"
|
|
37
|
-
require_relative "graphql/auth_context"
|
|
38
|
-
require_relative "graphql/record_source"
|
|
39
|
-
require_relative "graphql/resource_query_proxy"
|
|
40
|
-
require_relative "graphql/run_action_payload"
|
|
41
|
-
require_relative "graphql/run_action_mutation_config"
|
|
42
|
-
require_relative "graphql/run_action_mutation_dsl"
|
|
43
|
-
require_relative "graphql/key_value_pair_input"
|
|
44
|
-
require_relative "graphql/schema_builder"
|
|
31
|
+
require_graphql_dependencies!
|
|
32
|
+
require_active_admin_graphql_components!
|
|
45
33
|
end
|
|
46
34
|
|
|
47
35
|
# @param namespace [ActiveAdmin::Namespace]
|
|
48
36
|
# @return [Class] schema class (subclass of GraphQL::Schema)
|
|
37
|
+
#
|
|
38
|
+
# Cached per namespace for the process. In development, call
|
|
39
|
+
# {clear_schema_cache!} or {clear_schema_for!} after ActiveAdmin +unload!+
|
|
40
|
+
# (already wired in {Integration::ApplicationUnloadClearsGraphQLSchema}) or
|
|
41
|
+
# when admin registrations change without a full unload.
|
|
49
42
|
def schema_for(namespace)
|
|
50
43
|
cache_key = namespace.name
|
|
51
|
-
SCHEMA_CACHE.delete(cache_key) if defined?(Rails) && Rails.env.development?
|
|
52
44
|
SCHEMA_CACHE[cache_key] ||= SchemaBuilder.new(namespace).build
|
|
53
45
|
end
|
|
54
46
|
|
|
@@ -59,6 +51,28 @@ module ActiveAdmin
|
|
|
59
51
|
def clear_schema_for!(namespace)
|
|
60
52
|
SCHEMA_CACHE.delete(namespace.name)
|
|
61
53
|
end
|
|
54
|
+
|
|
55
|
+
def require_graphql_dependencies!
|
|
56
|
+
require "graphql"
|
|
57
|
+
require "graphql/types/json"
|
|
58
|
+
require "graphql/types/iso_8601_date_time"
|
|
59
|
+
require "graphql/types/iso_8601_date"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def require_active_admin_graphql_components!
|
|
63
|
+
%w[
|
|
64
|
+
graphql/resource_interface
|
|
65
|
+
graphql/schema_field
|
|
66
|
+
graphql/auth_context
|
|
67
|
+
graphql/record_source
|
|
68
|
+
graphql/resource_query_proxy
|
|
69
|
+
graphql/run_action_payload
|
|
70
|
+
graphql/run_action_mutation_config
|
|
71
|
+
graphql/run_action_mutation_dsl
|
|
72
|
+
graphql/key_value_pair_input
|
|
73
|
+
graphql/schema_builder
|
|
74
|
+
].each { |path| require_relative path }
|
|
75
|
+
end
|
|
62
76
|
end
|
|
63
77
|
end
|
|
64
78
|
end
|
|
@@ -23,28 +23,37 @@ unless defined?(ActiveAdmin::PrimaryKey)
|
|
|
23
23
|
|
|
24
24
|
def composite_attribute_hash(model, id_param)
|
|
25
25
|
cols = ordered_columns(model)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
26
|
+
hash = normalize_composite_input(cols, id_param)
|
|
27
|
+
validate_composite_keys!(cols, hash)
|
|
28
|
+
hash
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def normalize_composite_input(cols, id_param)
|
|
32
|
+
case id_param
|
|
33
|
+
when Hash
|
|
34
|
+
id_param.stringify_keys.slice(*cols)
|
|
35
|
+
when String
|
|
36
|
+
extract_composite_json(cols, id_param)
|
|
37
|
+
when Array
|
|
38
|
+
cols.zip(id_param).to_h
|
|
39
|
+
else
|
|
40
|
+
raise ArgumentError, "unsupported composite id type"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def extract_composite_json(cols, payload)
|
|
45
|
+
parsed = JSON.parse(payload)
|
|
46
|
+
raise ArgumentError, "composite id must be a JSON object" unless parsed.is_a?(Hash)
|
|
47
|
+
|
|
48
|
+
parsed.stringify_keys.slice(*cols)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_composite_keys!(cols, hash)
|
|
52
|
+
missing = cols - hash.keys
|
|
42
53
|
raise ArgumentError, "composite id missing keys: #{missing.join(", ")}" if missing.any?
|
|
43
54
|
|
|
44
|
-
absent = cols.select { |c|
|
|
55
|
+
absent = cols.select { |c| hash[c].nil? }
|
|
45
56
|
raise ArgumentError, "composite id missing values for: #{absent.join(", ")}" if absent.any?
|
|
46
|
-
|
|
47
|
-
h
|
|
48
57
|
end
|
|
49
58
|
|
|
50
59
|
def find_attributes(model, id_param)
|
|
@@ -82,36 +91,49 @@ unless defined?(ActiveAdmin::PrimaryKey)
|
|
|
82
91
|
blob = blob.stringify_keys
|
|
83
92
|
cols = ordered_columns(model)
|
|
84
93
|
if cols.size == 1
|
|
85
|
-
|
|
86
|
-
raise ArgumentError, "id is required" if id.blank?
|
|
87
|
-
|
|
88
|
-
{"id" => id.to_s}
|
|
89
|
-
elsif blob["id"].present?
|
|
90
|
-
find_attributes(model, blob["id"]).stringify_keys
|
|
91
|
-
elsif cols.all? { |c| blob.key?(c) && blob[c].present? }
|
|
92
|
-
cols.to_h { |c| [c, blob[c]] }
|
|
94
|
+
single_member_param(blob)
|
|
93
95
|
else
|
|
94
|
-
|
|
96
|
+
composite_member_param(model, blob, cols)
|
|
95
97
|
end
|
|
96
98
|
end
|
|
97
99
|
|
|
100
|
+
def single_member_param(blob)
|
|
101
|
+
id = blob["id"]
|
|
102
|
+
raise ArgumentError, "id is required" if id.blank?
|
|
103
|
+
|
|
104
|
+
{"id" => id.to_s}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def composite_member_param(model, blob, cols)
|
|
108
|
+
return find_attributes(model, blob["id"]).stringify_keys if blob["id"].present?
|
|
109
|
+
return cols.to_h { |c| [c, blob[c]] } if cols.all? { |c| blob.key?(c) && blob[c].present? }
|
|
110
|
+
|
|
111
|
+
raise ArgumentError, "composite id requires id (JSON) or all primary key columns"
|
|
112
|
+
end
|
|
113
|
+
|
|
98
114
|
def field_kw_to_param_hash(model, id:, **kw)
|
|
99
115
|
cols = ordered_columns(model)
|
|
100
116
|
if cols.size == 1
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
{"id" => id.to_s}
|
|
104
|
-
elsif id.present?
|
|
105
|
-
find_attributes(model, id).stringify_keys
|
|
117
|
+
single_field_hash(id)
|
|
106
118
|
else
|
|
107
|
-
|
|
108
|
-
if cols.all? { |c| !out[c].nil? }
|
|
109
|
-
out.transform_values(&:to_s)
|
|
110
|
-
else
|
|
111
|
-
raise ArgumentError, "composite id requires id (JSON) or all primary key fields as arguments"
|
|
112
|
-
end
|
|
119
|
+
composite_field_hash(model, cols, id, kw)
|
|
113
120
|
end
|
|
114
121
|
end
|
|
122
|
+
|
|
123
|
+
def single_field_hash(id)
|
|
124
|
+
raise ArgumentError, "id is required" if id.blank?
|
|
125
|
+
|
|
126
|
+
{"id" => id.to_s}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def composite_field_hash(model, cols, id, kw)
|
|
130
|
+
return find_attributes(model, id).stringify_keys if id.present?
|
|
131
|
+
|
|
132
|
+
out = cols.to_h { |c| [c, kw[c.to_sym] || kw[c]] }
|
|
133
|
+
return out.transform_values(&:to_s) if cols.all? { |c| !out[c].nil? }
|
|
134
|
+
|
|
135
|
+
raise ArgumentError, "composite id requires id (JSON) or all primary key fields as arguments"
|
|
136
|
+
end
|
|
115
137
|
end
|
|
116
138
|
end
|
|
117
139
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activeadmin-graphql
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -348,6 +348,7 @@ files:
|
|
|
348
348
|
- lib/active_admin/graphql/schema_builder/query_type_collection.rb
|
|
349
349
|
- lib/active_admin/graphql/schema_builder/query_type_member.rb
|
|
350
350
|
- lib/active_admin/graphql/schema_builder/query_type_pages.rb
|
|
351
|
+
- lib/active_admin/graphql/schema_builder/query_type_policies.rb
|
|
351
352
|
- lib/active_admin/graphql/schema_builder/query_type_registered.rb
|
|
352
353
|
- lib/active_admin/graphql/schema_builder/resolvers.rb
|
|
353
354
|
- lib/active_admin/graphql/schema_builder/resources.rb
|
|
@@ -383,7 +384,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
383
384
|
- !ruby/object:Gem::Version
|
|
384
385
|
version: '0'
|
|
385
386
|
requirements: []
|
|
386
|
-
rubygems_version: 4.0.
|
|
387
|
+
rubygems_version: 4.0.4
|
|
387
388
|
specification_version: 4
|
|
388
389
|
summary: GraphQL API extension for ActiveAdmin (graphql-ruby).
|
|
389
390
|
test_files: []
|