activeadmin-graphql 0.2.1 → 0.3.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 +17 -0
- data/CODE_OF_CONDUCT.md +14 -19
- data/CONTRIBUTING.md +12 -16
- data/README.md +19 -2
- data/activeadmin-graphql.gemspec +7 -6
- data/app/controllers/active_admin/graphql_controller.rb +9 -0
- data/docs/changelogs/20260818111600_rfc-0002-problems.md +25 -0
- data/docs/changelogs/20260818114000_rfc-as-built-coverage.md +27 -0
- data/docs/changelogs/20260818114700_rfc-design-not-version.md +27 -0
- data/docs/changelogs/20260907141000_advisory-lockfile-refresh.md +21 -0
- data/docs/changelogs/20260921103000_mutation-attribute-allowlists.md +24 -0
- data/docs/changelogs/20260921111000_release-0-3-0.md +42 -0
- data/docs/changelogs/20260921112800_drop-standard-rubocop-direct.md +35 -0
- data/docs/changelogs/20260921120000_graphql-floor-and-activeadmin-4.md +41 -0
- data/docs/changelogs/20261010101500_nonconventional-id-fields.md +34 -0
- data/docs/changelogs/20261010103800_release-0-3-1.md +32 -0
- data/docs/dependencies/20260907141000_json-cve-2026-71847.md +68 -0
- data/docs/dependencies/20260907141000_sqlite3-ghsa-mwm8-39rw-8826.md +62 -0
- data/docs/dependencies/20260921114000_rails-json-parse-kwargs.md +61 -0
- data/docs/dependencies/20260921114500_graphql-ghsa-rmxg-5p3r-j6hh.md +69 -0
- data/docs/dependencies/20261007161308_association-list-combobox.md +23 -0
- data/docs/graphql-api.md +49 -11
- data/docs/issues/20260907141000_engineering-and-dependency-audit.md +30 -0
- data/docs/issues/20260917123253_ci-drop-automatic-dependency-audit.md +29 -0
- data/docs/issues/20260921103000_mutation-attribute-allowlists.md +40 -0
- data/docs/issues/20260921104400_convention-alignment-audit.md +60 -0
- data/docs/issues/20260921114500_dependency-audit.md +90 -0
- data/docs/issues/20260921115500_file-length-metrics.md +31 -0
- data/docs/plan/20260817201500_rfc-starter.md +31 -0
- data/lib/active_admin/graphql/integration.rb +22 -23
- data/lib/active_admin/graphql/mutation_execution_error.rb +16 -0
- data/lib/active_admin/graphql/mutation_input_config.rb +11 -0
- data/lib/active_admin/graphql/mutation_input_definition_dsl.rb +22 -0
- data/lib/active_admin/graphql/policy_set_cache.rb +3 -1
- data/lib/active_admin/graphql/record_source.rb +3 -2
- data/lib/active_admin/graphql/resource_config.rb +21 -2
- data/lib/active_admin/graphql/resource_definition_dsl.rb +19 -0
- data/lib/active_admin/graphql/resource_methods.rb +55 -0
- data/lib/active_admin/graphql/resource_query_proxy/identity.rb +38 -0
- data/lib/active_admin/graphql/resource_query_proxy.rb +13 -8
- data/lib/active_admin/graphql/schema_builder/graph_params.rb +2 -2
- data/lib/active_admin/graphql/schema_builder/mutation_create.rb +6 -2
- data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +19 -9
- data/lib/active_admin/graphql/schema_builder/types_inputs.rb +24 -4
- data/lib/active_admin/graphql/schema_builder/types_object.rb +15 -2
- data/lib/active_admin/graphql/version.rb +1 -1
- data/lib/active_admin/graphql.rb +1 -0
- data/lib/active_admin/primary_key.rb +13 -4
- metadata +71 -24
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "resource_definition_dsl"
|
|
4
|
+
require_relative "resource_methods"
|
|
4
5
|
|
|
5
6
|
module ActiveAdmin
|
|
6
7
|
module GraphQL
|
|
@@ -60,36 +61,34 @@ module ActiveAdmin
|
|
|
60
61
|
end
|
|
61
62
|
end
|
|
62
63
|
|
|
63
|
-
module
|
|
64
|
-
def
|
|
65
|
-
|
|
64
|
+
module ResourceDSLMethods
|
|
65
|
+
def graphql(&block)
|
|
66
|
+
if block
|
|
67
|
+
ActiveAdmin::GraphQL::ResourceDefinitionDSL.new(config.graphql_config).instance_exec(&block)
|
|
68
|
+
end
|
|
69
|
+
config.graphql_config
|
|
66
70
|
end
|
|
67
71
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if cfg.only_attributes
|
|
72
|
-
keys &= cfg.only_attributes
|
|
72
|
+
def permit_params(*args, &block)
|
|
73
|
+
unless block
|
|
74
|
+
config.graphql_config.html_permit_params_attributes = flatten_html_permit_params(args)
|
|
73
75
|
end
|
|
74
|
-
|
|
75
|
-
keys
|
|
76
|
+
super
|
|
76
77
|
end
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
names = attributes_for_graphql.map(&:to_s)
|
|
80
|
-
pk_cols = ActiveAdmin::PrimaryKey.columns(resource_class)
|
|
81
|
-
return names if pk_cols.size > 1
|
|
82
|
-
|
|
83
|
-
names - pk_cols
|
|
84
|
-
end
|
|
85
|
-
end
|
|
79
|
+
private
|
|
86
80
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
def flatten_html_permit_params(args)
|
|
82
|
+
args.flat_map do |arg|
|
|
83
|
+
case arg
|
|
84
|
+
when Symbol, String
|
|
85
|
+
[arg.to_sym]
|
|
86
|
+
when Hash
|
|
87
|
+
arg.keys.map(&:to_sym)
|
|
88
|
+
else
|
|
89
|
+
[]
|
|
90
|
+
end
|
|
91
91
|
end
|
|
92
|
-
config.graphql_config
|
|
93
92
|
end
|
|
94
93
|
end
|
|
95
94
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
module MutationExecutionError
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def validation(record)
|
|
9
|
+
::GraphQL::ExecutionError.new(
|
|
10
|
+
record.errors.full_messages.to_sentence,
|
|
11
|
+
extensions: {"errors" => record.errors.full_messages}
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Create or update input options set from +graphql { create do … }+ / +update do … }+.
|
|
6
|
+
class MutationInputConfig
|
|
7
|
+
attr_accessor :permit_params_attributes
|
|
8
|
+
attr_accessor :resolve_proc
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Nested DSL for +graphql { create do … }+ and +update do … }+, pairing
|
|
6
|
+
# ActiveAdmin +permit_params+ with graphql-ruby-style +resolve+.
|
|
7
|
+
class MutationInputDefinitionDSL
|
|
8
|
+
def initialize(config)
|
|
9
|
+
@config = config
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def permit_params(*attrs)
|
|
13
|
+
@config.permit_params_attributes = attrs.flatten.map(&:to_sym)
|
|
14
|
+
end
|
|
15
|
+
alias_method :permit, :permit_params
|
|
16
|
+
|
|
17
|
+
def resolve(&block)
|
|
18
|
+
@config.resolve_proc = block
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -53,7 +53,9 @@ module ActiveAdmin
|
|
|
53
53
|
elsif subject.is_a?(ActiveAdmin::Page)
|
|
54
54
|
"page:#{subject.name}"
|
|
55
55
|
elsif subject.is_a?(ActiveRecord::Base)
|
|
56
|
-
|
|
56
|
+
identity = PrimaryKey.graphql_id_value(subject)
|
|
57
|
+
identity = "#{identity}:object:#{subject.object_id}" if subject.class.primary_key.blank?
|
|
58
|
+
"record:#{subject.class.name}:#{identity}"
|
|
57
59
|
else
|
|
58
60
|
"object:#{subject.object_id}"
|
|
59
61
|
end
|
|
@@ -6,13 +6,14 @@ module ActiveAdmin
|
|
|
6
6
|
class RecordSource < ::GraphQL::Dataloader::Source
|
|
7
7
|
def initialize(model_class)
|
|
8
8
|
@model_class = model_class
|
|
9
|
-
@
|
|
9
|
+
@pk_columns = ActiveAdmin::PrimaryKey.columns(model_class)
|
|
10
|
+
@pk = ActiveAdmin::PrimaryKey.composite?(model_class) ? model_class.primary_key : @pk_columns.first
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def fetch(ids)
|
|
13
14
|
keys = normalized_keys(ids)
|
|
14
15
|
uniq = keys.compact.uniq
|
|
15
|
-
return Array.new(ids.size) { nil } if uniq.empty?
|
|
16
|
+
return Array.new(ids.size) { nil } if uniq.empty? || @pk.nil?
|
|
16
17
|
|
|
17
18
|
indexed = load_records(uniq)
|
|
18
19
|
keys.map { |key| key.nil? ? nil : indexed[key] }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "run_action_mutation_config"
|
|
4
|
+
require_relative "mutation_input_config"
|
|
4
5
|
|
|
5
6
|
module ActiveAdmin
|
|
6
7
|
module GraphQL
|
|
@@ -10,14 +11,16 @@ module ActiveAdmin
|
|
|
10
11
|
attr_accessor :graphql_type_name
|
|
11
12
|
attr_accessor :only_attributes
|
|
12
13
|
attr_accessor :exclude_attributes
|
|
14
|
+
attr_accessor :permit_params_attributes
|
|
15
|
+
attr_accessor :html_permit_params_attributes
|
|
13
16
|
attr_accessor :extension_block
|
|
14
17
|
|
|
15
18
|
# Optional resolver overrides (set from +graphql do+). SchemaBuilder still owns field names,
|
|
16
19
|
# arguments, and types; procs replace only the Ruby resolution body.
|
|
17
20
|
attr_accessor :resolve_index_proc
|
|
18
21
|
attr_accessor :resolve_show_proc
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
attr_writer :resolve_create_proc
|
|
23
|
+
attr_writer :resolve_update_proc
|
|
21
24
|
attr_accessor :resolve_destroy_proc
|
|
22
25
|
|
|
23
26
|
# Default return type for run-action mutations (+batch+, +member+, +collection+) when a kind-specific
|
|
@@ -33,6 +36,22 @@ module ActiveAdmin
|
|
|
33
36
|
!enabled
|
|
34
37
|
end
|
|
35
38
|
|
|
39
|
+
def create_input
|
|
40
|
+
@create_input ||= MutationInputConfig.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def update_input
|
|
44
|
+
@update_input ||= MutationInputConfig.new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def resolve_create_proc
|
|
48
|
+
@resolve_create_proc || create_input.resolve_proc
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def resolve_update_proc
|
|
52
|
+
@resolve_update_proc || update_input.resolve_proc
|
|
53
|
+
end
|
|
54
|
+
|
|
36
55
|
def batch_run_action
|
|
37
56
|
@batch_run_action ||= RunActionMutationConfig.new
|
|
38
57
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "run_action_mutation_config"
|
|
4
4
|
require_relative "run_action_mutation_dsl"
|
|
5
|
+
require_relative "mutation_input_definition_dsl"
|
|
5
6
|
|
|
6
7
|
module ActiveAdmin
|
|
7
8
|
module GraphQL
|
|
@@ -18,6 +19,7 @@ module ActiveAdmin
|
|
|
18
19
|
def type_name(name)
|
|
19
20
|
@config.graphql_type_name = name.to_s
|
|
20
21
|
end
|
|
22
|
+
alias_method :graphql_name, :type_name
|
|
21
23
|
|
|
22
24
|
def only(*attrs)
|
|
23
25
|
@config.only_attributes = attrs.flatten.map(&:to_sym)
|
|
@@ -28,6 +30,23 @@ module ActiveAdmin
|
|
|
28
30
|
end
|
|
29
31
|
alias_method :exclude, :except
|
|
30
32
|
|
|
33
|
+
def permit_params(*attrs)
|
|
34
|
+
@config.permit_params_attributes = attrs.flatten.map(&:to_sym)
|
|
35
|
+
end
|
|
36
|
+
alias_method :permit, :permit_params
|
|
37
|
+
|
|
38
|
+
def create(&block)
|
|
39
|
+
return unless block
|
|
40
|
+
|
|
41
|
+
MutationInputDefinitionDSL.new(@config.create_input).instance_exec(&block)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def update(&block)
|
|
45
|
+
return unless block
|
|
46
|
+
|
|
47
|
+
MutationInputDefinitionDSL.new(@config.update_input).instance_exec(&block)
|
|
48
|
+
end
|
|
49
|
+
|
|
31
50
|
def configure(&block)
|
|
32
51
|
@config.extension_block = block
|
|
33
52
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
module Integration
|
|
6
|
+
module ResourceMethods
|
|
7
|
+
SERVER_OWNED_TIMESTAMP_COLUMNS = %w[created_at updated_at].freeze
|
|
8
|
+
|
|
9
|
+
def graphql_config
|
|
10
|
+
@graphql_config ||= ActiveAdmin::GraphQL::ResourceConfig.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def attributes_for_graphql
|
|
14
|
+
keys = resource_attributes.keys
|
|
15
|
+
cfg = graphql_config
|
|
16
|
+
if cfg.only_attributes
|
|
17
|
+
keys &= cfg.only_attributes
|
|
18
|
+
end
|
|
19
|
+
keys -= cfg.exclude_attributes
|
|
20
|
+
keys
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def graphql_assignable_attribute_names
|
|
24
|
+
names = attributes_for_graphql.map(&:to_s)
|
|
25
|
+
pk_cols = ActiveAdmin::PrimaryKey.columns(resource_class)
|
|
26
|
+
return names if pk_cols.size > 1
|
|
27
|
+
|
|
28
|
+
names - pk_cols
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def graphql_create_attribute_names
|
|
32
|
+
mutation_permit_params_names(
|
|
33
|
+
graphql_config.create_input.permit_params_attributes || graphql_config.permit_params_attributes
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def graphql_update_attribute_names
|
|
38
|
+
mutation_permit_params_names(
|
|
39
|
+
graphql_config.update_input.permit_params_attributes || graphql_config.permit_params_attributes
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def mutation_permit_params_names(permit_list)
|
|
46
|
+
names = graphql_assignable_attribute_names.map(&:to_s)
|
|
47
|
+
permit_list = graphql_config.html_permit_params_attributes if permit_list.nil?
|
|
48
|
+
return names - SERVER_OWNED_TIMESTAMP_COLUMNS if permit_list.nil?
|
|
49
|
+
|
|
50
|
+
names & permit_list.map(&:to_s)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
class ResourceQueryProxy
|
|
6
|
+
module Identity
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def find_inferred_member(controller, model, id)
|
|
10
|
+
primary_key = ActiveAdmin::PrimaryKey.columns(model).first
|
|
11
|
+
raise ActiveRecord::RecordNotFound unless primary_key
|
|
12
|
+
|
|
13
|
+
records = controller.send(:scoped_collection).where(primary_key => id).limit(2).to_a
|
|
14
|
+
raise_ambiguous_id!(model) if records.size > 1
|
|
15
|
+
|
|
16
|
+
records.first || raise(ActiveRecord::RecordNotFound)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def index_records_by_id(relation, model, primary_key)
|
|
20
|
+
if model.primary_key.present?
|
|
21
|
+
return relation.index_by { |record| record.public_send(primary_key).to_s }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
grouped = relation.to_a.group_by { |record| record.public_send(primary_key).to_s }
|
|
25
|
+
if grouped.any? { |_id, matches| matches.size > 1 }
|
|
26
|
+
raise_ambiguous_id!(model)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
grouped.transform_values(&:first)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def raise_ambiguous_id!(model)
|
|
33
|
+
raise ::GraphQL::ExecutionError, "ambiguous GraphQL id for #{model.name}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -4,14 +4,14 @@ require "rack/mock"
|
|
|
4
4
|
require "stringio"
|
|
5
5
|
|
|
6
6
|
require_relative "resource_query_proxy/controller"
|
|
7
|
+
require_relative "resource_query_proxy/identity"
|
|
7
8
|
|
|
8
9
|
module ActiveAdmin
|
|
9
10
|
module GraphQL
|
|
10
|
-
# Reuses
|
|
11
|
-
# +find_resource+, authorization scoping, Ransack, menu scopes, +sorting+, +includes+)
|
|
12
|
-
# so GraphQL list/detail mutations align with the HTML/JSON list and member REST endpoints.
|
|
11
|
+
# Reuses ResourceController data access so GraphQL operations align with the REST endpoints.
|
|
13
12
|
class ResourceQueryProxy
|
|
14
13
|
include Controller
|
|
14
|
+
include Identity
|
|
15
15
|
|
|
16
16
|
def initialize(aa_resource:, user:, namespace:, graph_params: {})
|
|
17
17
|
@aa_resource = aa_resource
|
|
@@ -26,7 +26,12 @@ module ActiveAdmin
|
|
|
26
26
|
|
|
27
27
|
def find_member(id)
|
|
28
28
|
extra = member_route_params_for_find(id)
|
|
29
|
-
controller_for("show", extra)
|
|
29
|
+
controller = controller_for("show", extra)
|
|
30
|
+
model = @aa_resource.resource_class
|
|
31
|
+
finder = controller.send(:method_for_find)
|
|
32
|
+
return controller.send(:find_resource) if model.primary_key.present? || finder != :find
|
|
33
|
+
|
|
34
|
+
find_inferred_member(controller, model, extra.fetch("id"))
|
|
30
35
|
rescue ActiveRecord::RecordNotFound
|
|
31
36
|
nil
|
|
32
37
|
end
|
|
@@ -42,10 +47,10 @@ module ActiveAdmin
|
|
|
42
47
|
|
|
43
48
|
controller = controller_for("index")
|
|
44
49
|
relation = controller.send(:apply_authorization_scope, controller.send(:scoped_collection))
|
|
45
|
-
primary_key = model.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
primary_key = ActiveAdmin::PrimaryKey.columns(model).first&.to_sym
|
|
51
|
+
return string_ids.index_with { nil } if primary_key.nil?
|
|
52
|
+
|
|
53
|
+
indexed = index_records_by_id(relation.where(primary_key => string_ids), model, primary_key)
|
|
49
54
|
string_ids.index_with { |identifier| indexed[identifier] }
|
|
50
55
|
end
|
|
51
56
|
|
|
@@ -28,9 +28,9 @@ module ActiveAdmin
|
|
|
28
28
|
h
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def assignable_slice_from_input(aa_res, input)
|
|
31
|
+
def assignable_slice_from_input(aa_res, input, attribute_names: nil)
|
|
32
32
|
blob = input.to_h.stringify_keys
|
|
33
|
-
names = aa_res.graphql_assignable_attribute_names.map(&:to_s)
|
|
33
|
+
names = (attribute_names || aa_res.graphql_assignable_attribute_names).map(&:to_s)
|
|
34
34
|
if (btc = aa_res.belongs_to_config)
|
|
35
35
|
names -= [btc.to_param.to_s]
|
|
36
36
|
end
|
|
@@ -29,7 +29,11 @@ module ActiveAdmin
|
|
|
29
29
|
namespace: ns,
|
|
30
30
|
graph_params: sb.graph_params_from_input(aa_res, input)
|
|
31
31
|
)
|
|
32
|
-
attrs = sb.assignable_slice_from_input(
|
|
32
|
+
attrs = sb.assignable_slice_from_input(
|
|
33
|
+
aa_res,
|
|
34
|
+
input,
|
|
35
|
+
attribute_names: aa_res.graphql_create_attribute_names
|
|
36
|
+
)
|
|
33
37
|
record = if (hook = aa_res.graphql_config.resolve_create_proc)
|
|
34
38
|
hook.call(
|
|
35
39
|
proxy: proxy,
|
|
@@ -46,7 +50,7 @@ module ActiveAdmin
|
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
unless r.save
|
|
49
|
-
raise
|
|
53
|
+
raise MutationExecutionError.validation(r)
|
|
50
54
|
end
|
|
51
55
|
r
|
|
52
56
|
end
|
|
@@ -42,7 +42,11 @@ module ActiveAdmin
|
|
|
42
42
|
raise ::GraphQL::ExecutionError, "not authorized to update this record"
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
attrs = sb.assignable_slice_from_input(
|
|
45
|
+
attrs = sb.assignable_slice_from_input(
|
|
46
|
+
aa_res,
|
|
47
|
+
input,
|
|
48
|
+
attribute_names: aa_res.graphql_update_attribute_names
|
|
49
|
+
)
|
|
46
50
|
record = if (hook = aa_res.graphql_config.resolve_update_proc)
|
|
47
51
|
hook.call(
|
|
48
52
|
proxy: proxy,
|
|
@@ -54,9 +58,9 @@ module ActiveAdmin
|
|
|
54
58
|
aa_resource: aa_res
|
|
55
59
|
)
|
|
56
60
|
else
|
|
57
|
-
attrs = attrs.stringify_keys.slice(*aa_res.
|
|
61
|
+
attrs = attrs.stringify_keys.slice(*aa_res.graphql_update_attribute_names)
|
|
58
62
|
unless record.update(attrs)
|
|
59
|
-
raise
|
|
63
|
+
raise MutationExecutionError.validation(record)
|
|
60
64
|
end
|
|
61
65
|
record
|
|
62
66
|
end
|
|
@@ -66,17 +70,22 @@ module ActiveAdmin
|
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
def mutation_destroy_field(sb, ns, aa_res, model)
|
|
69
|
-
|
|
73
|
+
stem = aa_res.resource_name.route_key.singularize.tr("-", "_")
|
|
74
|
+
delete_name = "delete_#{stem}"
|
|
75
|
+
destroy_name = "destroy_#{stem}"
|
|
70
76
|
find_type = @find_input_types[model]
|
|
77
|
+
type_basename = sb.send(:graphql_type_name_for, aa_res)
|
|
71
78
|
|
|
72
79
|
proc do
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
[delete_name, destroy_name].each do |fname|
|
|
81
|
+
field fname.to_sym, ::GraphQL::Types::Boolean, null: false, camelize: false,
|
|
82
|
+
visibility: {kind: :mutation_delete, graphql_type_name: type_basename, resource: aa_res},
|
|
83
|
+
description: "Destroy #{model.name}" do
|
|
84
|
+
argument :where, find_type, required: true, camelize: false
|
|
85
|
+
end
|
|
77
86
|
end
|
|
78
87
|
|
|
79
|
-
define_method(
|
|
88
|
+
define_method(delete_name.to_sym) do |where:, **|
|
|
80
89
|
auth = context[:auth]
|
|
81
90
|
blob = where.to_h.stringify_keys
|
|
82
91
|
graph = sb.graph_params_from_find_blob(aa_res, blob)
|
|
@@ -112,6 +121,7 @@ module ActiveAdmin
|
|
|
112
121
|
end
|
|
113
122
|
true
|
|
114
123
|
end
|
|
124
|
+
alias_method destroy_name.to_sym, delete_name.to_sym
|
|
115
125
|
end
|
|
116
126
|
end
|
|
117
127
|
end
|
|
@@ -6,10 +6,16 @@ module ActiveAdmin
|
|
|
6
6
|
module TypesInputs
|
|
7
7
|
private
|
|
8
8
|
|
|
9
|
-
def
|
|
9
|
+
def mark_empty_input_object!(input_class)
|
|
10
|
+
return if input_class.any_arguments?
|
|
11
|
+
|
|
12
|
+
input_class.has_no_arguments(true)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def define_input_assignable_arguments!(input_class, aa_res, required:, attribute_names:)
|
|
10
16
|
model = aa_res.resource_class
|
|
11
17
|
cols_by_name = model.columns.index_by(&:name)
|
|
12
|
-
gassign =
|
|
18
|
+
gassign = attribute_names.map(&:to_s)
|
|
13
19
|
if (btc = aa_res.belongs_to_config)
|
|
14
20
|
gassign -= [btc.to_param.to_s]
|
|
15
21
|
end
|
|
@@ -33,12 +39,19 @@ module ActiveAdmin
|
|
|
33
39
|
graphql_name "#{gname}CreateInput"
|
|
34
40
|
description "Attributes (and nested route params) for creating #{model.name}"
|
|
35
41
|
|
|
36
|
-
builder.send(
|
|
42
|
+
builder.send(
|
|
43
|
+
:define_input_assignable_arguments!,
|
|
44
|
+
self,
|
|
45
|
+
aa_res,
|
|
46
|
+
required: false,
|
|
47
|
+
attribute_names: aa_res.graphql_create_attribute_names
|
|
48
|
+
)
|
|
37
49
|
|
|
38
50
|
if btc
|
|
39
51
|
argument btc.to_param.to_sym, ::GraphQL::Types::ID, required: btc.required?, camelize: false
|
|
40
52
|
end
|
|
41
53
|
end
|
|
54
|
+
mark_empty_input_object!(klass)
|
|
42
55
|
attach_input_object_visibility!(klass, "#{gname}CreateInput", aa_res, :create_input)
|
|
43
56
|
klass
|
|
44
57
|
end
|
|
@@ -53,12 +66,19 @@ module ActiveAdmin
|
|
|
53
66
|
graphql_name "#{gname}UpdateInput"
|
|
54
67
|
description "Partial attributes (and nested route params) for updating #{model.name}"
|
|
55
68
|
|
|
56
|
-
builder.send(
|
|
69
|
+
builder.send(
|
|
70
|
+
:define_input_assignable_arguments!,
|
|
71
|
+
self,
|
|
72
|
+
aa_res,
|
|
73
|
+
required: false,
|
|
74
|
+
attribute_names: aa_res.graphql_update_attribute_names
|
|
75
|
+
)
|
|
57
76
|
|
|
58
77
|
if btc
|
|
59
78
|
argument btc.to_param.to_sym, ::GraphQL::Types::ID, required: false, camelize: false
|
|
60
79
|
end
|
|
61
80
|
end
|
|
81
|
+
mark_empty_input_object!(klass)
|
|
62
82
|
attach_input_object_visibility!(klass, "#{gname}UpdateInput", aa_res, :update_input)
|
|
63
83
|
klass
|
|
64
84
|
end
|
|
@@ -83,12 +83,25 @@ module ActiveAdmin
|
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
pk_cols = ActiveAdmin::PrimaryKey.columns(model)
|
|
86
|
+
if pk_cols.empty?
|
|
87
|
+
raise ArgumentError,
|
|
88
|
+
"#{model.name} has no primary key for GraphQL `id`; set self.primary_key on the model"
|
|
89
|
+
end
|
|
86
90
|
|
|
87
91
|
type_class.field :id, ::GraphQL::Types::ID, null: false, authorize: false
|
|
88
92
|
type_class.define_method(:id) { ActiveAdmin::PrimaryKey.graphql_id_value(object) }
|
|
89
93
|
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
# ActiveAdmin omits a single-column primary key from resource_attributes; still expose
|
|
95
|
+
# non-id key columns as scalars (composite keys already remain in attributes).
|
|
96
|
+
# ActiveAdminResource owns GraphQL `id`; never emit a second field from an `id` column.
|
|
97
|
+
configured_pk_cols = pk_cols
|
|
98
|
+
if (only = aa_res.graphql_config.only_attributes)
|
|
99
|
+
configured_pk_cols &= only.map(&:to_s)
|
|
100
|
+
end
|
|
101
|
+
configured_pk_cols -= aa_res.graphql_config.exclude_attributes.map(&:to_s)
|
|
102
|
+
readable_names = (attr_names + configured_pk_cols).uniq
|
|
103
|
+
readable_names.each do |name|
|
|
104
|
+
next if name == "id"
|
|
92
105
|
|
|
93
106
|
col = cols_by_name[name]
|
|
94
107
|
next unless col
|
data/lib/active_admin/graphql.rb
CHANGED
|
@@ -14,13 +14,22 @@ unless defined?(ActiveAdmin::PrimaryKey)
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def ordered_columns(model)
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
cols = case (pk = model.primary_key)
|
|
18
|
+
when nil, false then []
|
|
19
|
+
when Array then pk.map(&:to_s).reject(&:empty?)
|
|
20
|
+
else
|
|
21
|
+
(name = pk.to_s).empty? ? [] : [name]
|
|
22
|
+
end
|
|
23
|
+
return cols if cols.any?
|
|
24
|
+
return %w[id] if model.respond_to?(:columns_hash) && model.columns_hash.key?("id")
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
[]
|
|
27
|
+
rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished
|
|
28
|
+
[]
|
|
22
29
|
end
|
|
23
30
|
|
|
31
|
+
def columns(model) = ordered_columns(model)
|
|
32
|
+
|
|
24
33
|
def composite_attribute_hash(model, id_param)
|
|
25
34
|
cols = ordered_columns(model)
|
|
26
35
|
hash = normalize_composite_input(cols, id_param)
|