activeadmin-graphql 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/activeadmin-graphql.gemspec +5 -2
- data/app/controllers/active_admin/graphql_controller.rb +83 -31
- 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/policy_set_cache.rb +64 -0
- 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 +32 -4
- 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/run_action_payload.rb +2 -1
- 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 +160 -0
- data/lib/active_admin/graphql/schema_builder/types_object.rb +27 -2
- data/lib/active_admin/graphql/schema_builder/wire.rb +11 -1
- 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 +37 -16
- data/lib/active_admin/primary_key.rb +61 -39
- data/sig/active_admin/graphql.rbs +7 -0
- metadata +47 -16
|
@@ -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,160 @@
|
|
|
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
|
+
resources = builder.send(:active_resources).map do |aa_res|
|
|
52
|
+
{
|
|
53
|
+
type_name: builder.send(:graphql_type_name_for, aa_res),
|
|
54
|
+
activeadmin_policies: ActiveAdmin::GraphQL::PolicySetCache.fetch(
|
|
55
|
+
context,
|
|
56
|
+
subject_owner: aa_res,
|
|
57
|
+
subject: aa_res.resource_class
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
pages = ns.resources.select { |r| r.is_a?(ActiveAdmin::Page) }.map do |page|
|
|
62
|
+
{
|
|
63
|
+
name: page.name,
|
|
64
|
+
activeadmin_policies: ActiveAdmin::GraphQL::PolicySetCache.fetch(
|
|
65
|
+
context,
|
|
66
|
+
subject_owner: page,
|
|
67
|
+
subject: page
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
{resources: resources, pages: pages}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
define_method(:activeadmin_policies_for) do |type_name:, ids:, path: nil|
|
|
75
|
+
aa_res = aa_by_graphql_type_name[type_name.to_s]
|
|
76
|
+
raise ::GraphQL::ExecutionError, "unknown resource type_name #{type_name.inspect}" unless aa_res
|
|
77
|
+
|
|
78
|
+
proxy = ResourceQueryProxy.new(
|
|
79
|
+
aa_resource: aa_res,
|
|
80
|
+
user: context[:auth].user,
|
|
81
|
+
namespace: ns,
|
|
82
|
+
graph_params: KeyValuePairs.to_hash(path)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
records_by_id = proxy.find_members(ids.map(&:to_s))
|
|
86
|
+
ids.map do |id|
|
|
87
|
+
record = records_by_id[id.to_s]
|
|
88
|
+
raise ::GraphQL::ExecutionError, "not found" unless record
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
id: id.to_s,
|
|
92
|
+
activeadmin_policies: ActiveAdmin::GraphQL::PolicySetCache.fetch(
|
|
93
|
+
context,
|
|
94
|
+
subject_owner: aa_res,
|
|
95
|
+
subject: record
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def policy_action_pairs
|
|
106
|
+
hook = @namespace.graphql_policy_actions
|
|
107
|
+
if hook.respond_to?(:call)
|
|
108
|
+
raw = hook.call(@namespace)
|
|
109
|
+
return raw.to_h.transform_keys(&:to_sym) if raw.respond_to?(:to_h)
|
|
110
|
+
end
|
|
111
|
+
POLICY_ACTIONS
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def allowed_base_actions(auth:, subject_owner:, subject:, context:)
|
|
115
|
+
action_mapper = @namespace.graphql_policy_action_mapper
|
|
116
|
+
policy_action_pairs.each_with_object([]) do |(name, default_action), out|
|
|
117
|
+
allowed = if action_mapper.respond_to?(:call)
|
|
118
|
+
action_mapper.call(context, subject_owner, subject, name, default_action)
|
|
119
|
+
else
|
|
120
|
+
auth.authorized?(subject_owner, default_action, subject)
|
|
121
|
+
end
|
|
122
|
+
out << name.to_s if allowed
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def build_policy_set(auth:, subject_owner:, subject:, context:)
|
|
127
|
+
base = {
|
|
128
|
+
allowed_actions: allowed_base_actions(auth: auth, subject_owner: subject_owner, subject: subject, context: context),
|
|
129
|
+
allowed_member_actions: [],
|
|
130
|
+
allowed_collection_actions: [],
|
|
131
|
+
allowed_batch_actions: []
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if subject_owner.is_a?(ActiveAdmin::Resource)
|
|
135
|
+
class_subject = subject_owner.resource_class
|
|
136
|
+
if subject.is_a?(Class)
|
|
137
|
+
if auth.authorized?(subject_owner, ActiveAdmin::Authorization::READ, class_subject)
|
|
138
|
+
base[:allowed_member_actions] = subject_owner.member_actions.map { |a| a.name.to_s }
|
|
139
|
+
base[:allowed_collection_actions] = subject_owner.collection_actions.map { |a| a.name.to_s }
|
|
140
|
+
base[:allowed_batch_actions] = subject_owner.batch_actions.map { |a| a.sym.to_s }
|
|
141
|
+
end
|
|
142
|
+
elsif auth.authorized?(subject_owner, ActiveAdmin::Authorization::READ, subject)
|
|
143
|
+
base[:allowed_member_actions] = subject_owner.member_actions.map { |a| a.name.to_s }
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
if (extra = @namespace.graphql_policy_extra).respond_to?(:call)
|
|
148
|
+
out = extra.call(context, subject_owner, subject, base)
|
|
149
|
+
base = out if out.is_a?(Hash)
|
|
150
|
+
end
|
|
151
|
+
if (transform = @namespace.graphql_policy_transform).respond_to?(:call)
|
|
152
|
+
out = transform.call(context, subject_owner, subject, base)
|
|
153
|
+
base = out if out.is_a?(Hash)
|
|
154
|
+
end
|
|
155
|
+
base
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
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,20 @@ 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
|
+
ActiveAdmin::GraphQL::PolicySetCache.fetch(
|
|
105
|
+
context,
|
|
106
|
+
subject_owner: aa_res,
|
|
107
|
+
subject: object
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
86
111
|
if (ext = aa_res.graphql_config.extension_block)
|
|
87
112
|
type_class.class_eval(&ext)
|
|
88
113
|
end
|
|
@@ -14,6 +14,7 @@ module ActiveAdmin
|
|
|
14
14
|
model.reflect_on_all_associations(:belongs_to).each do |ref|
|
|
15
15
|
next if ref.polymorphic?
|
|
16
16
|
target = ref.klass
|
|
17
|
+
target_aa_res = @aa_by_model[target]
|
|
17
18
|
next unless @object_types[target]
|
|
18
19
|
|
|
19
20
|
field_name = ref.name.to_sym
|
|
@@ -25,7 +26,16 @@ module ActiveAdmin
|
|
|
25
26
|
fk_val = object.public_send(fk)
|
|
26
27
|
next nil if fk_val.nil?
|
|
27
28
|
|
|
28
|
-
dataloader.with(ActiveAdmin::GraphQL::RecordSource, target).load(fk_val)
|
|
29
|
+
record = dataloader.with(ActiveAdmin::GraphQL::RecordSource, target).load(fk_val)
|
|
30
|
+
next nil unless record
|
|
31
|
+
|
|
32
|
+
auth = context[:auth]
|
|
33
|
+
if target_aa_res && auth &&
|
|
34
|
+
!auth.authorized?(target_aa_res, ActiveAdmin::Authorization::READ, record)
|
|
35
|
+
next nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
record
|
|
29
39
|
end
|
|
30
40
|
end
|
|
31
41
|
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
|
@@ -14,6 +14,7 @@ module ActiveAdmin
|
|
|
14
14
|
#
|
|
15
15
|
module GraphQL
|
|
16
16
|
SCHEMA_CACHE = {}
|
|
17
|
+
SCHEMA_CACHE_MUTEX = Mutex.new
|
|
17
18
|
end
|
|
18
19
|
end
|
|
19
20
|
|
|
@@ -28,28 +29,25 @@ module ActiveAdmin
|
|
|
28
29
|
|
|
29
30
|
@graphql_features_loaded = true
|
|
30
31
|
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"
|
|
32
|
+
require_graphql_dependencies!
|
|
33
|
+
require_active_admin_graphql_components!
|
|
45
34
|
end
|
|
46
35
|
|
|
47
36
|
# @param namespace [ActiveAdmin::Namespace]
|
|
48
37
|
# @return [Class] schema class (subclass of GraphQL::Schema)
|
|
38
|
+
#
|
|
39
|
+
# Cached per namespace for the process. In development, call
|
|
40
|
+
# {clear_schema_cache!} or {clear_schema_for!} after ActiveAdmin +unload!+
|
|
41
|
+
# (already wired in {Integration::ApplicationUnloadClearsGraphQLSchema}) or
|
|
42
|
+
# when admin registrations change without a full unload.
|
|
49
43
|
def schema_for(namespace)
|
|
50
44
|
cache_key = namespace.name
|
|
51
|
-
SCHEMA_CACHE
|
|
52
|
-
|
|
45
|
+
cached = SCHEMA_CACHE[cache_key]
|
|
46
|
+
return cached if cached
|
|
47
|
+
|
|
48
|
+
SCHEMA_CACHE_MUTEX.synchronize do
|
|
49
|
+
SCHEMA_CACHE[cache_key] ||= SchemaBuilder.new(namespace).build
|
|
50
|
+
end
|
|
53
51
|
end
|
|
54
52
|
|
|
55
53
|
def clear_schema_cache!
|
|
@@ -59,6 +57,29 @@ module ActiveAdmin
|
|
|
59
57
|
def clear_schema_for!(namespace)
|
|
60
58
|
SCHEMA_CACHE.delete(namespace.name)
|
|
61
59
|
end
|
|
60
|
+
|
|
61
|
+
def require_graphql_dependencies!
|
|
62
|
+
require "graphql"
|
|
63
|
+
require "graphql/types/json"
|
|
64
|
+
require "graphql/types/iso_8601_date_time"
|
|
65
|
+
require "graphql/types/iso_8601_date"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def require_active_admin_graphql_components!
|
|
69
|
+
%w[
|
|
70
|
+
graphql/resource_interface
|
|
71
|
+
graphql/schema_field
|
|
72
|
+
graphql/auth_context
|
|
73
|
+
graphql/record_source
|
|
74
|
+
graphql/resource_query_proxy
|
|
75
|
+
graphql/run_action_payload
|
|
76
|
+
graphql/run_action_mutation_config
|
|
77
|
+
graphql/run_action_mutation_dsl
|
|
78
|
+
graphql/key_value_pair_input
|
|
79
|
+
graphql/policy_set_cache
|
|
80
|
+
graphql/schema_builder
|
|
81
|
+
].each { |path| require_relative path }
|
|
82
|
+
end
|
|
62
83
|
end
|
|
63
84
|
end
|
|
64
85
|
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
|