hq-graphql 2.1.12 → 2.2.3
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/README.md +25 -9
- data/lib/hq/graphql.rb +17 -8
- data/lib/hq/graphql/association_loader.rb +1 -0
- data/lib/hq/graphql/comparator.rb +22 -21
- data/lib/hq/graphql/enum/filter_operation.rb +15 -0
- data/lib/hq/graphql/enum/sort_by.rb +8 -4
- data/lib/hq/graphql/enum/sort_order.rb +8 -4
- data/lib/hq/graphql/ext.rb +8 -0
- data/lib/hq/graphql/ext/active_record_extensions.rb +148 -0
- data/lib/hq/graphql/ext/enum_extensions.rb +81 -0
- data/lib/hq/graphql/ext/input_object_extensions.rb +110 -0
- data/lib/hq/graphql/ext/mutation_extensions.rb +24 -0
- data/lib/hq/graphql/ext/object_extensions.rb +122 -0
- data/lib/hq/graphql/ext/schema_extensions.rb +50 -0
- data/lib/hq/graphql/field.rb +1 -1
- data/lib/hq/graphql/filter_operations.rb +59 -0
- data/lib/hq/graphql/filters.rb +150 -0
- data/lib/hq/graphql/paginated_association_loader.rb +1 -0
- data/lib/hq/graphql/record_loader.rb +1 -0
- data/lib/hq/graphql/resource.rb +86 -15
- data/lib/hq/graphql/resource/auto_mutation.rb +8 -5
- data/lib/hq/graphql/root_mutation.rb +1 -1
- data/lib/hq/graphql/root_query.rb +3 -3
- data/lib/hq/graphql/scalars.rb +0 -2
- data/lib/hq/graphql/types.rb +1 -2
- data/lib/hq/graphql/types/uuid.rb +3 -5
- data/lib/hq/graphql/util.rb +13 -0
- data/lib/hq/graphql/version.rb +1 -1
- metadata +28 -38
- data/lib/hq/graphql/active_record_extensions.rb +0 -143
- data/lib/hq/graphql/enum.rb +0 -78
- data/lib/hq/graphql/input_object.rb +0 -104
- data/lib/hq/graphql/mutation.rb +0 -15
- data/lib/hq/graphql/object.rb +0 -120
- data/lib/hq/graphql/schema.rb +0 -22
- data/lib/hq/graphql/types/object.rb +0 -35
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hq/graphql/types"
|
4
|
+
|
5
|
+
module HQ::GraphQL
|
6
|
+
module Ext
|
7
|
+
module EnumExtensions
|
8
|
+
## Auto generate enums from the database using ActiveRecord
|
9
|
+
# This comes in handy when we have constants that we want represented as enums.
|
10
|
+
#
|
11
|
+
# == Example
|
12
|
+
# Let's assume we're saving data into a user types table
|
13
|
+
# # select * from user_types;
|
14
|
+
# id | name
|
15
|
+
# --- +-------------
|
16
|
+
# 1 | Admin
|
17
|
+
# 2 | Support User
|
18
|
+
# (2 rows)
|
19
|
+
#
|
20
|
+
# ```ruby
|
21
|
+
# class Enums::UserType < ::HQ::GraphQL::Enum
|
22
|
+
# with_model
|
23
|
+
# end
|
24
|
+
# ```
|
25
|
+
#
|
26
|
+
# Creates the following enum:
|
27
|
+
# ```graphql
|
28
|
+
# enum UserType {
|
29
|
+
# Admin
|
30
|
+
# SupportUser
|
31
|
+
# }
|
32
|
+
# ```
|
33
|
+
def with_model(
|
34
|
+
klass = default_model_name.safe_constantize,
|
35
|
+
prefix: nil,
|
36
|
+
register: true,
|
37
|
+
scope: nil,
|
38
|
+
strip: /(^[^_a-zA-Z])|([^_a-zA-Z0-9]*)/,
|
39
|
+
value_method: :name
|
40
|
+
)
|
41
|
+
raise ArgumentError.new(<<~ERROR) if !klass
|
42
|
+
`::HQ::GraphQL::Enum.with_model {...}' had trouble automatically inferring the class name.
|
43
|
+
Avoid this by manually passing in the class name: `::HQ::GraphQL::Enum.with_model(#{default_model_name}) {...}`
|
44
|
+
ERROR
|
45
|
+
|
46
|
+
if register
|
47
|
+
::HQ::GraphQL.enums << klass
|
48
|
+
::HQ::GraphQL::Types.register(klass, self)
|
49
|
+
end
|
50
|
+
|
51
|
+
lazy_load do
|
52
|
+
records = scope ? klass.instance_exec(&scope) : klass.all
|
53
|
+
records.each do |record|
|
54
|
+
value "#{prefix}#{record.send(value_method).gsub(strip, "")}", value: record
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def lazy_load(&block)
|
60
|
+
@lazy_load ||= []
|
61
|
+
if block
|
62
|
+
::HQ::GraphQL.lazy_load(self)
|
63
|
+
@lazy_load << block
|
64
|
+
end
|
65
|
+
@lazy_load
|
66
|
+
end
|
67
|
+
|
68
|
+
def lazy_load!
|
69
|
+
lazy_load.shift.call while lazy_load.length > 0
|
70
|
+
@lazy_load = []
|
71
|
+
end
|
72
|
+
|
73
|
+
def default_model_name
|
74
|
+
to_s.sub(/^((::)?\w+)::/, "")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
::GraphQL::Schema::Enum.extend ::HQ::GraphQL::Ext::EnumExtensions
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hq/graphql/ext/active_record_extensions"
|
4
|
+
require "hq/graphql/inputs"
|
5
|
+
require "hq/graphql/types"
|
6
|
+
|
7
|
+
module HQ
|
8
|
+
module GraphQL
|
9
|
+
module Ext
|
10
|
+
module InputObjectExtensions
|
11
|
+
def self.included(klass)
|
12
|
+
klass.include Scalars
|
13
|
+
klass.include InstanceMethods
|
14
|
+
klass.include ActiveRecordExtensions
|
15
|
+
klass.extend ActiveRecordExtensions
|
16
|
+
klass.extend ClassMethods
|
17
|
+
end
|
18
|
+
|
19
|
+
module InstanceMethods
|
20
|
+
# Recursively format attributes so that they are compatible with `accepts_nested_attributes_for`
|
21
|
+
def format_nested_attributes
|
22
|
+
self.each.inject({}) do |formatted_attrs, (key, value) |
|
23
|
+
if self.class.nested_attributes.include?(key.to_s)
|
24
|
+
formatted_value =
|
25
|
+
if value.is_a?(Array)
|
26
|
+
value.map(&:format_nested_attributes)
|
27
|
+
elsif value
|
28
|
+
value.format_nested_attributes
|
29
|
+
end
|
30
|
+
|
31
|
+
formatted_attrs[:"#{key}_attributes"] = formatted_value if formatted_value
|
32
|
+
elsif key.to_s == "x"
|
33
|
+
formatted_attrs[:X] = value
|
34
|
+
else
|
35
|
+
formatted_attrs[key] = value
|
36
|
+
end
|
37
|
+
formatted_attrs
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
#### Class Methods ####
|
44
|
+
def with_model(model_name, attributes: true, associations: false, enums: true, excluded_inputs: [])
|
45
|
+
self.model_name = model_name
|
46
|
+
self.auto_load_attributes = attributes
|
47
|
+
self.auto_load_associations = associations
|
48
|
+
self.auto_load_enums = enums
|
49
|
+
|
50
|
+
lazy_load do
|
51
|
+
excluded_inputs += ::HQ::GraphQL.excluded_inputs
|
52
|
+
|
53
|
+
model_columns.each do |column|
|
54
|
+
argument_from_column(column) unless excluded_inputs.include?(column.name.to_sym)
|
55
|
+
end
|
56
|
+
|
57
|
+
model_associations.each do |association|
|
58
|
+
argument_from_association(association) unless excluded_inputs.include?(association.name.to_sym)
|
59
|
+
end
|
60
|
+
|
61
|
+
argument :X, String, required: false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def nested_attributes
|
66
|
+
@nested_attributes ||= Set.new
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def argument_from_association(association)
|
72
|
+
is_enum = is_enum?(association)
|
73
|
+
input_or_type = is_enum ? ::HQ::GraphQL::Types[association.klass] : ::HQ::GraphQL::Inputs[association.klass]
|
74
|
+
name = association.name
|
75
|
+
return if argument_exists?(name)
|
76
|
+
|
77
|
+
case association.macro
|
78
|
+
when :has_many
|
79
|
+
argument name, [input_or_type], required: false
|
80
|
+
else
|
81
|
+
argument name, input_or_type, required: false
|
82
|
+
end
|
83
|
+
|
84
|
+
return if is_enum
|
85
|
+
|
86
|
+
if !model_klass.nested_attributes_options.key?(name.to_sym)
|
87
|
+
model_klass.accepts_nested_attributes_for name, allow_destroy: true
|
88
|
+
end
|
89
|
+
|
90
|
+
nested_attributes << name.to_s
|
91
|
+
rescue ::HQ::GraphQL::Inputs::Error
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def argument_from_column(column)
|
96
|
+
name = column.name
|
97
|
+
return if argument_exists?(name)
|
98
|
+
argument name, ::HQ::GraphQL::Types.type_from_column(column), required: false
|
99
|
+
end
|
100
|
+
|
101
|
+
def argument_exists?(name)
|
102
|
+
!!arguments[camelize(name)]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
::GraphQL::Schema::InputObject.include ::HQ::GraphQL::Ext::InputObjectExtensions
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HQ
|
4
|
+
module GraphQL
|
5
|
+
module Ext
|
6
|
+
module MutationExtensions
|
7
|
+
def self.included(klass)
|
8
|
+
klass.include Scalars
|
9
|
+
klass.include ActiveRecordExtensions
|
10
|
+
klass.singleton_class.prepend PrependMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
module PrependMethods
|
14
|
+
def generate_payload_type
|
15
|
+
lazy_load!
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
::GraphQL::Schema::Mutation.include ::HQ::GraphQL::Ext::MutationExtensions
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hq/graphql/ext/active_record_extensions"
|
4
|
+
require "hq/graphql/field"
|
5
|
+
require "hq/graphql/field_extension/association_loader_extension"
|
6
|
+
require "hq/graphql/field_extension/paginated_arguments"
|
7
|
+
require "hq/graphql/field_extension/paginated_loader"
|
8
|
+
require "hq/graphql/object_association"
|
9
|
+
require "hq/graphql/types"
|
10
|
+
|
11
|
+
module HQ
|
12
|
+
module GraphQL
|
13
|
+
module Ext
|
14
|
+
module ObjectExtensions
|
15
|
+
def self.included(klass)
|
16
|
+
klass.include Scalars
|
17
|
+
klass.include ActiveRecordExtensions
|
18
|
+
klass.extend ObjectAssociation
|
19
|
+
klass.singleton_class.prepend PrependMethods
|
20
|
+
klass.field_class Field
|
21
|
+
end
|
22
|
+
|
23
|
+
module PrependMethods
|
24
|
+
def authorize_action(action)
|
25
|
+
self.authorized_action = action
|
26
|
+
end
|
27
|
+
|
28
|
+
def authorized?(object, context)
|
29
|
+
super && ::HQ::GraphQL.authorized?(authorized_action, object, context)
|
30
|
+
end
|
31
|
+
|
32
|
+
def with_model(model_name, attributes: true, associations: true, auto_nil: true, enums: true)
|
33
|
+
self.model_name = model_name
|
34
|
+
self.auto_load_attributes = attributes
|
35
|
+
self.auto_load_associations = associations
|
36
|
+
self.auto_load_enums = enums
|
37
|
+
|
38
|
+
lazy_load do
|
39
|
+
model_columns.each do |column|
|
40
|
+
field_from_column(column, auto_nil: auto_nil)
|
41
|
+
end
|
42
|
+
|
43
|
+
model_associations.each do |association|
|
44
|
+
next if resource_reflections[association.name.to_s]
|
45
|
+
field_from_association(association, auto_nil: auto_nil)
|
46
|
+
end
|
47
|
+
|
48
|
+
resource_reflections.values.each do |resource_reflection|
|
49
|
+
reflection = resource_reflection.reflection(model_klass)
|
50
|
+
next unless reflection
|
51
|
+
field_from_association(reflection, auto_nil: auto_nil, internal_association: true, &resource_reflection.block)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
attr_writer :authorized_action
|
58
|
+
|
59
|
+
def authorized_action
|
60
|
+
@authorized_action ||= :read
|
61
|
+
end
|
62
|
+
|
63
|
+
def field_from_association(association, auto_nil:, internal_association: false, &block)
|
64
|
+
association_klass = association.klass
|
65
|
+
name = association.name.to_s
|
66
|
+
return if field_exists?(name)
|
67
|
+
|
68
|
+
klass = model_klass
|
69
|
+
type = Types[association_klass]
|
70
|
+
|
71
|
+
case association.macro
|
72
|
+
when :has_many
|
73
|
+
field name, [type], null: false, klass: model_name do
|
74
|
+
if ::HQ::GraphQL.use_experimental_associations?
|
75
|
+
extension FieldExtension::PaginatedArguments, klass: association_klass
|
76
|
+
extension FieldExtension::PaginatedLoader, klass: klass, association: name, internal_association: internal_association
|
77
|
+
else
|
78
|
+
extension FieldExtension::AssociationLoaderExtension, klass: klass
|
79
|
+
end
|
80
|
+
instance_eval(&block) if block
|
81
|
+
end
|
82
|
+
when :has_one
|
83
|
+
field name, type, null: !auto_nil || !has_presence_validation?(association), klass: model_name do
|
84
|
+
extension FieldExtension::AssociationLoaderExtension, klass: klass
|
85
|
+
end
|
86
|
+
else
|
87
|
+
field name, type, null: !auto_nil || !association_required?(association), klass: model_name do
|
88
|
+
extension FieldExtension::AssociationLoaderExtension, klass: klass
|
89
|
+
end
|
90
|
+
end
|
91
|
+
rescue Types::Error
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def field_from_column(column, auto_nil:)
|
96
|
+
name = column.name
|
97
|
+
return if field_exists?(name)
|
98
|
+
|
99
|
+
field name, Types.type_from_column(column), null: !auto_nil || column.null
|
100
|
+
end
|
101
|
+
|
102
|
+
def field_exists?(name)
|
103
|
+
!!fields[camelize(name)]
|
104
|
+
end
|
105
|
+
|
106
|
+
def association_required?(association)
|
107
|
+
!association.options[:optional] || has_presence_validation?(association)
|
108
|
+
end
|
109
|
+
|
110
|
+
def has_presence_validation?(association)
|
111
|
+
model_klass.validators.any? do |validation|
|
112
|
+
next unless validation.class == ActiveRecord::Validations::PresenceValidator && !(validation.options.include?(:if) || validation.options.include?(:unless))
|
113
|
+
validation.attributes.any? { |a| a.to_s == association.name.to_s }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
::GraphQL::Schema::Object.include ::HQ::GraphQL::Ext::ObjectExtensions
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HQ
|
4
|
+
module GraphQL
|
5
|
+
module Ext
|
6
|
+
module SchemaExtensions
|
7
|
+
def self.prepended(klass)
|
8
|
+
klass.alias_method :add_type_and_traverse_without_types, :add_type_and_traverse
|
9
|
+
klass.alias_method :add_type_and_traverse, :add_type_and_traverse_with_types
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute(*args, **options)
|
13
|
+
load_types!
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def dump_directory(directory = Rails.root.join("app/graphql"))
|
18
|
+
@dump_directory ||= directory
|
19
|
+
end
|
20
|
+
|
21
|
+
def dump_filename(filename = "#{self.name.underscore}.graphql")
|
22
|
+
@dump_filename ||= filename
|
23
|
+
end
|
24
|
+
|
25
|
+
def dump
|
26
|
+
load_types!
|
27
|
+
::FileUtils.mkdir_p(dump_directory)
|
28
|
+
::File.open(::File.join(dump_directory, dump_filename), "w") { |file| file.write(self.to_definition) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def load_types!
|
32
|
+
::HQ::GraphQL.load_types!
|
33
|
+
return if @add_type_and_traverse_with_types.blank?
|
34
|
+
while (args, options = @add_type_and_traverse_with_types.shift)
|
35
|
+
add_type_and_traverse_without_types(*args, **options)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Defer adding types until first schema execution
|
40
|
+
# https://github.com/rmosolgo/graphql-ruby/blob/345ebb2e3833909963067d81e0e8378717b5bdbf/lib/graphql/schema.rb#L1792
|
41
|
+
def add_type_and_traverse_with_types(*args, **options)
|
42
|
+
@add_type_and_traverse_with_types ||= []
|
43
|
+
@add_type_and_traverse_with_types.push([args, options])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
::GraphQL::Schema.singleton_class.prepend ::HQ::GraphQL::Ext::SchemaExtensions
|
data/lib/hq/graphql/field.rb
CHANGED
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hq/graphql/enum/filter_operation"
|
4
|
+
|
5
|
+
module HQ
|
6
|
+
module GraphQL
|
7
|
+
module FilterOperations
|
8
|
+
class Operation
|
9
|
+
attr_reader :name, :arel, :check_for_null, :sanitize
|
10
|
+
|
11
|
+
def initialize(name:, arel:, check_for_null: false, sanitize: nil)
|
12
|
+
@name = name
|
13
|
+
@arel = arel
|
14
|
+
@check_for_null = check_for_null
|
15
|
+
@sanitize = sanitize
|
16
|
+
end
|
17
|
+
|
18
|
+
def sanitize_value(value)
|
19
|
+
sanitize ? sanitize.call(value) : value
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_arel(table:, column_name:, value:)
|
23
|
+
sanitized_value = sanitize_value(value)
|
24
|
+
|
25
|
+
if arel.is_a?(Proc)
|
26
|
+
return arel.call(table: table, column_name: column_name, value: sanitized_value)
|
27
|
+
end
|
28
|
+
|
29
|
+
expression = table[column_name].send(arel, sanitized_value)
|
30
|
+
|
31
|
+
if check_for_null
|
32
|
+
expression = expression.or(table[column_name].eq(nil))
|
33
|
+
end
|
34
|
+
|
35
|
+
expression
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
OPERATIONS = [
|
40
|
+
EQUAL = Operation.new(name: "EQUAL", arel: :eq),
|
41
|
+
NOT_EQUAL = Operation.new(name: "NOT_EQUAL", arel: :not_eq, check_for_null: true),
|
42
|
+
LIKE = Operation.new(name: "LIKE", arel: :matches, sanitize: ->(value) { "%#{ActiveRecord::Base.sanitize_sql_like(value)}%" }),
|
43
|
+
NOT_LIKE = Operation.new(name: "NOT_LIKE", arel: :does_not_match, check_for_null: true, sanitize: ->(value) { "%#{ActiveRecord::Base.sanitize_sql_like(value)}%" }),
|
44
|
+
GREATER_THAN = Operation.new(name: "GREATER_THAN", arel: :gt),
|
45
|
+
LESS_THAN = Operation.new(name: "LESS_THAN", arel: :lt),
|
46
|
+
WITH = Operation.new(
|
47
|
+
name: "WITH",
|
48
|
+
arel: ->(table:, column_name:, value:) do
|
49
|
+
if value.casecmp("t") == 0 || value.casecmp("true") == 0
|
50
|
+
table[column_name].not_eq(nil)
|
51
|
+
else
|
52
|
+
table[column_name].eq(nil)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
)
|
56
|
+
]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|