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,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,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
|