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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/CODE_OF_CONDUCT.md +31 -0
  4. data/CONTRIBUTING.md +27 -0
  5. data/LICENSE.md +21 -0
  6. data/README.md +49 -0
  7. data/activeadmin-graphql.gemspec +66 -0
  8. data/app/controllers/active_admin/graphql_controller.rb +168 -0
  9. data/docs/graphql-api.md +486 -0
  10. data/lib/active_admin/graphql/auth_context.rb +35 -0
  11. data/lib/active_admin/graphql/engine.rb +9 -0
  12. data/lib/active_admin/graphql/integration.rb +135 -0
  13. data/lib/active_admin/graphql/key_value_pair_input.rb +48 -0
  14. data/lib/active_admin/graphql/railtie.rb +10 -0
  15. data/lib/active_admin/graphql/record_source.rb +30 -0
  16. data/lib/active_admin/graphql/resource_config.rb +68 -0
  17. data/lib/active_admin/graphql/resource_definition_dsl.rb +117 -0
  18. data/lib/active_admin/graphql/resource_interface.rb +25 -0
  19. data/lib/active_admin/graphql/resource_query_proxy/controller.rb +149 -0
  20. data/lib/active_admin/graphql/resource_query_proxy.rb +112 -0
  21. data/lib/active_admin/graphql/run_action_mutation_config.rb +23 -0
  22. data/lib/active_admin/graphql/run_action_mutation_dsl.rb +32 -0
  23. data/lib/active_admin/graphql/run_action_payload.rb +27 -0
  24. data/lib/active_admin/graphql/schema_builder/build.rb +84 -0
  25. data/lib/active_admin/graphql/schema_builder/graph_params.rb +75 -0
  26. data/lib/active_admin/graphql/schema_builder/mutation_action_types.rb +52 -0
  27. data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +61 -0
  28. data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +118 -0
  29. data/lib/active_admin/graphql/schema_builder/mutation_create.rb +65 -0
  30. data/lib/active_admin/graphql/schema_builder/mutation_member.rb +122 -0
  31. data/lib/active_admin/graphql/schema_builder/mutation_type_builder.rb +52 -0
  32. data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +120 -0
  33. data/lib/active_admin/graphql/schema_builder/query_type.rb +53 -0
  34. data/lib/active_admin/graphql/schema_builder/query_type_collection.rb +84 -0
  35. data/lib/active_admin/graphql/schema_builder/query_type_member.rb +91 -0
  36. data/lib/active_admin/graphql/schema_builder/query_type_pages.rb +44 -0
  37. data/lib/active_admin/graphql/schema_builder/query_type_registered.rb +57 -0
  38. data/lib/active_admin/graphql/schema_builder/resolvers.rb +116 -0
  39. data/lib/active_admin/graphql/schema_builder/resources.rb +48 -0
  40. data/lib/active_admin/graphql/schema_builder/types_inputs.rb +119 -0
  41. data/lib/active_admin/graphql/schema_builder/types_object.rb +96 -0
  42. data/lib/active_admin/graphql/schema_builder/visibility.rb +58 -0
  43. data/lib/active_admin/graphql/schema_builder/wire.rb +36 -0
  44. data/lib/active_admin/graphql/schema_builder.rb +62 -0
  45. data/lib/active_admin/graphql/schema_field.rb +29 -0
  46. data/lib/active_admin/graphql/version.rb +7 -0
  47. data/lib/active_admin/graphql.rb +68 -0
  48. data/lib/active_admin/primary_key.rb +117 -0
  49. data/lib/activeadmin/graphql.rb +5 -0
  50. metadata +389 -0
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ class SchemaBuilder
6
+ module TypesInputs
7
+ private
8
+
9
+ def define_input_assignable_arguments!(input_class, aa_res, required:)
10
+ model = aa_res.resource_class
11
+ cols_by_name = model.columns.index_by(&:name)
12
+ gassign = aa_res.graphql_assignable_attribute_names.map(&:to_s)
13
+ if (btc = aa_res.belongs_to_config)
14
+ gassign -= [btc.to_param.to_s]
15
+ end
16
+
17
+ gassign.each do |name|
18
+ col = cols_by_name[name]
19
+ next unless col
20
+
21
+ gql_t = graphql_scalar_for_column(aa_res, model, col)
22
+ input_class.argument(name.to_sym, gql_t, required: required, camelize: false)
23
+ end
24
+ end
25
+
26
+ def build_create_input_type(aa_res)
27
+ model = aa_res.resource_class
28
+ gname = graphql_type_name_for(aa_res)
29
+ btc = aa_res.belongs_to_config
30
+ builder = self
31
+
32
+ klass = Class.new(::GraphQL::Schema::InputObject) do
33
+ graphql_name "#{gname}CreateInput"
34
+ description "Attributes (and nested route params) for creating #{model.name}"
35
+
36
+ builder.send(:define_input_assignable_arguments!, self, aa_res, required: false)
37
+
38
+ if btc
39
+ argument btc.to_param.to_sym, ::GraphQL::Types::ID, required: btc.required?, camelize: false
40
+ end
41
+ end
42
+ attach_input_object_visibility!(klass, "#{gname}CreateInput", aa_res, :create_input)
43
+ klass
44
+ end
45
+
46
+ def build_update_input_type(aa_res)
47
+ model = aa_res.resource_class
48
+ gname = graphql_type_name_for(aa_res)
49
+ btc = aa_res.belongs_to_config
50
+ builder = self
51
+
52
+ klass = Class.new(::GraphQL::Schema::InputObject) do
53
+ graphql_name "#{gname}UpdateInput"
54
+ description "Partial attributes (and nested route params) for updating #{model.name}"
55
+
56
+ builder.send(:define_input_assignable_arguments!, self, aa_res, required: false)
57
+
58
+ if btc
59
+ argument btc.to_param.to_sym, ::GraphQL::Types::ID, required: false, camelize: false
60
+ end
61
+ end
62
+ attach_input_object_visibility!(klass, "#{gname}UpdateInput", aa_res, :update_input)
63
+ klass
64
+ end
65
+
66
+ def build_list_filter_input_type(aa_res)
67
+ gname = graphql_type_name_for(aa_res)
68
+ btc = aa_res.belongs_to_config
69
+
70
+ klass = Class.new(::GraphQL::Schema::InputObject) do
71
+ graphql_name "#{gname}ListFilterInput"
72
+ description "Index-style filters for #{aa_res.resource_name} (+scope+, +order+, Ransack +q+, parent ids)."
73
+
74
+ argument :scope, ::GraphQL::Types::String, required: false, camelize: false
75
+ argument :order, ::GraphQL::Types::String, required: false, camelize: false
76
+ argument :q, ::GraphQL::Types::JSON, required: false, camelize: false
77
+ if btc
78
+ argument btc.to_param.to_sym, ::GraphQL::Types::ID, required: false, camelize: false
79
+ end
80
+ end
81
+ attach_input_object_visibility!(klass, "#{gname}ListFilterInput", aa_res, :list_filter_input)
82
+ klass
83
+ end
84
+
85
+ def build_find_input_type(aa_res)
86
+ gname = graphql_type_name_for(aa_res)
87
+ btc = aa_res.belongs_to_config
88
+ model = aa_res.resource_class
89
+ builder = self
90
+
91
+ klass = Class.new(::GraphQL::Schema::InputObject) do
92
+ graphql_name "#{gname}WhereInput"
93
+ description "Primary key (and nested parents) for loading one #{aa_res.resource_name} record."
94
+
95
+ if ActiveAdmin::PrimaryKey.composite?(model)
96
+ argument :id, ::GraphQL::Types::ID, required: false, camelize: false,
97
+ description: "JSON object string with all primary keys, e.g. " \
98
+ "{\"book_code\":\"x\",\"seq\":1}"
99
+ ActiveAdmin::PrimaryKey.ordered_columns(model).each do |col|
100
+ coldef = model.columns_hash[col]
101
+ next unless coldef
102
+
103
+ gql_t = builder.send(:graphql_scalar_for_column, aa_res, model, coldef)
104
+ argument col.to_sym, gql_t, required: false, camelize: false
105
+ end
106
+ else
107
+ argument :id, ::GraphQL::Types::ID, required: true, camelize: false
108
+ end
109
+ if btc
110
+ argument btc.to_param.to_sym, ::GraphQL::Types::ID, required: btc.required?, camelize: false
111
+ end
112
+ end
113
+ attach_input_object_visibility!(klass, "#{gname}WhereInput", aa_res, :where_input)
114
+ klass
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ class SchemaBuilder
6
+ module TypesObject
7
+ private
8
+
9
+ def build_enum_type(aa_res, column_name, mapping)
10
+ key = [aa_res.resource_class.name, column_name.to_s]
11
+ return @enum_types[key] if @enum_types[key]
12
+
13
+ gql_enum_name = self.class.graphql_enum_type_name(graphql_type_name_for(aa_res), column_name)
14
+ enum_class = Class.new(::GraphQL::Schema::Enum) do
15
+ graphql_name gql_enum_name
16
+ description "Rails enum `#{aa_res.resource_class.name}##{column_name}`"
17
+
18
+ mapping.each_key do |k|
19
+ value(k.to_s.upcase.gsub(/[^A-Z0-9_]/, "_").squeeze("_"), value: k)
20
+ end
21
+ end
22
+ gql_en = gql_enum_name
23
+ coln = column_name.to_s
24
+ ar = aa_res
25
+ enum_class.define_singleton_method(:visible?) do |ctx|
26
+ hook = ctx[:namespace]&.graphql_schema_visible
27
+ return super(ctx) if hook.nil?
28
+
29
+ super(ctx) && !!hook.call(ctx, {kind: :resource_enum, graphql_type_name: gql_en, resource: ar, column: coln})
30
+ end
31
+ @enum_types[key] = enum_class
32
+ end
33
+
34
+ def graphql_scalar_for_column(aa_res, model, col)
35
+ if model.defined_enums.key?(col.name)
36
+ return build_enum_type(aa_res, col.name, model.defined_enums[col.name])
37
+ end
38
+
39
+ case col.type
40
+ when :integer, :bigint
41
+ ::GraphQL::Types::Int
42
+ when :float, :decimal
43
+ ::GraphQL::Types::Float
44
+ when :boolean
45
+ ::GraphQL::Types::Boolean
46
+ when :datetime, :timestamp
47
+ ::GraphQL::Types::ISO8601DateTime
48
+ when :date
49
+ ::GraphQL::Types::ISO8601Date
50
+ when :json, :jsonb
51
+ ::GraphQL::Types::JSON
52
+ else
53
+ ::GraphQL::Types::String
54
+ end
55
+ end
56
+
57
+ def build_object_type(aa_res)
58
+ model = aa_res.resource_class
59
+ gname = graphql_type_name_for(aa_res)
60
+ cols_by_name = model.columns.index_by(&:name)
61
+ attr_names = attributes_for(aa_res).map(&:to_s)
62
+
63
+ type_class = Class.new(::GraphQL::Schema::Object) do
64
+ graphql_name gname
65
+ description "ActiveAdmin resource `#{aa_res.resource_name}`"
66
+ implements ::ActiveAdmin::GraphQL::ResourceInterface
67
+ end
68
+
69
+ pk_cols = ActiveAdmin::PrimaryKey.columns(model)
70
+
71
+ type_class.field :id, ::GraphQL::Types::ID, null: false
72
+ type_class.define_method(:id) { ActiveAdmin::PrimaryKey.graphql_id_value(object) }
73
+
74
+ attr_names.each do |name|
75
+ next if pk_cols.include?(name) && pk_cols.size == 1
76
+
77
+ col = cols_by_name[name]
78
+ next unless col
79
+
80
+ gql_t = graphql_scalar_for_column(aa_res, model, col)
81
+ type_class.field(name.to_sym, gql_t, null: true, camelize: false)
82
+
83
+ type_class.define_method(name.to_sym) { object.public_send(name) }
84
+ end
85
+
86
+ if (ext = aa_res.graphql_config.extension_block)
87
+ type_class.class_eval(&ext)
88
+ end
89
+
90
+ attach_resource_object_visibility!(type_class, gname, aa_res)
91
+ type_class
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ class SchemaBuilder
6
+ module Visibility
7
+ private
8
+
9
+ def apply_graphql_visibility!(schema)
10
+ vis = @namespace.graphql_visibility
11
+ return if vis.nil?
12
+
13
+ if vis == true
14
+ schema.use(::GraphQL::Schema::Visibility)
15
+ elsif vis.is_a?(Hash)
16
+ schema.use(::GraphQL::Schema::Visibility, **vis.symbolize_keys)
17
+ else
18
+ raise ActiveAdmin::DependencyError,
19
+ "namespace graphql_visibility must be nil, true, or a Hash of GraphQL::Schema::Visibility options"
20
+ end
21
+ end
22
+
23
+ def attach_registered_resource_union_visibility!(union_class)
24
+ union_class.define_singleton_method(:visible?) do |ctx|
25
+ hook = ctx[:namespace]&.graphql_schema_visible
26
+ return super(ctx) if hook.nil?
27
+
28
+ super(ctx) && !!hook.call(ctx, {kind: :registered_resource_union})
29
+ end
30
+ end
31
+
32
+ def attach_resource_object_visibility!(type_class, graphql_name, aa_res)
33
+ type_class.field_class(::ActiveAdmin::GraphQL::SchemaField)
34
+ gn = graphql_name
35
+ ar = aa_res
36
+ type_class.define_singleton_method(:visible?) do |ctx|
37
+ hook = ctx[:namespace]&.graphql_schema_visible
38
+ return super(ctx) if hook.nil?
39
+
40
+ super(ctx) && !!hook.call(ctx, {kind: :resource_object, graphql_type_name: gn, resource: ar})
41
+ end
42
+ end
43
+
44
+ def attach_input_object_visibility!(input_class, graphql_type_name, aa_res, role)
45
+ gtn = graphql_type_name
46
+ ar = aa_res
47
+ r = role
48
+ input_class.define_singleton_method(:visible?) do |ctx|
49
+ hook = ctx[:namespace]&.graphql_schema_visible
50
+ return super(ctx) if hook.nil?
51
+
52
+ super(ctx) && !!hook.call(ctx, {kind: :input_object, graphql_type_name: gtn, resource: ar, role: r})
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ class SchemaBuilder
6
+ module Wire
7
+ private
8
+
9
+ def wire_belongs_to_associations
10
+ @object_types.each do |model, type_class|
11
+ aa_res = @aa_by_model[model]
12
+ next unless aa_res
13
+
14
+ model.reflect_on_all_associations(:belongs_to).each do |ref|
15
+ next if ref.polymorphic?
16
+ target = ref.klass
17
+ next unless @object_types[target]
18
+
19
+ field_name = ref.name.to_sym
20
+ fk = ref.foreign_key.to_s
21
+
22
+ type_class.field field_name, @object_types[target], null: true, camelize: false
23
+
24
+ type_class.define_method(field_name) do
25
+ fk_val = object.public_send(fk)
26
+ next nil if fk_val.nil?
27
+
28
+ dataloader.with(ActiveAdmin::GraphQL::RecordSource, target).load(fk_val)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ class SchemaBuilder
6
+ end
7
+ end
8
+ end
9
+
10
+ require_relative "schema_builder/graph_params"
11
+ require_relative "schema_builder/visibility"
12
+ require_relative "schema_builder/resources"
13
+ require_relative "schema_builder/types_object"
14
+ require_relative "schema_builder/types_inputs"
15
+ require_relative "schema_builder/wire"
16
+ require_relative "schema_builder/query_type_registered"
17
+ require_relative "schema_builder/query_type_collection"
18
+ require_relative "schema_builder/query_type_member"
19
+ require_relative "schema_builder/query_type_pages"
20
+ require_relative "schema_builder/query_type"
21
+ require_relative "schema_builder/resolvers"
22
+ require_relative "schema_builder/mutation_action_types"
23
+ require_relative "schema_builder/mutation_type_builder"
24
+ require_relative "schema_builder/mutation_create"
25
+ require_relative "schema_builder/mutation_update_destroy"
26
+ require_relative "schema_builder/mutation_batch"
27
+ require_relative "schema_builder/mutation_member"
28
+ require_relative "schema_builder/mutation_collection"
29
+ require_relative "schema_builder/build"
30
+
31
+ module ActiveAdmin
32
+ module GraphQL
33
+ class SchemaBuilder
34
+ def self.graphql_enum_type_name(type_basename, column_name)
35
+ base = type_basename.to_s.gsub(/[^a-zA-Z0-9_]/, "_").squeeze("_")
36
+ col = column_name.to_s.gsub(/[^a-zA-Z0-9_]/, "_").squeeze("_")
37
+ "#{base.camelize}Enum#{col.camelize(:upper)}"
38
+ end
39
+
40
+ def initialize(namespace)
41
+ @namespace = namespace
42
+ end
43
+
44
+ include GraphParams
45
+ include Visibility
46
+ include Resources
47
+ include TypesObject
48
+ include TypesInputs
49
+ include Wire
50
+ include QueryType
51
+ include Resolvers
52
+ include MutationActionTypes
53
+ include MutationTypeBuilder
54
+ include MutationCreate
55
+ include MutationUpdateDestroy
56
+ include MutationBatch
57
+ include MutationMember
58
+ include MutationCollection
59
+ include Build
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Subclass of {::GraphQL::Schema::Field} for namespace +Query+ / +Mutation+ fields.
6
+ #
7
+ # The +visibility:+ keyword accepts an optional Hash of metadata forwarded to
8
+ # +graphql_schema_visible+ (second argument) when {GraphQL::Schema::Visibility} is enabled.
9
+ # That hook runs from {#visible?}; it does not replace graphql-ruby's visibility system—it
10
+ # composes with +super+ like any custom +Field+ class.
11
+ class SchemaField < ::GraphQL::Schema::Field
12
+ def initialize(visibility: nil, **kwargs, &block)
13
+ @visibility = visibility
14
+ super(**kwargs, &block)
15
+ end
16
+
17
+ def visible?(ctx)
18
+ return false unless super
19
+
20
+ return true unless @visibility
21
+
22
+ hook = ctx[:namespace]&.graphql_schema_visible
23
+ return true if hook.nil?
24
+
25
+ !!hook.call(ctx, @visibility)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ # GraphQL HTTP API for ActiveAdmin (graphql-ruby), shipped in the +activeadmin-graphql+ gem.
5
+ #
6
+ # Bundling +activeadmin-graphql+ loads +graphql-ruby+ and registers this integration. Enable the
7
+ # HTTP endpoint per namespace in +config/initializers/active_admin.rb+:
8
+ #
9
+ # ActiveAdmin.setup do |config|
10
+ # config.namespace :admin do |api|
11
+ # api.graphql = true
12
+ # end
13
+ # end
14
+ #
15
+ module GraphQL
16
+ SCHEMA_CACHE = {}
17
+ end
18
+ end
19
+
20
+ require_relative "graphql/resource_config"
21
+
22
+ module ActiveAdmin
23
+ module GraphQL
24
+ class << self
25
+ # Requires graphql-ruby and schema builders. Safe to call repeatedly; loads once.
26
+ def load!
27
+ return if @graphql_features_loaded
28
+
29
+ @graphql_features_loaded = true
30
+ 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"
45
+ end
46
+
47
+ # @param namespace [ActiveAdmin::Namespace]
48
+ # @return [Class] schema class (subclass of GraphQL::Schema)
49
+ def schema_for(namespace)
50
+ 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
53
+ end
54
+
55
+ def clear_schema_cache!
56
+ SCHEMA_CACHE.clear
57
+ end
58
+
59
+ def clear_schema_for!(namespace)
60
+ SCHEMA_CACHE.delete(namespace.name)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ require_relative "graphql/integration"
67
+ ActiveAdmin::GraphQL::Integration.install!
68
+ ActiveAdmin::GraphQL.load!
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shim: GraphQL uses composite-PK helpers; upstream ActiveAdmin may not ship this module yet.
4
+ unless defined?(ActiveAdmin::PrimaryKey)
5
+ require "json"
6
+
7
+ module ActiveAdmin
8
+ module PrimaryKey
9
+ module_function
10
+
11
+ def composite?(model)
12
+ pk = model.primary_key
13
+ pk.is_a?(Array) && pk.size > 1
14
+ end
15
+
16
+ def ordered_columns(model)
17
+ Array(model.primary_key).map(&:to_s)
18
+ end
19
+
20
+ def columns(model)
21
+ ordered_columns(model)
22
+ end
23
+
24
+ def composite_attribute_hash(model, id_param)
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
42
+ raise ArgumentError, "composite id missing keys: #{missing.join(", ")}" if missing.any?
43
+
44
+ absent = cols.select { |c| h[c].nil? }
45
+ raise ArgumentError, "composite id missing values for: #{absent.join(", ")}" if absent.any?
46
+
47
+ h
48
+ end
49
+
50
+ def find_attributes(model, id_param)
51
+ cols = ordered_columns(model)
52
+ if cols.size == 1
53
+ {cols.first => id_param}
54
+ else
55
+ composite_attribute_hash(model, id_param)
56
+ end
57
+ end
58
+
59
+ def graphql_id_value(record)
60
+ cols = ordered_columns(record.class)
61
+ if cols.size == 1
62
+ record.public_send(cols.first).to_s
63
+ else
64
+ payload = cols.each_with_object({}) { |c, m| m[c] = record.read_attribute(c) }
65
+ JSON.generate(payload)
66
+ end
67
+ end
68
+
69
+ def dataloader_tuple(model, raw)
70
+ cols = ordered_columns(model)
71
+ return raw if cols.size == 1
72
+
73
+ h = composite_attribute_hash(model, raw)
74
+ cols.map { |c| h[c] }
75
+ end
76
+
77
+ def dataloader_tuple_from_record(record)
78
+ ordered_columns(record.class).map { |c| record.read_attribute(c) }
79
+ end
80
+
81
+ def member_param_hash(model, blob)
82
+ blob = blob.stringify_keys
83
+ cols = ordered_columns(model)
84
+ 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]] }
93
+ else
94
+ raise ArgumentError, "composite id requires id (JSON) or all primary key columns"
95
+ end
96
+ end
97
+
98
+ def field_kw_to_param_hash(model, id:, **kw)
99
+ cols = ordered_columns(model)
100
+ 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
106
+ 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
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_admin"
4
+ require_relative "../active_admin/primary_key"
5
+ require_relative "../active_admin/graphql"