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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/activeadmin-graphql.gemspec +5 -2
  4. data/app/controllers/active_admin/graphql_controller.rb +83 -31
  5. data/docs/graphql-api.md +28 -4
  6. data/lib/active_admin/graphql/integration.rb +33 -13
  7. data/lib/active_admin/graphql/key_value_pair_input.rb +20 -12
  8. data/lib/active_admin/graphql/policy_set_cache.rb +64 -0
  9. data/lib/active_admin/graphql/record_source.rb +24 -11
  10. data/lib/active_admin/graphql/resource_query_proxy/controller.rb +22 -6
  11. data/lib/active_admin/graphql/resource_query_proxy.rb +32 -4
  12. data/lib/active_admin/graphql/run_action_mutation_config.rb +2 -0
  13. data/lib/active_admin/graphql/run_action_mutation_dsl.rb +4 -0
  14. data/lib/active_admin/graphql/run_action_payload.rb +2 -1
  15. data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +9 -1
  16. data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +11 -2
  17. data/lib/active_admin/graphql/schema_builder/mutation_member.rb +11 -2
  18. data/lib/active_admin/graphql/schema_builder/query_type.rb +7 -0
  19. data/lib/active_admin/graphql/schema_builder/query_type_policies.rb +160 -0
  20. data/lib/active_admin/graphql/schema_builder/types_object.rb +27 -2
  21. data/lib/active_admin/graphql/schema_builder/wire.rb +11 -1
  22. data/lib/active_admin/graphql/schema_builder.rb +1 -0
  23. data/lib/active_admin/graphql/schema_field.rb +21 -1
  24. data/lib/active_admin/graphql/version.rb +1 -1
  25. data/lib/active_admin/graphql.rb +37 -16
  26. data/lib/active_admin/primary_key.rb +61 -39
  27. data/sig/active_admin/graphql.rbs +7 -0
  28. 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
- unless auth.authorized?(aa_res, ActiveAdmin::Authorization::READ, mdl)
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
- unless auth.authorized?(aa_res, ActiveAdmin::Authorization::READ, mdl)
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
- unless auth.authorized?(aa_res, ActiveAdmin::Authorization::READ, mdl)
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
- unless auth.authorized?(aa_res, ActiveAdmin::Authorization::READ, mdl)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveAdmin
4
4
  module GraphQL
5
- VERSION = "0.1.2"
5
+ VERSION = "0.2.1"
6
6
  end
7
7
  end
@@ -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
- require "graphql"
32
- require "graphql/types/json"
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.delete(cache_key) if defined?(Rails) && Rails.env.development?
52
- SCHEMA_CACHE[cache_key] ||= SchemaBuilder.new(namespace).build
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
- h =
27
- case id_param
28
- when Hash
29
- id_param.stringify_keys.slice(*cols)
30
- when String
31
- parsed = JSON.parse(id_param)
32
- raise ArgumentError, "composite id must be a JSON object" unless parsed.is_a?(Hash)
33
-
34
- parsed.stringify_keys.slice(*cols)
35
- when Array
36
- cols.zip(id_param).to_h
37
- else
38
- raise ArgumentError, "unsupported composite id type"
39
- end
40
-
41
- missing = cols - h.keys
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| h[c].nil? }
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
- id = blob["id"]
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
- raise ArgumentError, "composite id requires id (JSON) or all primary key columns"
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
- raise ArgumentError, "id is required" if id.blank?
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
- out = cols.to_h { |c| [c, kw[c.to_sym] || kw[c]] }
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
@@ -0,0 +1,7 @@
1
+ module ActiveAdmin
2
+ module GraphQL
3
+ VERSION: String
4
+
5
+ def self.load!: () -> void
6
+ end
7
+ end