hq-graphql 2.0.11 → 2.1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40c69615fc4b2b0d14d3c510d5c4973aa5a0f7bf0285278ca98505af62d73fb0
4
- data.tar.gz: 4f0926421e6e2f8658f9e6aaf0234756eaf66c6dbf126210f56a92f0d4701a8a
3
+ metadata.gz: cffa70521d220d85df139e5dab62fec20a97b121578dea7d5fa6cc0991c46501
4
+ data.tar.gz: b6d9949097849efdd3002dacfc46fe5bf000830d7c86d47bc8f4bfb743321d5a
5
5
  SHA512:
6
- metadata.gz: 945bad36e906b28a2c1444f7e87df594c306b817c61f3bdfa1176f061c645904e9206f663fe82861b0a44ee61c881d8899d6979dc57842b1920070d6a48fa6ff
7
- data.tar.gz: 82cf817c3d801273c0cc4a099aa513585ba96220fc9b9ce84f996bd14cbe04d248a8d759d864fc964d7f07571bb11169c59cf670d7893ce1ea860a4d41dd86d8
6
+ metadata.gz: 3d3933664e856fba5c7111dbea3e6fd6fa653aa5ec170ce5ff6a800d07573d5791290745651513a04ae57e1f2171b38b93d48961350b8bc7a5f9af01fffad507
7
+ data.tar.gz: 4029d92a6198a414fac136c1b87c850aa94ca92e789065cea70c0e31b0c8e3cdaf3578c1b068bfb367d362e0499580223269f3e467773c57e0621e8a3aa627c1
@@ -8,6 +8,10 @@ require "hq/graphql/config"
8
8
 
9
9
  module HQ
10
10
  module GraphQL
11
+ class << self
12
+ delegate :default_object_class, to: :config
13
+ end
14
+
11
15
  def self.config
12
16
  @config ||= ::HQ::GraphQL::Config.new
13
17
  end
@@ -32,14 +36,20 @@ module HQ
32
36
  config.extract_class.call(klass)
33
37
  end
34
38
 
35
- def self.resource_lookup(klass)
36
- config.resource_lookup.call(klass)
39
+ def self.lookup_resource(klass)
40
+ [klass, klass.base_class, klass.superclass].lazy.map do |k|
41
+ config.resource_lookup.call(k) || resources.detect { |r| r.model_klass == k }
42
+ end.reject(&:nil?).first
43
+ end
44
+
45
+ def self.use_experimental_associations?
46
+ !!config.use_experimental_associations
37
47
  end
38
48
 
39
49
  def self.reset!
40
50
  @root_queries = nil
41
51
  @enums = nil
42
- @types = nil
52
+ @resources = nil
43
53
  ::HQ::GraphQL::Inputs.reset!
44
54
  ::HQ::GraphQL::Types.reset!
45
55
  end
@@ -52,13 +62,12 @@ module HQ
52
62
  @enums ||= Set.new
53
63
  end
54
64
 
55
- def self.types
56
- @types ||= Set.new
65
+ def self.resources
66
+ @resources ||= Set.new
57
67
  end
58
68
  end
59
69
  end
60
70
 
61
- require "hq/graphql/active_record_extensions"
62
71
  require "hq/graphql/association_loader"
63
72
  require "hq/graphql/scalars"
64
73
  require "hq/graphql/comparator"
@@ -67,6 +76,7 @@ require "hq/graphql/inputs"
67
76
  require "hq/graphql/input_object"
68
77
  require "hq/graphql/mutation"
69
78
  require "hq/graphql/object"
79
+ require "hq/graphql/paginated_association_loader"
70
80
  require "hq/graphql/resource"
71
81
  require "hq/graphql/root_mutation"
72
82
  require "hq/graphql/root_query"
@@ -27,7 +27,7 @@ module HQ
27
27
  end
28
28
 
29
29
  def lazy_load!
30
- lazy_load.map(&:call)
30
+ lazy_load.each(&:call)
31
31
  @lazy_load = []
32
32
  end
33
33
 
@@ -5,9 +5,11 @@ module HQ
5
5
  class Config < Struct.new(
6
6
  :authorize,
7
7
  :authorize_field,
8
+ :default_object_class,
8
9
  :default_scope,
9
10
  :extract_class,
10
11
  :resource_lookup,
12
+ :use_experimental_associations,
11
13
  keyword_init: true
12
14
  )
13
15
  def initialize(
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "hq/graphql/types"
4
+
3
5
  module HQ::GraphQL
4
6
  class Enum < ::GraphQL::Schema::Enum
5
7
  ## Auto generate enums from the database using ActiveRecord
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hq/graphql/enum"
4
+
5
+ module HQ
6
+ class GraphQL::Enum::SortBy < ::HQ::GraphQL::Enum
7
+ value "CreatedAt", value: :created_at
8
+ value "UpdatedAt", value: :updated_at
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hq/graphql/enum"
4
+
5
+ module HQ
6
+ class GraphQL::Enum::SortOrder < ::HQ::GraphQL::Enum
7
+ value "ASC", value: :asc
8
+ value "DESC", value: :desc
9
+ end
10
+ end
@@ -3,13 +3,21 @@
3
3
  module HQ
4
4
  module GraphQL
5
5
  class Field < ::GraphQL::Schema::Field
6
- attr_reader :authorize_action, :authorize, :klass
6
+ attr_reader :authorize_action, :authorize
7
7
 
8
8
  def initialize(*args, authorize_action: :read, authorize: nil, klass: nil, **options, &block)
9
9
  super(*args, **options, &block)
10
10
  @authorize_action = authorize_action
11
11
  @authorize = authorize
12
- @klass = klass
12
+ @klass_or_string = klass
13
+ end
14
+
15
+ def scope(&block)
16
+ if block
17
+ @scope = block
18
+ else
19
+ @scope
20
+ end
13
21
  end
14
22
 
15
23
  def authorized?(object, ctx)
@@ -18,14 +26,8 @@ module HQ
18
26
  ::HQ::GraphQL.authorize_field(authorize_action, self, object, ctx)
19
27
  end
20
28
 
21
- def resolve_field(object, args, ctx)
22
- if klass.present? && !!::GraphQL::Batch::Executor.current && object.object
23
- AssociationLoader.for(klass.constantize, original_name).load(object.object).then do
24
- super
25
- end
26
- else
27
- super
28
- end
29
+ def klass
30
+ @klass ||= @klass_or_string.is_a?(String) ? @klass_or_string.constantize : @klass_or_string
29
31
  end
30
32
  end
31
33
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hq/graphql/association_loader"
4
+
5
+ module HQ
6
+ module GraphQL
7
+ module FieldExtension
8
+ class AssociationLoaderExtension < ::GraphQL::Schema::FieldExtension
9
+ def resolve(object:, **_kwargs)
10
+ AssociationLoader.for(options[:klass], field.original_name).load(object.object)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hq/graphql/enum/sort_by"
4
+ require "hq/graphql/enum/sort_order"
5
+
6
+ module HQ
7
+ module GraphQL
8
+ module FieldExtension
9
+ class PaginatedArguments < ::GraphQL::Schema::FieldExtension
10
+ def apply
11
+ field.argument :offset, Integer, required: false
12
+ field.argument :limit, Integer, required: false
13
+ field.argument :sort_order, Enum::SortOrder, required: false
14
+
15
+ resource = ::HQ::GraphQL.lookup_resource(options[:klass])
16
+ enum = resource ? resource.sort_fields_enum : ::HQ::GraphQL::Enum::SortBy
17
+ field.argument :sort_by, enum, required: false
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hq/graphql/paginated_association_loader"
4
+
5
+ module HQ
6
+ module GraphQL
7
+ module FieldExtension
8
+ class PaginatedLoader < ::GraphQL::Schema::FieldExtension
9
+ def resolve(object:, arguments:, **_options)
10
+ limit = arguments[:limit]
11
+ offset = arguments[:offset]
12
+ sort_by = arguments[:sort_by]
13
+ sort_order = arguments[:sort_order]
14
+ scope = field.scope.call(**arguments.except(:limit, :offset, :sort_by, :sort_order)) if field.scope
15
+ loader = PaginatedAssociationLoader.for(
16
+ klass,
17
+ association,
18
+ internal_association: internal_association,
19
+ scope: scope,
20
+ limit: limit,
21
+ offset: offset,
22
+ sort_by: sort_by,
23
+ sort_order: sort_order
24
+ )
25
+
26
+ loader.load(object.object)
27
+ end
28
+
29
+ private
30
+
31
+ def association
32
+ options[:association]
33
+ end
34
+
35
+ def internal_association
36
+ options[:internal_association]
37
+ end
38
+
39
+ def klass
40
+ options[:klass]
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "hq/graphql/active_record_extensions"
4
+ require "hq/graphql/inputs"
5
+ require "hq/graphql/types"
6
+
3
7
  module HQ
4
8
  module GraphQL
5
9
  class InputObject < ::GraphQL::Schema::InputObject
@@ -24,14 +24,10 @@ module HQ
24
24
 
25
25
  def klass_for(klass_or_string)
26
26
  klass = klass_or_string.is_a?(String) ? klass_or_string.constantize : klass_or_string
27
- type = find_type(klass)
27
+ resource = ::HQ::GraphQL.lookup_resource(klass)
28
28
 
29
- raise(Error, Error::MISSING_TYPE_MSG % { klass: klass.name }) if !type
30
- type.input_klass
31
- end
32
-
33
- def find_type(klass)
34
- ::HQ::GraphQL.resource_lookup(klass) || ::HQ::GraphQL.types.detect { |t| t.model_klass == klass }
29
+ raise(Error, Error::MISSING_TYPE_MSG % { klass: klass.name }) if !resource
30
+ resource.input_klass
35
31
  end
36
32
  end
37
33
  end
@@ -1,12 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "hq/graphql/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
+
3
11
  module HQ
4
12
  module GraphQL
5
13
  class Object < ::GraphQL::Schema::Object
6
14
  include Scalars
7
- include ::HQ::GraphQL::ActiveRecordExtensions
15
+ include ActiveRecordExtensions
16
+ extend ObjectAssociation
8
17
 
9
- field_class ::HQ::GraphQL::Field
18
+ field_class Field
10
19
 
11
20
  def self.authorize_action(action)
12
21
  self.authorized_action = action
@@ -28,8 +37,15 @@ module HQ
28
37
  end
29
38
 
30
39
  model_associations.each do |association|
40
+ next if resource_reflections[association.name.to_s]
31
41
  field_from_association(association, auto_nil: auto_nil)
32
42
  end
43
+
44
+ resource_reflections.values.each do |resource_reflection|
45
+ reflection = resource_reflection.reflection(model_klass)
46
+ next unless reflection
47
+ field_from_association(reflection, auto_nil: auto_nil, internal_association: true, &resource_reflection.block)
48
+ end
33
49
  end
34
50
  end
35
51
 
@@ -46,21 +62,38 @@ module HQ
46
62
  @authorized_action ||= :read
47
63
  end
48
64
 
49
- def field_from_association(association, auto_nil:)
50
- type = ::HQ::GraphQL::Types[association.klass]
51
- name = association.name
65
+ def field_from_association(association, auto_nil:, internal_association: false, &block)
66
+ association_klass = association.klass
67
+ name = association.name.to_s
68
+ return if fields[name]
69
+
70
+ klass = model_klass
71
+ type = Types[association_klass]
52
72
  case association.macro
53
73
  when :has_many
54
- field name, [type], null: false, klass: model_name
74
+ field name, [type], null: false, klass: model_name do
75
+ if ::HQ::GraphQL.use_experimental_associations?
76
+ extension FieldExtension::PaginatedArguments, klass: association_klass
77
+ extension FieldExtension::PaginatedLoader, klass: klass, association: name, internal_association: internal_association
78
+ else
79
+ extension FieldExtension::AssociationLoaderExtension, klass: klass
80
+ end
81
+ instance_eval(&block) if block
82
+ end
55
83
  else
56
- field name, type, null: !auto_nil || !association_required?(association), klass: model_name
84
+ field name, type, null: !auto_nil || !association_required?(association), klass: model_name do
85
+ extension FieldExtension::AssociationLoaderExtension, klass: klass
86
+ end
57
87
  end
58
- rescue ::HQ::GraphQL::Types::Error
88
+ rescue Types::Error
59
89
  nil
60
90
  end
61
91
 
62
92
  def field_from_column(column, auto_nil:)
63
- field column.name, ::HQ::GraphQL::Types.type_from_column(column), null: !auto_nil || column.null
93
+ name = column.name
94
+ return if fields[name]
95
+
96
+ field name, Types.type_from_column(column), null: !auto_nil || column.null
64
97
  end
65
98
 
66
99
  def association_required?(association)
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HQ
4
+ module GraphQL
5
+ module ObjectAssociation
6
+ def reflect_on_association(association)
7
+ resource_reflections[association.to_s]&.reflection(model_klass)
8
+ end
9
+
10
+ def update(name, &block)
11
+ resource_reflections[name.to_s] = UpdatedReflection.new(name, block)
12
+ end
13
+
14
+ def belongs_to(name, scope = nil, **options, &block)
15
+ add_reflection(name, scope, options, :belongs_to, block)
16
+ end
17
+
18
+ def has_many(name, scope = nil, through: nil, **options, &block)
19
+ raise TypeError, "has_many through is unsupported" if through
20
+ add_reflection(name, scope, options, :has_many, block)
21
+ end
22
+
23
+ private
24
+
25
+ def resource_reflections
26
+ @resource_reflections ||= {}
27
+ end
28
+
29
+ def add_reflection(name, scope, options, macro, block)
30
+ resource_reflections[name.to_s] = ResourceReflection.new(name, scope, options, macro, block)
31
+ end
32
+
33
+ class ResourceReflection
34
+ attr_reader :name, :scope, :options, :macro, :block
35
+
36
+ def initialize(name, scope, options, macro, block)
37
+ @name = name
38
+ @scope = scope
39
+ @options = options
40
+ @macro = macro
41
+ @block = block
42
+ end
43
+
44
+ def reflection(model_klass)
45
+ if macro == :has_many
46
+ ::ActiveRecord::Associations::Builder::HasMany.create_reflection(model_klass, name, scope, options)
47
+ elsif macro == :belongs_to
48
+ ::ActiveRecord::Associations::Builder::BelongsTo.create_reflection(model_klass, name, scope, options)
49
+ end
50
+ end
51
+ end
52
+
53
+ class UpdatedReflection
54
+ attr_reader :name, :block
55
+
56
+ def initialize(name, block)
57
+ @name = name
58
+ @block = block
59
+ end
60
+
61
+ def reflection(model_klass)
62
+ model_klass.reflect_on_association(name)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hq/graphql/types"
4
+
5
+ module HQ
6
+ module GraphQL
7
+ class PaginatedAssociationLoader < ::GraphQL::Batch::Loader
8
+ def self.for(*args, scope: nil, **kwargs)
9
+ if scope
10
+ raise TypeError, "scope must be an ActiveRecord::Relation" unless scope.is_a?(::ActiveRecord::Relation)
11
+ executor = ::GraphQL::Batch::Executor.current
12
+ loader_key = loader_key_for(*args, **kwargs, scope: scope.to_sql)
13
+ executor.loader(loader_key) { new(*args, **kwargs, scope: scope) }
14
+ else
15
+ super
16
+ end
17
+ end
18
+
19
+ def initialize(model, association_name, internal_association: false, limit: nil, offset: nil, scope: nil, sort_by: nil, sort_order: nil)
20
+ @model = model
21
+ @association_name = association_name
22
+ @internal_association = internal_association
23
+ @limit = [0, limit].max if limit
24
+ @offset = [0, offset].max if offset
25
+ @scope = scope
26
+ @sort_by = sort_by || :updated_at
27
+ @sort_order = normalize_sort_order(sort_order)
28
+
29
+ validate!
30
+ end
31
+
32
+ def load(record)
33
+ raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
34
+ super
35
+ end
36
+
37
+ def cache_key(record)
38
+ record.send(primary_key)
39
+ end
40
+
41
+ def perform(records)
42
+ values = records.map { |r| source_value(r) }
43
+ scope =
44
+ if @limit || @offset
45
+ # If a limit or offset is added, then we need to transform the query
46
+ # into a lateral join so that we can limit on groups of data.
47
+ #
48
+ # > SELECT * FROM addresses WHERE addresses.user_id IN ($1, $2, ..., $N) ORDER BY addresses.created_at DESC;
49
+ # ...becomes
50
+ # > SELECT DISTINCT a_top.*
51
+ # > FROM addresses
52
+ # > INNER JOIN LATERAL (
53
+ # > SELECT inner.*
54
+ # > FROM addresses inner
55
+ # > WHERE inner.user_id = addresses.user_id
56
+ # > ORDER BY inner.created_at DESC
57
+ # > LIMIT 1
58
+ # > ) a_top ON TRUE
59
+ # > WHERE addresses.user_id IN ($1, $2, ..., $N)
60
+ # > ORDER BY a_top.created_at DESC
61
+ inner_table = association_class.arel_table
62
+ lateral_join_table = through_reflection? ? through_association.klass.arel_table : inner_table
63
+ from_table = lateral_join_table.alias("outer")
64
+
65
+ inside_scope = default_scope.
66
+ select(inner_table[::Arel.star]).
67
+ from(inner_table).
68
+ where(lateral_join_table[target_join_key].eq(from_table[target_join_key])).
69
+ reorder(arel_order(inner_table)).
70
+ limit(@limit).
71
+ offset(@offset)
72
+
73
+ if through_reflection?
74
+ # expose the through_reflection key
75
+ inside_scope = inside_scope.select(lateral_join_table[target_join_key])
76
+ end
77
+
78
+ lateral_table = ::Arel::Table.new("top")
79
+ association_class.
80
+ select(lateral_table[::Arel.star]).distinct.
81
+ from(from_table).
82
+ where(from_table[target_join_key].in(values)).
83
+ joins("INNER JOIN LATERAL (#{inside_scope.to_sql}) #{lateral_table.name} ON TRUE").
84
+ reorder(arel_order(lateral_table))
85
+ else
86
+ scope = default_scope.reorder(arel_order(association_class.arel_table))
87
+
88
+ if through_reflection?
89
+ scope.where(through_association.name => { target_join_key => values }).
90
+ # expose the through_reflection key
91
+ select(association_class.arel_table[::Arel.star], through_association.klass.arel_table[target_join_key])
92
+ else
93
+ scope.where(target_join_key => values)
94
+ end
95
+ end
96
+
97
+ results = scope.to_a
98
+ records.each do |record|
99
+ fulfill(record, target_value(record, results)) unless fulfilled?(record)
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def source_join_key
106
+ belongs_to? ? association.foreign_key : association.association_primary_key
107
+ end
108
+
109
+ def source_value(record)
110
+ record.send(source_join_key)
111
+ end
112
+
113
+ def target_join_key
114
+ if through_reflection?
115
+ through_association.foreign_key
116
+ elsif belongs_to?
117
+ association.association_primary_key
118
+ else
119
+ association.foreign_key
120
+ end
121
+ end
122
+
123
+ def target_value(record, results)
124
+ enumerator = has_many? ? :select : :detect
125
+ results.send(enumerator) { |r| r.send(target_join_key) == source_value(record) }
126
+ end
127
+
128
+ def default_scope
129
+ scope = association_class
130
+ scope = association.scopes.reduce(scope, &:merge)
131
+ scope = association_class.default_scopes.reduce(scope, &:merge)
132
+ scope = scope.merge(@scope) if @scope
133
+
134
+ if through_reflection?
135
+ source = association_class.arel_table
136
+ target = through_association.klass.arel_table
137
+ join = source.join(target).on(target[association.foreign_key].eq(source[source_join_key]))
138
+ scope = scope.joins(join.join_sources)
139
+ end
140
+
141
+ scope
142
+ end
143
+
144
+ def belongs_to?
145
+ association.macro == :belongs_to
146
+ end
147
+
148
+ def has_many?
149
+ association.macro == :has_many
150
+ end
151
+
152
+ def through_association
153
+ association.through_reflection
154
+ end
155
+
156
+ def through_reflection?
157
+ association.through_reflection?
158
+ end
159
+
160
+ def association
161
+ if @internal_association
162
+ Types[@model].reflect_on_association(@association_name)
163
+ else
164
+ @model.reflect_on_association(@association_name)
165
+ end
166
+ end
167
+
168
+ def association_class
169
+ association.klass
170
+ end
171
+
172
+ def primary_key
173
+ @model.primary_key
174
+ end
175
+
176
+ def arel_order(table)
177
+ table[@sort_by].send(@sort_order)
178
+ end
179
+
180
+ def normalize_sort_order(input)
181
+ if input.to_s.casecmp("asc").zero?
182
+ :asc
183
+ else
184
+ :desc
185
+ end
186
+ end
187
+
188
+ def validate!
189
+ raise ArgumentError, "No association #{@association_name} on #{@model}" unless association
190
+ end
191
+ end
192
+ end
193
+ end
@@ -1,19 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "hq/graphql/resource/mutation"
3
+ require "hq/graphql/enum/sort_by"
4
+ require "hq/graphql/field_extension/paginated_arguments"
5
+ require "hq/graphql/input_object"
6
+ require "hq/graphql/object"
7
+ require "hq/graphql/resource/auto_mutation"
8
+ require "hq/graphql/scalars"
4
9
 
5
10
  module HQ
6
11
  module GraphQL
7
12
  module Resource
8
13
  def self.included(base)
9
14
  super
10
- ::HQ::GraphQL.types << base
15
+ ::HQ::GraphQL.resources << base
11
16
  base.include Scalars
12
17
  base.include ::GraphQL::Types
13
18
  base.extend ClassMethods
14
19
  end
15
20
 
16
21
  module ClassMethods
22
+ include AutoMutation
23
+
17
24
  attr_writer :graphql_name, :model_name
18
25
 
19
26
  def scope(context)
@@ -52,12 +59,24 @@ module HQ
52
59
  @input_klass ||= build_input_object
53
60
  end
54
61
 
55
- def nil_query_klass
56
- @nil_query_klass ||= build_graphql_object(name: "#{graphql_name}Copy", auto_nil: false)
62
+ def nil_query_object
63
+ @nil_query_object ||= build_graphql_object(name: "#{graphql_name}Copy", auto_nil: false)
64
+ end
65
+
66
+ def query_object
67
+ @query_object ||= begin
68
+ if @query_object_options
69
+ options, block = @query_object_options
70
+ @query_object_options = nil
71
+ build_graphql_object(**options, &block)
72
+ else
73
+ build_graphql_object
74
+ end
75
+ end
57
76
  end
58
77
 
59
- def query_klass
60
- @query_klass ||= build_graphql_object
78
+ def sort_fields_enum
79
+ @sort_fields_enum || ::HQ::GraphQL::Enum::SortBy
61
80
  end
62
81
 
63
82
  protected
@@ -71,155 +90,29 @@ module HQ
71
90
  end
72
91
 
73
92
  def mutations(create: true, copy: true, update: true, destroy: true)
74
- scoped_graphql_name = graphql_name
75
- scoped_model_name = model_name
76
- scoped_self = self
77
-
78
- if create
79
- create_mutation = ::HQ::GraphQL::Resource::Mutation.build(model_name, action: :create, graphql_name: "#{scoped_graphql_name}Create") do
80
- define_method(:resolve) do |**args|
81
- resource = scoped_self.new_record(context)
82
- resource.assign_attributes(args[:attributes].format_nested_attributes)
83
- if resource.save
84
- {
85
- resource: resource,
86
- errors: {},
87
- }
88
- else
89
- {
90
- resource: nil,
91
- errors: errors_from_resource(resource)
92
- }
93
- end
94
- end
95
-
96
- lazy_load do
97
- argument :attributes, ::HQ::GraphQL::Inputs[scoped_model_name], required: true
98
- end
99
- end
100
-
101
- mutation_klasses["create_#{scoped_graphql_name.underscore}"] = create_mutation
102
- end
103
-
104
- if copy
105
- copy_mutation = ::HQ::GraphQL::Resource::Mutation.build(
106
- model_name,
107
- action: :copy,
108
- graphql_name: "#{scoped_graphql_name}Copy",
109
- require_primary_key: true,
110
- nil_klass: true
111
- ) do
112
- define_method(:resolve) do |**args|
113
- resource = scoped_self.find_record(args, context)
114
-
115
- if resource
116
- copy = resource.copy
117
- if copy.save
118
- {
119
- resource: copy,
120
- errors: {},
121
- }
122
- else
123
- {
124
- resource: copy,
125
- errors: errors_from_resource(copy)
126
- }
127
- end
128
- else
129
- {
130
- resource: nil,
131
- errors: { resource: "Unable to find #{scoped_graphql_name}" }
132
- }
133
- end
134
- end
135
- end
136
-
137
- mutation_klasses["copy_#{scoped_graphql_name.underscore}"] = copy_mutation
138
- end
139
-
140
- if update
141
- update_mutation = ::HQ::GraphQL::Resource::Mutation.build(
142
- model_name,
143
- action: :update,
144
- graphql_name: "#{scoped_graphql_name}Update",
145
- require_primary_key: true
146
- ) do
147
- define_method(:resolve) do |**args|
148
- resource = scoped_self.find_record(args, context)
149
-
150
- if resource
151
- resource.assign_attributes(args[:attributes].format_nested_attributes)
152
- if resource.save
153
- {
154
- resource: resource,
155
- errors: {},
156
- }
157
- else
158
- {
159
- resource: nil,
160
- errors: errors_from_resource(resource)
161
- }
162
- end
163
- else
164
- {
165
- resource: nil,
166
- errors: { resource: "Unable to find #{scoped_graphql_name}" }
167
- }
168
- end
169
- end
170
-
171
- lazy_load do
172
- argument :attributes, ::HQ::GraphQL::Inputs[scoped_model_name], required: true
173
- end
174
- end
175
-
176
- mutation_klasses["update_#{scoped_graphql_name.underscore}"] = update_mutation
177
- end
93
+ mutation_klasses["create_#{graphql_name.underscore}"] = build_create if create
94
+ mutation_klasses["copy_#{graphql_name.underscore}"] = build_copy if copy
95
+ mutation_klasses["update_#{graphql_name.underscore}"] = build_update if update
96
+ mutation_klasses["destroy_#{graphql_name.underscore}"] = build_destroy if destroy
97
+ end
178
98
 
179
- if destroy
180
- destroy_mutation = ::HQ::GraphQL::Resource::Mutation.build(
181
- model_name,
182
- action: :destroy,
183
- graphql_name: "#{scoped_graphql_name}Destroy",
184
- require_primary_key: true
185
- ) do
186
- define_method(:resolve) do |**attrs|
187
- resource = scoped_self.find_record(attrs, context)
188
-
189
- if resource
190
- if resource.destroy
191
- {
192
- resource: resource,
193
- errors: {},
194
- }
195
- else
196
- {
197
- resource: nil,
198
- errors: errors_from_resource(resource)
199
- }
200
- end
201
- else
202
- {
203
- resource: nil,
204
- errors: { resource: "Unable to find #{scoped_graphql_name}" }
205
- }
206
- end
207
- end
208
- end
99
+ def query(**options, &block)
100
+ @query_object_options = [options, block]
101
+ end
209
102
 
210
- mutation_klasses["destroy_#{scoped_graphql_name.underscore}"] = destroy_mutation
211
- end
103
+ def query_class(klass)
104
+ @query_class = klass
212
105
  end
213
106
 
214
- def query(**options, &block)
215
- @query_klass = build_graphql_object(**options, &block)
107
+ def sort_fields(*fields)
108
+ self.sort_fields_enum = fields
216
109
  end
217
110
 
218
111
  def def_root(field_name, is_array: false, null: true, &block)
219
- graphql = self
112
+ resource = self
220
113
  resolver = -> {
221
114
  Class.new(::GraphQL::Schema::Resolver) do
222
- type = is_array ? [graphql.query_klass] : graphql.query_klass
115
+ type = is_array ? [resource.query_object] : resource.query_object
223
116
  type type, null: null
224
117
  class_eval(&block) if block
225
118
  end
@@ -229,7 +122,7 @@ module HQ
229
122
  }
230
123
  end
231
124
 
232
- def root_query(find_one: true, find_all: true, pagination: false, per_page_max: 250)
125
+ def root_query(find_one: true, find_all: true, pagination: true, limit_max: 250)
233
126
  field_name = graphql_name.underscore
234
127
  scoped_self = self
235
128
 
@@ -248,18 +141,22 @@ module HQ
248
141
 
249
142
  if find_all
250
143
  def_root field_name.pluralize, is_array: true, null: false do
251
- argument :page, Integer, required: false
252
- argument :per_page, Integer, required: false
144
+ extension FieldExtension::PaginatedArguments, klass: scoped_self.model_klass if pagination
253
145
 
254
- define_method(:resolve) do |page: nil, per_page: nil, **_attrs|
146
+ define_method(:resolve) do |limit: nil, offset: nil, sort_by: nil, sort_order: nil, **_attrs|
255
147
  scope = scoped_self.scope(context).all
256
148
 
257
- if pagination || page || per_page
258
- page ||= 0
259
- limit = [per_page_max, *per_page].min
260
- scope = scope.limit(limit).offset(page * limit)
149
+ if pagination || page || limit
150
+ offset = [0, *offset].max
151
+ limit = [[limit_max, *limit].min, 0].max
152
+ scope = scope.limit(limit).offset(offset)
261
153
  end
262
154
 
155
+ sort_by ||= :updated_at
156
+ sort_order ||= :desc
157
+ # There should be no risk for SQL injection since an enum is being used for both sort_by and sort_order
158
+ scope = scope.reorder(sort_by => sort_order)
159
+
263
160
  scope
264
161
  end
265
162
  end
@@ -271,7 +168,8 @@ module HQ
271
168
  def build_graphql_object(name: graphql_name, **options, &block)
272
169
  scoped_graphql_name = name
273
170
  scoped_model_name = model_name
274
- Class.new(::HQ::GraphQL::Object) do
171
+ object_class = @query_class || ::HQ::GraphQL.default_object_class || ::HQ::GraphQL::Object
172
+ Class.new(object_class) do
275
173
  graphql_name scoped_graphql_name
276
174
 
277
175
  with_model scoped_model_name, **options
@@ -291,6 +189,16 @@ module HQ
291
189
  class_eval(&block) if block
292
190
  end
293
191
  end
192
+
193
+ def sort_fields_enum=(fields)
194
+ @sort_fields_enum ||= Class.new(::HQ::GraphQL::Enum::SortBy).tap do |c|
195
+ c.graphql_name "#{graphql_name}Sort"
196
+ end
197
+
198
+ Array(fields).each do |field|
199
+ @sort_fields_enum.value field.to_s.classify, value: field
200
+ end
201
+ end
294
202
  end
295
203
  end
296
204
  end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hq/graphql/inputs"
4
+ require "hq/graphql/mutation"
5
+ require "hq/graphql/types"
6
+
7
+ module HQ
8
+ module GraphQL
9
+ module Resource
10
+ module AutoMutation
11
+ def build_create
12
+ scoped_self = self
13
+
14
+ build_mutation(action: :create) do
15
+ define_method(:resolve) do |**args|
16
+ resource = scoped_self.new_record(context)
17
+ resource.assign_attributes(args[:attributes].format_nested_attributes)
18
+ if resource.save
19
+ {
20
+ resource: resource,
21
+ errors: {},
22
+ }
23
+ else
24
+ {
25
+ resource: nil,
26
+ errors: errors_from_resource(resource)
27
+ }
28
+ end
29
+ end
30
+
31
+ lazy_load do
32
+ argument :attributes, ::HQ::GraphQL::Inputs[scoped_self.model_name], required: true
33
+ end
34
+ end
35
+ end
36
+
37
+ def build_update
38
+ scoped_self = self
39
+
40
+ build_mutation(action: :update, require_primary_key: true) do
41
+ define_method(:resolve) do |**args|
42
+ resource = scoped_self.find_record(args, context)
43
+
44
+ if resource
45
+ resource.assign_attributes(args[:attributes].format_nested_attributes)
46
+ if resource.save
47
+ {
48
+ resource: resource,
49
+ errors: {},
50
+ }
51
+ else
52
+ {
53
+ resource: nil,
54
+ errors: errors_from_resource(resource)
55
+ }
56
+ end
57
+ else
58
+ {
59
+ resource: nil,
60
+ errors: { resource: "Unable to find #{self.class.graphql_name}" }
61
+ }
62
+ end
63
+ end
64
+
65
+ lazy_load do
66
+ argument :attributes, ::HQ::GraphQL::Inputs[scoped_self.model_name], required: true
67
+ end
68
+ end
69
+ end
70
+
71
+ def build_copy
72
+ scoped_self = self
73
+
74
+ build_mutation(action: :copy, require_primary_key: true, nil_klass: true) do
75
+ define_method(:resolve) do |**args|
76
+ resource = scoped_self.find_record(args, context)
77
+
78
+ if resource
79
+ copy = resource.copy
80
+ if copy.save
81
+ {
82
+ resource: copy,
83
+ errors: {},
84
+ }
85
+ else
86
+ {
87
+ resource: copy,
88
+ errors: errors_from_resource(copy)
89
+ }
90
+ end
91
+ else
92
+ {
93
+ resource: nil,
94
+ errors: { resource: "Unable to find #{self.class.graphql_name}" }
95
+ }
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ def build_destroy
102
+ scoped_self = self
103
+
104
+ build_mutation(action: :destroy, require_primary_key: true) do
105
+ define_method(:resolve) do |**attrs|
106
+ resource = scoped_self.find_record(attrs, context)
107
+
108
+ if resource
109
+ if resource.destroy
110
+ {
111
+ resource: resource,
112
+ errors: {},
113
+ }
114
+ else
115
+ {
116
+ resource: nil,
117
+ errors: errors_from_resource(resource)
118
+ }
119
+ end
120
+ else
121
+ {
122
+ resource: nil,
123
+ errors: { resource: "Unable to find #{self.class.graphql_name}" }
124
+ }
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ def build_mutation(action:, require_primary_key: false, nil_klass: false, &block)
131
+ gql_name = "#{graphql_name}#{action.to_s.titleize}"
132
+ scoped_model_name = model_name
133
+ Class.new(::HQ::GraphQL::Mutation) do
134
+ graphql_name gql_name
135
+
136
+ define_method(:ready?) do |*args|
137
+ super(*args) && ::HQ::GraphQL.authorized?(action, scoped_model_name, context)
138
+ end
139
+
140
+ lazy_load do
141
+ field :errors, ::HQ::GraphQL::Types::Object, null: false
142
+ field :resource, ::HQ::GraphQL::Types[scoped_model_name, nil_klass], null: true
143
+ end
144
+
145
+ instance_eval(&block)
146
+
147
+ if require_primary_key
148
+ lazy_load do
149
+ klass = scoped_model_name.constantize
150
+ primary_key = klass.primary_key
151
+ argument primary_key, ::GraphQL::Types::ID, required: true
152
+ end
153
+ end
154
+
155
+ def errors_from_resource(resource)
156
+ resource.errors.to_h.deep_transform_keys { |k| k.to_s.camelize(:lower) }
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -7,7 +7,7 @@ module HQ
7
7
  super
8
8
  base.class_eval do
9
9
  lazy_load do
10
- ::HQ::GraphQL.types.each do |type|
10
+ ::HQ::GraphQL.resources.each do |type|
11
11
  type.mutation_klasses.each do |mutation_name, klass|
12
12
  field mutation_name, mutation: klass
13
13
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "hq/graphql/types/object"
4
+ require "hq/graphql/types/uuid"
5
+
3
6
  module HQ
4
7
  module GraphQL
5
8
  module Types
@@ -10,7 +13,7 @@ module HQ
10
13
  def self.registry
11
14
  @registry ||= Hash.new do |hash, options|
12
15
  klass, nil_klass = Array(options)
13
- hash[klass] = nil_klass ? nil_query_klass(klass) : klass_for(klass)
16
+ hash[options] = nil_klass ? nil_query_object(klass) : klass_for(klass)
14
17
  end
15
18
  end
16
19
 
@@ -39,6 +42,10 @@ module HQ
39
42
  ::GraphQL::Types::Float
40
43
  when :boolean
41
44
  ::GraphQL::Types::Boolean
45
+ when :date
46
+ ::GraphQL::Types::ISO8601Date
47
+ when :datetime
48
+ ::GraphQL::Types::ISO8601DateTime
42
49
  else
43
50
  ::GraphQL::Types::String
44
51
  end
@@ -54,26 +61,20 @@ module HQ
54
61
  class << self
55
62
  private
56
63
 
57
- def nil_query_klass(klass_or_string)
58
- find_klass(klass_or_string, :nil_query_klass)
64
+ def nil_query_object(klass_or_string)
65
+ find_klass(klass_or_string, :nil_query_object)
59
66
  end
60
67
 
61
68
  def klass_for(klass_or_string)
62
- find_klass(klass_or_string, :query_klass)
69
+ find_klass(klass_or_string, :query_object)
63
70
  end
64
71
 
65
72
  def find_klass(klass_or_string, method)
66
73
  klass = klass_or_string.is_a?(String) ? klass_or_string.constantize : klass_or_string
67
- type = find_type(klass)
68
- type ||= find_type(klass.base_class)
69
- type ||= find_type(klass.superclass)
70
-
71
- raise(Error, Error::MISSING_TYPE_MSG % { klass: klass.name }) if !type
72
- type.send(method)
73
- end
74
+ resource = ::HQ::GraphQL.lookup_resource(klass)
74
75
 
75
- def find_type(klass)
76
- ::HQ::GraphQL.resource_lookup(klass) || ::HQ::GraphQL.types.detect { |t| t.model_klass == klass }
76
+ raise(Error, Error::MISSING_TYPE_MSG % { klass: klass.name }) if !resource
77
+ resource.send(method)
77
78
  end
78
79
  end
79
80
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module HQ
4
4
  module GraphQL
5
- VERSION = "2.0.11"
5
+ VERSION = "2.1.4"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hq-graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.11
4
+ version: 2.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danny Jones
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-18 00:00:00.000000000 Z
11
+ date: 2020-06-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -252,13 +252,20 @@ files:
252
252
  - lib/hq/graphql/config.rb
253
253
  - lib/hq/graphql/engine.rb
254
254
  - lib/hq/graphql/enum.rb
255
+ - lib/hq/graphql/enum/sort_by.rb
256
+ - lib/hq/graphql/enum/sort_order.rb
255
257
  - lib/hq/graphql/field.rb
258
+ - lib/hq/graphql/field_extension/association_loader_extension.rb
259
+ - lib/hq/graphql/field_extension/paginated_arguments.rb
260
+ - lib/hq/graphql/field_extension/paginated_loader.rb
256
261
  - lib/hq/graphql/input_object.rb
257
262
  - lib/hq/graphql/inputs.rb
258
263
  - lib/hq/graphql/mutation.rb
259
264
  - lib/hq/graphql/object.rb
265
+ - lib/hq/graphql/object_association.rb
266
+ - lib/hq/graphql/paginated_association_loader.rb
260
267
  - lib/hq/graphql/resource.rb
261
- - lib/hq/graphql/resource/mutation.rb
268
+ - lib/hq/graphql/resource/auto_mutation.rb
262
269
  - lib/hq/graphql/root_mutation.rb
263
270
  - lib/hq/graphql/root_query.rb
264
271
  - lib/hq/graphql/scalars.rb
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module HQ
4
- module GraphQL
5
- module Resource
6
- module Mutation
7
- def self.build(model_name, action:, graphql_name:, require_primary_key: false, nil_klass: false, &block)
8
- Class.new(::HQ::GraphQL::Mutation) do
9
- graphql_name graphql_name
10
-
11
- define_method(:ready?) do |*args|
12
- super(*args) && ::HQ::GraphQL.authorized?(action, model_name, context)
13
- end
14
-
15
- lazy_load do
16
- field :errors, ::HQ::GraphQL::Types::Object, null: false
17
- field :resource, ::HQ::GraphQL::Types[model_name, nil_klass], null: true
18
- end
19
-
20
- instance_eval(&block)
21
-
22
- if require_primary_key
23
- lazy_load do
24
- klass = model_name.constantize
25
- primary_key = klass.primary_key
26
- argument primary_key, ::GraphQL::Types::ID, required: true
27
- end
28
- end
29
-
30
- def errors_from_resource(resource)
31
- resource.errors.to_h.deep_transform_keys { |k| k.to_s.camelize(:lower) }
32
- end
33
- end
34
- end
35
- end
36
- end
37
- end
38
- end