activeadmin-graphql 0.1.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 +7 -0
- data/CHANGELOG.md +6 -0
- data/CODE_OF_CONDUCT.md +31 -0
- data/CONTRIBUTING.md +27 -0
- data/LICENSE.md +21 -0
- data/README.md +49 -0
- data/activeadmin-graphql.gemspec +66 -0
- data/app/controllers/active_admin/graphql_controller.rb +168 -0
- data/docs/graphql-api.md +486 -0
- data/lib/active_admin/graphql/auth_context.rb +35 -0
- data/lib/active_admin/graphql/engine.rb +9 -0
- data/lib/active_admin/graphql/integration.rb +135 -0
- data/lib/active_admin/graphql/key_value_pair_input.rb +48 -0
- data/lib/active_admin/graphql/railtie.rb +10 -0
- data/lib/active_admin/graphql/record_source.rb +30 -0
- data/lib/active_admin/graphql/resource_config.rb +68 -0
- data/lib/active_admin/graphql/resource_definition_dsl.rb +117 -0
- data/lib/active_admin/graphql/resource_interface.rb +25 -0
- data/lib/active_admin/graphql/resource_query_proxy/controller.rb +149 -0
- data/lib/active_admin/graphql/resource_query_proxy.rb +112 -0
- data/lib/active_admin/graphql/run_action_mutation_config.rb +23 -0
- data/lib/active_admin/graphql/run_action_mutation_dsl.rb +32 -0
- data/lib/active_admin/graphql/run_action_payload.rb +27 -0
- data/lib/active_admin/graphql/schema_builder/build.rb +84 -0
- data/lib/active_admin/graphql/schema_builder/graph_params.rb +75 -0
- data/lib/active_admin/graphql/schema_builder/mutation_action_types.rb +52 -0
- data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +61 -0
- data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +118 -0
- data/lib/active_admin/graphql/schema_builder/mutation_create.rb +65 -0
- data/lib/active_admin/graphql/schema_builder/mutation_member.rb +122 -0
- data/lib/active_admin/graphql/schema_builder/mutation_type_builder.rb +52 -0
- data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +120 -0
- data/lib/active_admin/graphql/schema_builder/query_type.rb +53 -0
- data/lib/active_admin/graphql/schema_builder/query_type_collection.rb +84 -0
- data/lib/active_admin/graphql/schema_builder/query_type_member.rb +91 -0
- data/lib/active_admin/graphql/schema_builder/query_type_pages.rb +44 -0
- data/lib/active_admin/graphql/schema_builder/query_type_registered.rb +57 -0
- data/lib/active_admin/graphql/schema_builder/resolvers.rb +116 -0
- data/lib/active_admin/graphql/schema_builder/resources.rb +48 -0
- data/lib/active_admin/graphql/schema_builder/types_inputs.rb +119 -0
- data/lib/active_admin/graphql/schema_builder/types_object.rb +96 -0
- data/lib/active_admin/graphql/schema_builder/visibility.rb +58 -0
- data/lib/active_admin/graphql/schema_builder/wire.rb +36 -0
- data/lib/active_admin/graphql/schema_builder.rb +62 -0
- data/lib/active_admin/graphql/schema_field.rb +29 -0
- data/lib/active_admin/graphql/version.rb +7 -0
- data/lib/active_admin/graphql.rb +68 -0
- data/lib/active_admin/primary_key.rb +117 -0
- data/lib/activeadmin/graphql.rb +5 -0
- metadata +389 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Shared input for flat string key/value pairs (Rails param-style). Used instead of a JSON map
|
|
6
|
+
# for nested route segments and custom action arguments so the schema stays explicit.
|
|
7
|
+
class KeyValuePairInput < ::GraphQL::Schema::InputObject
|
|
8
|
+
graphql_name "ActiveAdminKeyValuePair"
|
|
9
|
+
description "Param entry: string key and value (same shape as flat Rails query/form fields)."
|
|
10
|
+
|
|
11
|
+
argument :key, ::GraphQL::Types::String, required: true, camelize: false
|
|
12
|
+
argument :value, ::GraphQL::Types::String, required: true, camelize: false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module KeyValuePairs
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# @param entries [nil, Array<#key, #value>, Array<Hash>]
|
|
19
|
+
# @return [Hash<String, String>]
|
|
20
|
+
def to_hash(entries)
|
|
21
|
+
return {} if entries.nil?
|
|
22
|
+
|
|
23
|
+
unless entries.is_a?(Array)
|
|
24
|
+
raise ArgumentError, "expected an array of key/value pairs, got #{entries.class}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
entries.each_with_object({}) do |e, h|
|
|
28
|
+
key, val = extract_pair(e)
|
|
29
|
+
h[key.to_s] = val.to_s
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def extract_pair(e)
|
|
34
|
+
if e.respond_to?(:key) && e.respond_to?(:value) && !e.is_a?(Hash)
|
|
35
|
+
[e.key, e.value]
|
|
36
|
+
elsif e.is_a?(Hash)
|
|
37
|
+
k = e["key"] || e[:key]
|
|
38
|
+
v = e["value"] || e[:value]
|
|
39
|
+
raise ::GraphQL::ExecutionError, "key/value pair missing key" if k.nil?
|
|
40
|
+
|
|
41
|
+
[k, v]
|
|
42
|
+
else
|
|
43
|
+
raise ::GraphQL::ExecutionError, "invalid key/value pair entry: #{e.class}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Batches +find+ by primary key for belongs_to-style fields (graphql-ruby dataloader).
|
|
6
|
+
class RecordSource < ::GraphQL::Dataloader::Source
|
|
7
|
+
def initialize(model_class)
|
|
8
|
+
@model_class = model_class
|
|
9
|
+
@pk = model_class.primary_key
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def fetch(ids)
|
|
13
|
+
keys = ids.map { |raw| raw.nil? ? nil : ActiveAdmin::PrimaryKey.dataloader_tuple(@model_class, raw) }
|
|
14
|
+
uniq = keys.compact.uniq
|
|
15
|
+
return ids.map { nil } if uniq.empty?
|
|
16
|
+
|
|
17
|
+
if ActiveAdmin::PrimaryKey.composite?(@model_class)
|
|
18
|
+
records = @model_class.where(@pk => uniq).to_a
|
|
19
|
+
by_tuple = records.index_by { |r| ActiveAdmin::PrimaryKey.dataloader_tuple_from_record(r) }
|
|
20
|
+
keys.map { |t| t.nil? ? nil : by_tuple[t] }
|
|
21
|
+
else
|
|
22
|
+
pk_sym = @pk.to_sym
|
|
23
|
+
records = @model_class.where(pk_sym => uniq).to_a
|
|
24
|
+
by_id = records.index_by { |r| r.public_send(pk_sym) }
|
|
25
|
+
keys.map { |k| k.nil? ? nil : by_id[k] }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "run_action_mutation_config"
|
|
4
|
+
|
|
5
|
+
module ActiveAdmin
|
|
6
|
+
module GraphQL
|
|
7
|
+
# Per-resource GraphQL options set via +graphql+ in +ActiveAdmin.register+.
|
|
8
|
+
class ResourceConfig
|
|
9
|
+
attr_accessor :enabled
|
|
10
|
+
attr_accessor :graphql_type_name
|
|
11
|
+
attr_accessor :only_attributes
|
|
12
|
+
attr_accessor :exclude_attributes
|
|
13
|
+
attr_accessor :extension_block
|
|
14
|
+
|
|
15
|
+
# Optional resolver overrides (set from +graphql do+). SchemaBuilder still owns field names,
|
|
16
|
+
# arguments, and types; procs replace only the Ruby resolution body.
|
|
17
|
+
attr_accessor :resolve_index_proc
|
|
18
|
+
attr_accessor :resolve_show_proc
|
|
19
|
+
attr_accessor :resolve_create_proc
|
|
20
|
+
attr_accessor :resolve_update_proc
|
|
21
|
+
attr_accessor :resolve_destroy_proc
|
|
22
|
+
|
|
23
|
+
# Default return type for run-action mutations (+batch+, +member+, +collection+) when a kind-specific
|
|
24
|
+
# {RunActionMutationConfig#payload_type} is not set. Falls back to {RunActionPayload}.
|
|
25
|
+
attr_accessor :run_action_payload_type
|
|
26
|
+
|
|
27
|
+
def initialize
|
|
28
|
+
@enabled = true
|
|
29
|
+
@exclude_attributes = []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def disabled?
|
|
33
|
+
!enabled
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def batch_run_action
|
|
37
|
+
@batch_run_action ||= RunActionMutationConfig.new
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def member_run_action
|
|
41
|
+
@member_run_action ||= RunActionMutationConfig.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def collection_run_action
|
|
45
|
+
@collection_run_action ||= RunActionMutationConfig.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Per +member_action+ name (string) -> {RunActionMutationConfig} for typed fields like +posts_member_publish+.
|
|
49
|
+
def member_action_mutations
|
|
50
|
+
@member_action_mutations ||= {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def member_action_mutation_for(name)
|
|
54
|
+
key = name.to_s
|
|
55
|
+
member_action_mutations[key] ||= RunActionMutationConfig.new
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def collection_action_mutations
|
|
59
|
+
@collection_action_mutations ||= {}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def collection_action_mutation_for(name)
|
|
63
|
+
key = name.to_s
|
|
64
|
+
collection_action_mutations[key] ||= RunActionMutationConfig.new
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "run_action_mutation_config"
|
|
4
|
+
require_relative "run_action_mutation_dsl"
|
|
5
|
+
|
|
6
|
+
module ActiveAdmin
|
|
7
|
+
module GraphQL
|
|
8
|
+
# DSL for +graphql do ... end+ inside +ActiveAdmin.register+.
|
|
9
|
+
class ResourceDefinitionDSL
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def disable!
|
|
15
|
+
@config.enabled = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def type_name(name)
|
|
19
|
+
@config.graphql_type_name = name.to_s
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def only(*attrs)
|
|
23
|
+
@config.only_attributes = attrs.flatten.map(&:to_sym)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def except(*attrs)
|
|
27
|
+
@config.exclude_attributes.concat(attrs.flatten.map(&:to_sym))
|
|
28
|
+
end
|
|
29
|
+
alias_method :exclude, :except
|
|
30
|
+
|
|
31
|
+
def configure(&block)
|
|
32
|
+
@config.extension_block = block
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Override the list field resolver (+posts+, etc.). Must return an +ActiveRecord::Relation+
|
|
36
|
+
# (or compatible with the connection type). Same auth and +ResourceQueryProxy+ as the default.
|
|
37
|
+
def resolve_index(&block)
|
|
38
|
+
@config.resolve_index_proc = block
|
|
39
|
+
end
|
|
40
|
+
alias_method :resolve_collection, :resolve_index
|
|
41
|
+
|
|
42
|
+
# Override the singular field resolver (+post+, +registered_resource+ for this type).
|
|
43
|
+
# Must return a record instance or +nil+.
|
|
44
|
+
def resolve_show(&block)
|
|
45
|
+
@config.resolve_show_proc = block
|
|
46
|
+
end
|
|
47
|
+
alias_method :resolve_member, :resolve_show
|
|
48
|
+
|
|
49
|
+
def resolve_create(&block)
|
|
50
|
+
@config.resolve_create_proc = block
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def resolve_update(&block)
|
|
54
|
+
@config.resolve_update_proc = block
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def resolve_destroy(&block)
|
|
58
|
+
@config.resolve_destroy_proc = block
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @see #batch_action_mutation
|
|
62
|
+
def resolve_batch_action(&block)
|
|
63
|
+
@config.batch_run_action.resolve_proc = block
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @see #member_action_mutation
|
|
67
|
+
def resolve_member_action(&block)
|
|
68
|
+
@config.member_run_action.resolve_proc = block
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @see #collection_action_mutation
|
|
72
|
+
def resolve_collection_action(&block)
|
|
73
|
+
@config.collection_run_action.resolve_proc = block
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Pair +type+ and +resolve+ for +posts_batch_action+ (graphql-ruby-style block).
|
|
77
|
+
def batch_action_mutation(&block)
|
|
78
|
+
RunActionMutationDSL.new(@config.batch_run_action).instance_exec(&block)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# With no name: configures the aggregate +posts_member_action(action: …)+ field.
|
|
82
|
+
# With a symbol/string: configures +posts_member_<action>+ (one field per +member_action+), so each
|
|
83
|
+
# action can use its own +type+, +resolve+, +arguments+, and GraphQL inputs.
|
|
84
|
+
def member_action_mutation(name = nil, &block)
|
|
85
|
+
cfg = name.nil? ? @config.member_run_action : @config.member_action_mutation_for(name)
|
|
86
|
+
RunActionMutationDSL.new(cfg).instance_exec(&block)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def collection_action_mutation(name = nil, &block)
|
|
90
|
+
cfg = name.nil? ? @config.collection_run_action : @config.collection_action_mutation_for(name)
|
|
91
|
+
RunActionMutationDSL.new(cfg).instance_exec(&block)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Default return type for all run-action fields unless a kind-specific +type+ is set inside
|
|
95
|
+
# +batch_action_mutation+ / +member_action_mutation+ / +collection_action_mutation+.
|
|
96
|
+
def run_action_payload_type(type)
|
|
97
|
+
RunActionMutationConfig.ensure_graphql_object_subclass!(type)
|
|
98
|
+
@config.run_action_payload_type = type
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def batch_action_run_action_payload_type(type)
|
|
102
|
+
RunActionMutationConfig.ensure_graphql_object_subclass!(type)
|
|
103
|
+
@config.batch_run_action.payload_type = type
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def member_action_run_action_payload_type(type)
|
|
107
|
+
RunActionMutationConfig.ensure_graphql_object_subclass!(type)
|
|
108
|
+
@config.member_run_action.payload_type = type
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def collection_action_run_action_payload_type(type)
|
|
112
|
+
RunActionMutationConfig.ensure_graphql_object_subclass!(type)
|
|
113
|
+
@config.collection_run_action.payload_type = type
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Implemented by every namespace resource +GraphQL::Schema::Object+ type so clients can
|
|
6
|
+
# fragment on a shared +id+ field or use abstract selections consistently.
|
|
7
|
+
module ResourceInterface
|
|
8
|
+
include ::GraphQL::Schema::Interface
|
|
9
|
+
|
|
10
|
+
graphql_name "ActiveAdminResource"
|
|
11
|
+
description "Shared shape for Active Admin resource records exposed in this namespace."
|
|
12
|
+
|
|
13
|
+
field :id, ::GraphQL::Types::ID, null: false
|
|
14
|
+
|
|
15
|
+
definition_methods do
|
|
16
|
+
def visible?(context)
|
|
17
|
+
hook = context[:namespace]&.graphql_schema_visible
|
|
18
|
+
return super if hook.nil?
|
|
19
|
+
|
|
20
|
+
super && !!hook.call(context, {kind: :resource_interface})
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
class ResourceQueryProxy
|
|
6
|
+
module Controller
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def normalize_graph_params(graph_params)
|
|
10
|
+
h = graph_params.respond_to?(:to_unsafe_h) ? graph_params.to_unsafe_h : graph_params.to_h
|
|
11
|
+
h.stringify_keys
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def param_hash_for(action, extra)
|
|
15
|
+
{
|
|
16
|
+
"action" => action,
|
|
17
|
+
"controller" => "active_admin/graphql"
|
|
18
|
+
}.merge(extra.stringify_keys).tap do |h|
|
|
19
|
+
merge_ransack!(h)
|
|
20
|
+
h["scope"] = @graph_params["scope"].to_s if @graph_params["scope"].present?
|
|
21
|
+
h["order"] = @graph_params["order"].to_s if @graph_params["order"].present?
|
|
22
|
+
merge_belongs_to!(h)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def merge_ransack!(h)
|
|
27
|
+
q = @graph_params["q"]
|
|
28
|
+
return if q.blank? && !@graph_params.key?("q")
|
|
29
|
+
|
|
30
|
+
h["q"] = normalize_q(q)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def normalize_q(q)
|
|
34
|
+
case q
|
|
35
|
+
when Hash
|
|
36
|
+
q.deep_stringify_keys
|
|
37
|
+
when ActionController::Parameters
|
|
38
|
+
q.to_unsafe_h.deep_stringify_keys
|
|
39
|
+
else
|
|
40
|
+
{}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def merge_belongs_to!(h)
|
|
45
|
+
btc = @aa_resource.belongs_to_config
|
|
46
|
+
return unless btc
|
|
47
|
+
|
|
48
|
+
key = btc.to_param.to_s
|
|
49
|
+
val = @graph_params[key]
|
|
50
|
+
h[key] = val.to_s if val.present?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def stub_controller!(controller, hash)
|
|
54
|
+
user = @user
|
|
55
|
+
params_obj = ActionController::Parameters.new(hash)
|
|
56
|
+
params_obj.permit!
|
|
57
|
+
|
|
58
|
+
controller.define_singleton_method(:params) { params_obj }
|
|
59
|
+
controller.define_singleton_method(:current_active_admin_user) { user }
|
|
60
|
+
controller.define_singleton_method(:current_user) { user }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def normalize_batch_inputs(inputs)
|
|
64
|
+
return {} if inputs.nil?
|
|
65
|
+
|
|
66
|
+
case inputs
|
|
67
|
+
when Array
|
|
68
|
+
KeyValuePairs.to_hash(inputs)
|
|
69
|
+
when String
|
|
70
|
+
JSON.parse(inputs)
|
|
71
|
+
when Hash
|
|
72
|
+
inputs
|
|
73
|
+
when ActionController::Parameters
|
|
74
|
+
inputs.to_unsafe_h
|
|
75
|
+
else
|
|
76
|
+
raise ::GraphQL::ExecutionError, "batch inputs must be a list of key/value pairs or a JSON object"
|
|
77
|
+
end.then do |h|
|
|
78
|
+
raise ::GraphQL::ExecutionError, "batch inputs must resolve to a hash" unless h.is_a?(Hash)
|
|
79
|
+
|
|
80
|
+
h.deep_stringify_keys
|
|
81
|
+
end
|
|
82
|
+
rescue JSON::ParserError => e
|
|
83
|
+
raise ::GraphQL::ExecutionError, "invalid batch inputs JSON (#{e.message})"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def normalize_extra_params(extra_params)
|
|
87
|
+
return {} if extra_params.nil?
|
|
88
|
+
|
|
89
|
+
if extra_params.is_a?(Array)
|
|
90
|
+
return KeyValuePairs.to_hash(extra_params)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
h = extra_params.respond_to?(:to_unsafe_h) ? extra_params.to_unsafe_h : extra_params.to_h
|
|
94
|
+
h.stringify_keys
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def attach_graphql_request!(controller)
|
|
98
|
+
return if controller.instance_variable_defined?(:@_request) && controller.request
|
|
99
|
+
|
|
100
|
+
env = Rack::MockRequest.env_for("http://test.host/", method: "POST", params: {_graphql: "1"})
|
|
101
|
+
req = ActionDispatch::Request.new(env)
|
|
102
|
+
res = ActionDispatch::Response.new
|
|
103
|
+
controller.send(:set_request!, req)
|
|
104
|
+
controller.send(:set_response!, res)
|
|
105
|
+
controller.define_singleton_method(:default_url_options) { {host: "test.host"} }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def perform_controller_command!(controller)
|
|
109
|
+
attach_graphql_request!(controller)
|
|
110
|
+
yield
|
|
111
|
+
build_run_payload(controller)
|
|
112
|
+
rescue ActiveAdmin::AccessDenied => e
|
|
113
|
+
raise ::GraphQL::ExecutionError, e.message
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def build_run_payload(controller)
|
|
117
|
+
res = controller.response
|
|
118
|
+
RunActionPayload::Result.new(
|
|
119
|
+
ok: true,
|
|
120
|
+
status: res&.status,
|
|
121
|
+
location: extract_location(res),
|
|
122
|
+
body: extract_response_body(res)
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def extract_location(res)
|
|
127
|
+
return nil unless res
|
|
128
|
+
|
|
129
|
+
loc = res.location if res.respond_to?(:location)
|
|
130
|
+
loc.presence || res.headers&.[]("Location")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def extract_response_body(res)
|
|
134
|
+
return nil unless res
|
|
135
|
+
|
|
136
|
+
b = res.body
|
|
137
|
+
return nil if b.nil? || (b.respond_to?(:empty?) && b.empty?)
|
|
138
|
+
|
|
139
|
+
s = b.is_a?(String) ? b : b.to_s
|
|
140
|
+
s = +s
|
|
141
|
+
s.force_encoding("UTF-8")
|
|
142
|
+
s.valid_encoding? ? s : nil
|
|
143
|
+
rescue
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack/mock"
|
|
4
|
+
|
|
5
|
+
require_relative "resource_query_proxy/controller"
|
|
6
|
+
|
|
7
|
+
module ActiveAdmin
|
|
8
|
+
module GraphQL
|
|
9
|
+
# Reuses {ResourceController} data-access behaviour (+scoped_collection+, +find_collection+,
|
|
10
|
+
# +find_resource+, authorization scoping, Ransack, menu scopes, +sorting+, +includes+)
|
|
11
|
+
# so GraphQL list/detail mutations align with the HTML/JSON list and member REST endpoints.
|
|
12
|
+
class ResourceQueryProxy
|
|
13
|
+
include Controller
|
|
14
|
+
|
|
15
|
+
def initialize(aa_resource:, user:, namespace:, graph_params: {})
|
|
16
|
+
@aa_resource = aa_resource
|
|
17
|
+
@user = user
|
|
18
|
+
@namespace = namespace
|
|
19
|
+
@graph_params = normalize_graph_params(graph_params)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def relation_for_index
|
|
23
|
+
controller_for("index").send(:find_collection, except: %i[pagination collection_decorator])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def find_member(id)
|
|
27
|
+
extra = member_route_params_for_find(id)
|
|
28
|
+
controller_for("show", extra).send(:find_resource)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build_new(attributes)
|
|
32
|
+
if missing_required_belongs_to?
|
|
33
|
+
raise ::GraphQL::ExecutionError,
|
|
34
|
+
"#{@aa_resource.belongs_to_config.to_param} is required for nested resource #{@aa_resource.resource_name}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
c = controller_for("new")
|
|
38
|
+
chain = c.send(:apply_authorization_scope, c.send(:scoped_collection))
|
|
39
|
+
permitted = attributes.stringify_keys.slice(*@aa_resource.graphql_assignable_attribute_names)
|
|
40
|
+
chain.build(permitted)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def run_batch_action(batch_sym, ids, inputs: {})
|
|
44
|
+
unless @aa_resource.batch_actions_enabled?
|
|
45
|
+
raise ::GraphQL::ExecutionError, "batch actions are disabled for #{@aa_resource.resource_name}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
inputs = normalize_batch_inputs(inputs)
|
|
49
|
+
extra = {
|
|
50
|
+
"batch_action" => batch_sym.to_s,
|
|
51
|
+
"collection_selection" => Array(ids).map(&:to_s),
|
|
52
|
+
"batch_action_inputs" => inputs.to_json
|
|
53
|
+
}
|
|
54
|
+
c = controller_for("batch_action", extra)
|
|
55
|
+
perform_controller_command!(c) { c.send(:batch_action) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def run_member_action(action_name, id, extra_params: {})
|
|
59
|
+
action_name = action_name.to_s
|
|
60
|
+
unless @aa_resource.member_actions.any? { |a| a.name.to_s == action_name }
|
|
61
|
+
raise ::GraphQL::ExecutionError, "unknown member action #{action_name.inspect}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
extras = normalize_extra_params(extra_params)
|
|
65
|
+
c = controller_for(action_name, {"id" => id.to_s}.merge(extras))
|
|
66
|
+
perform_controller_command!(c) { c.send(action_name) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def run_collection_action(action_name, extra_params: {})
|
|
70
|
+
action_name = action_name.to_s
|
|
71
|
+
unless @aa_resource.collection_actions.any? { |a| a.name.to_s == action_name }
|
|
72
|
+
raise ::GraphQL::ExecutionError, "unknown collection action #{action_name.inspect}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
extras = normalize_extra_params(extra_params)
|
|
76
|
+
c = controller_for(action_name, extras)
|
|
77
|
+
perform_controller_command!(c) { c.send(action_name) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def member_route_params_for_find(id)
|
|
83
|
+
model = @aa_resource.resource_class
|
|
84
|
+
if ActiveAdmin::PrimaryKey.composite?(model)
|
|
85
|
+
attrs = ActiveAdmin::PrimaryKey.find_attributes(model, id)
|
|
86
|
+
tuple = ActiveAdmin::PrimaryKey.ordered_columns(model).map { |c| attrs[c] }
|
|
87
|
+
{"id" => tuple}
|
|
88
|
+
elsif id.is_a?(Hash)
|
|
89
|
+
id.stringify_keys
|
|
90
|
+
else
|
|
91
|
+
{"id" => id.to_s}
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def missing_required_belongs_to?
|
|
96
|
+
btc = @aa_resource.belongs_to_config
|
|
97
|
+
return false unless btc&.required?
|
|
98
|
+
|
|
99
|
+
key = btc.to_param.to_s
|
|
100
|
+
val = @graph_params[key] || @graph_params[key.to_sym]
|
|
101
|
+
val.blank?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def controller_for(action, extra = {})
|
|
105
|
+
c = @aa_resource.controller.new
|
|
106
|
+
h = param_hash_for(action, extra)
|
|
107
|
+
stub_controller!(c, h)
|
|
108
|
+
c
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Pairs return type and optional resolver for one run-action mutation kind (+batch+, +member+,
|
|
6
|
+
# or +collection+). Default return type resolution falls back to {ResourceConfig#run_action_payload_type},
|
|
7
|
+
# then {RunActionPayload}.
|
|
8
|
+
class RunActionMutationConfig
|
|
9
|
+
# @return [Class, nil] +GraphQL::Schema::Object+ subclass
|
|
10
|
+
attr_accessor :payload_type
|
|
11
|
+
# @return [Proc, nil]
|
|
12
|
+
attr_accessor :resolve_proc
|
|
13
|
+
# Optional block evaluated in the graphql-ruby +field+ DSL context (+argument+, …) for per-action fields.
|
|
14
|
+
attr_accessor :arguments_proc
|
|
15
|
+
|
|
16
|
+
def self.ensure_graphql_object_subclass!(type)
|
|
17
|
+
unless type.is_a?(Class) && type < ::GraphQL::Schema::Object
|
|
18
|
+
raise ArgumentError, "Expected a GraphQL::Schema::Object subclass, got #{type.inspect}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "run_action_mutation_config"
|
|
4
|
+
|
|
5
|
+
module ActiveAdmin
|
|
6
|
+
module GraphQL
|
|
7
|
+
# Nested DSL for a single run-action mutation kind (+batch_action_mutation+, etc.): +type+ and +resolve+,
|
|
8
|
+
# similar to graphql-ruby pairing field type with resolution.
|
|
9
|
+
class RunActionMutationDSL
|
|
10
|
+
def initialize(mutation_config)
|
|
11
|
+
@mutation_config = mutation_config
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# GraphQL object type for this mutation field (alias: +payload_type+).
|
|
15
|
+
def type(gql_object_class)
|
|
16
|
+
RunActionMutationConfig.ensure_graphql_object_subclass!(gql_object_class)
|
|
17
|
+
@mutation_config.payload_type = gql_object_class
|
|
18
|
+
end
|
|
19
|
+
alias_method :payload_type, :type
|
|
20
|
+
|
|
21
|
+
def resolve(&block)
|
|
22
|
+
@mutation_config.resolve_proc = block
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Per-action fields only: extra GraphQL arguments (+argument :reason, String, …+).
|
|
26
|
+
# Not used on the aggregate +posts_member_action(action: …)+ field.
|
|
27
|
+
def arguments(&block)
|
|
28
|
+
@mutation_config.arguments_proc = block
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Return type for mutations that run batch, member, or collection controller actions.
|
|
6
|
+
class RunActionPayload < ::GraphQL::Schema::Object
|
|
7
|
+
graphql_name "ActiveAdminRunActionPayload"
|
|
8
|
+
|
|
9
|
+
def self.visible?(context)
|
|
10
|
+
hook = context[:namespace]&.graphql_schema_visible
|
|
11
|
+
return super if hook.nil?
|
|
12
|
+
|
|
13
|
+
super && !!hook.call(context, {kind: :run_action_payload})
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
field :ok, ::GraphQL::Types::Boolean, null: false
|
|
17
|
+
field :status, ::GraphQL::Types::Int, null: true,
|
|
18
|
+
description: "HTTP-style status from the controller response, when set."
|
|
19
|
+
field :location, ::GraphQL::Types::String, null: true,
|
|
20
|
+
description: "Redirect target, if the action called +redirect_to+."
|
|
21
|
+
field :body, ::GraphQL::Types::String, null: true,
|
|
22
|
+
description: "Response body text when the action rendered (e.g. JSON)."
|
|
23
|
+
|
|
24
|
+
Result = Struct.new(:ok, :status, :location, :body, keyword_init: true)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|