lighthouse-graphql 0.1.1 → 0.2.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 +4 -4
- data/CHANGELOG.md +154 -1
- data/README.md +42 -2
- data/docs/authorization.md +116 -57
- data/docs/best-practices.md +16 -3
- data/docs/caching.md +161 -0
- data/docs/configuration.md +83 -4
- data/docs/custom-directives.md +5 -1
- data/docs/directives.md +117 -38
- data/docs/getting-started.md +84 -0
- data/docs/mutations.md +260 -0
- data/docs/performance.md +138 -0
- data/docs/relationships.md +80 -33
- data/docs/resolvers.md +179 -0
- data/docs/validation.md +132 -0
- data/lib/generators/lighthouse/builder_generator.rb +30 -0
- data/lib/generators/lighthouse/class_generator.rb +61 -0
- data/lib/generators/lighthouse/install/install_generator.rb +127 -0
- data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
- data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
- data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
- data/lib/generators/lighthouse/mutation_generator.rb +30 -0
- data/lib/generators/lighthouse/query_generator.rb +29 -0
- data/lib/generators/lighthouse/validator_generator.rb +36 -0
- data/lib/lighthouse/cache_control.rb +126 -0
- data/lib/lighthouse/caching.rb +68 -0
- data/lib/lighthouse/config.rb +85 -19
- data/lib/lighthouse/contracts/arg_builder.rb +8 -12
- data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
- data/lib/lighthouse/contracts/directive.rb +10 -14
- data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
- data/lib/lighthouse/contracts/field_middleware.rb +7 -11
- data/lib/lighthouse/contracts/field_resolver.rb +9 -13
- data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
- data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
- data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
- data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
- data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
- data/lib/lighthouse/directive_resolvers/base.rb +142 -86
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
- data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
- data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
- data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
- data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
- data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
- data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
- data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
- data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
- data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
- data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
- data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
- data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
- data/lib/lighthouse/directives/arguments/base.rb +23 -31
- data/lib/lighthouse/directives/arguments/filters.rb +113 -93
- data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
- data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
- data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
- data/lib/lighthouse/directives/registry.rb +210 -141
- data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
- data/lib/lighthouse/errors.rb +27 -0
- data/lib/lighthouse/federation_directives.rb +53 -0
- data/lib/lighthouse/field_resolver_stack.rb +56 -0
- data/lib/lighthouse/generated_resolvers.rb +82 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
- data/lib/lighthouse/ide_helper.rb +98 -0
- data/lib/lighthouse/namespaces.rb +21 -0
- data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
- data/lib/lighthouse/nested_mutations/executor.rb +149 -0
- data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
- data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
- data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
- data/lib/lighthouse/nested_mutations.rb +210 -0
- data/lib/lighthouse/pagination.rb +221 -0
- data/lib/lighthouse/persisted_queries.rb +167 -0
- data/lib/lighthouse/railtie.rb +26 -0
- data/lib/lighthouse/rb_lighthouse.rb +50 -29
- data/lib/lighthouse/reference_resolver.rb +13 -15
- data/lib/lighthouse/relation_batch.rb +156 -0
- data/lib/lighthouse/resolver_dispatch.rb +26 -0
- data/lib/lighthouse/schema_cache.rb +120 -0
- data/lib/lighthouse/schema_factory.rb +27 -37
- data/lib/lighthouse/schema_generator.rb +196 -239
- data/lib/lighthouse/schema_implementation.rb +51 -48
- data/lib/lighthouse/sdl_loader.rb +17 -20
- data/lib/lighthouse/support/arguments.rb +119 -0
- data/lib/lighthouse/support/authorization.rb +18 -19
- data/lib/lighthouse/support/batching.rb +16 -0
- data/lib/lighthouse/support/directive_args.rb +26 -24
- data/lib/lighthouse/support/model_resolver.rb +56 -56
- data/lib/lighthouse/support/naming.rb +55 -59
- data/lib/lighthouse/tasks/lighthouse.rake +33 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
- metadata +51 -63
- data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
- data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
- data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
- data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
- data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
- data/lib/lighthouse/directives/relation_directive.rb +0 -332
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# ActiveSupport core extensions the library relies on (underscore, pluralize,
|
|
4
|
-
# blank?/present?, deep_symbolize_keys). In a Rails host these are already
|
|
4
|
+
# blank?/present?, deep_symbolize_keys, index_by/pluck/exclude?). In a Rails host these are already
|
|
5
5
|
# loaded; requiring them here keeps the gem usable standalone.
|
|
6
6
|
require 'active_support'
|
|
7
7
|
require 'active_support/core_ext/string/inflections'
|
|
8
8
|
require 'active_support/core_ext/object/blank'
|
|
9
9
|
require 'active_support/core_ext/hash/keys'
|
|
10
|
+
require 'active_support/core_ext/enumerable'
|
|
11
|
+
require 'active_support/core_ext/string/exclude'
|
|
12
|
+
require 'set'
|
|
13
|
+
require 'graphql'
|
|
14
|
+
|
|
15
|
+
require_relative 'namespaces'
|
|
10
16
|
|
|
11
17
|
## Load internal dependencies individually so a single missing file
|
|
12
18
|
## doesn't prevent the rest from loading. Failures are logged but not
|
|
13
19
|
## raised so callers can require specific files if needed.
|
|
14
20
|
%w[
|
|
15
21
|
config
|
|
22
|
+
errors
|
|
23
|
+
graphql_ruby_patches
|
|
16
24
|
reference_resolver
|
|
17
25
|
support/naming
|
|
18
26
|
support/directive_args
|
|
19
27
|
support/model_resolver
|
|
20
28
|
support/authorization
|
|
29
|
+
support/arguments
|
|
30
|
+
support/batching
|
|
21
31
|
where_conditions/operator_map
|
|
22
32
|
contracts/directive
|
|
23
33
|
contracts/field_resolver
|
|
@@ -26,22 +36,29 @@ require 'active_support/core_ext/hash/keys'
|
|
|
26
36
|
contracts/arg_manipulator
|
|
27
37
|
contracts/arg_builder
|
|
28
38
|
sdl_loader
|
|
39
|
+
generated_resolvers
|
|
40
|
+
federation_directives
|
|
41
|
+
field_resolver_stack
|
|
29
42
|
schema_generator
|
|
43
|
+
schema_cache
|
|
44
|
+
persisted_queries
|
|
30
45
|
schema_factory
|
|
31
46
|
schema_implementation
|
|
32
|
-
|
|
47
|
+
resolver_dispatch
|
|
48
|
+
caching
|
|
49
|
+
cache_control
|
|
50
|
+
pagination
|
|
51
|
+
relation_batch
|
|
52
|
+
nested_mutations
|
|
53
|
+
directives/pagination_manipulator
|
|
33
54
|
directives/where_conditions_directive
|
|
34
|
-
directive_resolvers/has_many_resolver
|
|
35
55
|
directive_resolvers/aggregate_resolver
|
|
36
56
|
directive_resolvers/field_resolver
|
|
37
57
|
directive_resolvers/where_conditions_applier
|
|
38
|
-
directive_resolvers/belongs_to_resolver
|
|
39
58
|
directive_resolvers/all_resolver
|
|
40
59
|
directive_resolvers/find_resolver
|
|
41
60
|
directive_resolvers/paginate_resolver
|
|
42
61
|
directive_resolvers/auth_resolver
|
|
43
|
-
directive_resolvers/belongs_to_many_resolver
|
|
44
|
-
directive_resolvers/has_one_resolver
|
|
45
62
|
directive_resolvers/first_resolver
|
|
46
63
|
directive_resolvers/count_resolver
|
|
47
64
|
directive_resolvers/rename_resolver
|
|
@@ -49,40 +66,44 @@ require 'active_support/core_ext/hash/keys'
|
|
|
49
66
|
directive_resolvers/base
|
|
50
67
|
directive_resolvers/resolution
|
|
51
68
|
directive_resolvers/guard_resolver
|
|
52
|
-
directive_resolvers/
|
|
69
|
+
directive_resolvers/association_resolvers
|
|
70
|
+
directive_resolvers/can_resolvers
|
|
71
|
+
directive_resolvers/convention_resolver
|
|
72
|
+
directive_resolvers/model_mutation_resolver
|
|
73
|
+
directive_resolvers/cache_resolvers
|
|
74
|
+
directive_resolvers/validator_resolver
|
|
53
75
|
directives/arguments/base
|
|
54
76
|
directives/arguments/filters
|
|
55
77
|
directives/arguments/order_by
|
|
56
78
|
directives/registry
|
|
57
79
|
dataloader_sources/association_loader
|
|
58
80
|
dataloader_sources/model_loader
|
|
81
|
+
dataloader_sources/relation_page_loader
|
|
82
|
+
dataloader_sources/relation_count_loader
|
|
83
|
+
ide_helper
|
|
59
84
|
].each do |rel|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
warn("Lighthouse: could not require #{rel}: #{e.message}")
|
|
64
|
-
end
|
|
85
|
+
require_relative rel
|
|
86
|
+
rescue LoadError => e
|
|
87
|
+
warn("Lighthouse: could not require #{rel}: #{e.message}")
|
|
65
88
|
end
|
|
66
89
|
|
|
67
|
-
|
|
68
|
-
module GraphQL
|
|
69
|
-
module RbLightHouse
|
|
70
|
-
class << self
|
|
71
|
-
def get_schema(sdl_folder: nil, options: {})
|
|
72
|
-
base_types = options.delete(:base_types) || {}
|
|
73
|
-
schema_class = options.delete(:base_schema)
|
|
90
|
+
require_relative 'railtie' if defined?(Rails::Railtie)
|
|
74
91
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
module Lighthouse::GraphQL::RbLightHouse
|
|
93
|
+
class << self
|
|
94
|
+
def get_schema(sdl_folder: nil, options: {})
|
|
95
|
+
base_types = options.delete(:base_types) || {}
|
|
96
|
+
schema_class = options.delete(:base_schema)
|
|
97
|
+
|
|
98
|
+
Lighthouse::GraphQL::SchemaFactory.build_base_schema(
|
|
99
|
+
sdl_folder: sdl_folder,
|
|
100
|
+
base_types: base_types,
|
|
101
|
+
schema_options: options.merge(schema_class: schema_class)
|
|
102
|
+
)
|
|
103
|
+
end
|
|
81
104
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
end
|
|
85
|
-
end
|
|
105
|
+
def configure(&block)
|
|
106
|
+
Lighthouse::GraphQL::Config.configure(&block) if defined?(Lighthouse::GraphQL::Config)
|
|
86
107
|
end
|
|
87
108
|
end
|
|
88
109
|
end
|
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
@resolvers = {}
|
|
3
|
+
class Lighthouse::ReferenceResolver
|
|
4
|
+
@resolvers = {}
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
class << self
|
|
7
|
+
def register(typename, &block)
|
|
8
|
+
@resolvers[typename.to_s] = block
|
|
9
|
+
end
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
def resolve(typename, reference, context)
|
|
12
|
+
resolver = @resolvers[typename.to_s]
|
|
13
|
+
return nil unless resolver
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
resolver.call(reference, context)
|
|
16
|
+
end
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
end
|
|
18
|
+
def clear!
|
|
19
|
+
@resolvers.clear
|
|
22
20
|
end
|
|
23
21
|
end
|
|
24
22
|
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The pages of one paginated relation for many parents at once — what PHP
|
|
4
|
+
# Lighthouse's PaginatedModelsLoader does with a UNION ALL per parent and
|
|
5
|
+
# `loadCount`. Here a window function numbers each parent's rows, so the pages
|
|
6
|
+
# of every parent come from one query, and one GROUP BY counts them all:
|
|
7
|
+
#
|
|
8
|
+
# SELECT * FROM (
|
|
9
|
+
# SELECT comments.*, ROW_NUMBER() OVER (PARTITION BY comments.author_id ORDER BY ...) AS lighthouse_row,
|
|
10
|
+
# comments.author_id AS lighthouse_owner
|
|
11
|
+
# FROM comments WHERE comments.author_id IN (1, 2, 3) AND <the field's scopes and filters>
|
|
12
|
+
# ) comments WHERE lighthouse_row > 0 AND lighthouse_row <= 10
|
|
13
|
+
#
|
|
14
|
+
# SELECT COUNT(*), comments.author_id FROM comments WHERE ... GROUP BY comments.author_id
|
|
15
|
+
#
|
|
16
|
+
# A parent's association scope links it through one column on the table
|
|
17
|
+
# nearest to it (comments.author_id; taggings.post_id for a :through), which
|
|
18
|
+
# ActiveRecord's reflection names. The batch swaps that link for an IN over
|
|
19
|
+
# every parent and partitions by the same column. The field's scopes and
|
|
20
|
+
# filters (`decorator.decorate_relation`) apply on top, as for one parent.
|
|
21
|
+
#
|
|
22
|
+
# A batch is equal to another for the same field, parent class and arguments,
|
|
23
|
+
# so the Dataloader collects every parent of a level into one.
|
|
24
|
+
class Lighthouse::GraphQL::RelationBatch
|
|
25
|
+
ROW = 'lighthouse_row'
|
|
26
|
+
OWNER = 'lighthouse_owner'
|
|
27
|
+
|
|
28
|
+
attr_reader :association_name
|
|
29
|
+
|
|
30
|
+
# The batch `owner`'s page belongs to, or nil when this relation cannot be
|
|
31
|
+
# batched (the database has no window functions, a composite key, a scope
|
|
32
|
+
# that reads the owner, DISTINCT, ...): each parent then loads its own page.
|
|
33
|
+
def self.for(owner, association_name, args, decorator, context)
|
|
34
|
+
return nil unless owner.respond_to?(:association) && owner.class.respond_to?(:reflect_on_association)
|
|
35
|
+
return nil unless owner.class.reflect_on_association(association_name.to_sym)&.collection?
|
|
36
|
+
|
|
37
|
+
batch = new(owner.class, association_name.to_s, args, decorator, context)
|
|
38
|
+
batch if batch.batchable?(owner)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.window_functions?(connection)
|
|
42
|
+
version = connection.database_version
|
|
43
|
+
case connection.adapter_name
|
|
44
|
+
when /postg/i then true
|
|
45
|
+
when /sqlite/i then version >= '3.25'
|
|
46
|
+
when /mysql|trilogy/i then version >= (connection.respond_to?(:mariadb?) && connection.mariadb? ? '10.2' : '8.0')
|
|
47
|
+
else false
|
|
48
|
+
end
|
|
49
|
+
rescue StandardError
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def initialize(owner_class, association_name, args, decorator, context)
|
|
54
|
+
@owner_class = owner_class
|
|
55
|
+
@association_name = association_name
|
|
56
|
+
@args = args
|
|
57
|
+
@decorator = decorator
|
|
58
|
+
@context = context
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def batchable?(owner)
|
|
62
|
+
linked_by_one_column?(owner) && self.class.window_functions?(owner.class.connection) &&
|
|
63
|
+
owner_independent?(owner) && rankable?(relation([owner]))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# { owner key => its records in rows offset+1..offset+limit }
|
|
67
|
+
def pages(owners, offset, limit)
|
|
68
|
+
relation = relation(owners)
|
|
69
|
+
ranked = relation.unscope(:order).select(*selection(relation), "#{row_number(relation)} AS #{ROW}", "#{partition} AS #{OWNER}")
|
|
70
|
+
rows = relation.klass.unscoped.from(ranked, relation.klass.table_name)
|
|
71
|
+
.where("#{ROW} > ? AND #{ROW} <= ?", offset, offset + limit)
|
|
72
|
+
.order(Arel.sql("#{OWNER}, #{ROW}"))
|
|
73
|
+
preloads = relation.preload_values + relation.includes_values
|
|
74
|
+
rows = rows.preload(*preloads) if preloads.any?
|
|
75
|
+
rows.to_a.group_by { |record| record[OWNER].to_s }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# { owner key => number of related records }
|
|
79
|
+
def counts(owners)
|
|
80
|
+
relation(owners).unscope(:order, :select).group(Arel.sql(partition)).count.transform_keys(&:to_s)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def owner_key(owner)
|
|
84
|
+
owner[@owner_column].to_s
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def eql?(other)
|
|
88
|
+
other.is_a?(self.class) && identity == other.identity
|
|
89
|
+
end
|
|
90
|
+
alias == eql?
|
|
91
|
+
|
|
92
|
+
def hash
|
|
93
|
+
identity.hash
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
protected
|
|
97
|
+
|
|
98
|
+
def identity
|
|
99
|
+
@identity ||= [@owner_class, @association_name, @decorator, Lighthouse::Support::Arguments.plain(@args)]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
# The column on the table nearest the owner that holds the owner's key —
|
|
105
|
+
# where ActiveRecord's AssociationScope puts the owner's condition.
|
|
106
|
+
def linked_by_one_column?(owner)
|
|
107
|
+
link = owner.association(@association_name.to_sym).reflection.chain.last
|
|
108
|
+
return false unless link.respond_to?(:join_primary_key)
|
|
109
|
+
|
|
110
|
+
@link_table = link.klass.table_name
|
|
111
|
+
@link_column = link.join_primary_key
|
|
112
|
+
@owner_column = link.join_foreign_key
|
|
113
|
+
[@link_column, @owner_column].none?(Array)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The owner's condition can be swapped for an IN, and no scope reads the owner.
|
|
117
|
+
def owner_independent?(owner)
|
|
118
|
+
association = owner.association(@association_name.to_sym)
|
|
119
|
+
association.reflection.chain.none? { |reflection| reflection.scope&.arity&.positive? } &&
|
|
120
|
+
unlinked(association.scope).where_clause != association.scope.where_clause
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Rows can be numbered in a stable order, and DISTINCT does not fold them.
|
|
124
|
+
def rankable?(relation)
|
|
125
|
+
!relation.distinct_value && (relation.arel.orders.any? || relation.klass.primary_key.is_a?(String))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def relation(owners)
|
|
129
|
+
scope = unlinked(owners.first.association(@association_name.to_sym).scope)
|
|
130
|
+
keys = owners.pluck(@owner_column).uniq
|
|
131
|
+
@decorator.decorate_relation(scope.where(@link_table => { @link_column => keys }), @args, @context)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def unlinked(scope)
|
|
135
|
+
scope.unscope(where: { @link_table => @link_column })
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def partition
|
|
139
|
+
connection = @owner_class.connection
|
|
140
|
+
"#{connection.quote_table_name(@link_table)}.#{connection.quote_column_name(@link_column)}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Numbers each parent's rows in the order the field asks for, else by primary key.
|
|
144
|
+
def row_number(relation)
|
|
145
|
+
orders = relation.arel.orders.map { |node| node.respond_to?(:to_sql) ? node.to_sql : node.to_s }
|
|
146
|
+
if orders.empty?
|
|
147
|
+
connection = relation.klass.connection
|
|
148
|
+
orders = ["#{connection.quote_table_name(relation.klass.table_name)}.#{connection.quote_column_name(relation.klass.primary_key)}"]
|
|
149
|
+
end
|
|
150
|
+
"ROW_NUMBER() OVER (PARTITION BY #{partition} ORDER BY #{orders.join(', ')})"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def selection(relation)
|
|
154
|
+
relation.select_values.presence || [relation.klass.arel_table[Arel.star]]
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Calls the class a field resolves to. Classes in the queries and mutations
|
|
4
|
+
# namespaces follow graphql-ruby's contract — GraphQL::Schema::Resolver and
|
|
5
|
+
# GraphQL::Schema::Mutation: `resolve(**args)` with `object` and `context`,
|
|
6
|
+
# plus `ready?` / `authorized?` — exactly as in a graphql-ruby schema.
|
|
7
|
+
# @field also accepts a plain callable, `call(object, args, context)`.
|
|
8
|
+
module Lighthouse::GraphQL::ResolverDispatch
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def graphql_resolver?(klass)
|
|
12
|
+
klass.is_a?(Class) && klass < ::GraphQL::Schema::Resolver
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(klass, field_def, obj, args, ctx)
|
|
16
|
+
if graphql_resolver?(klass)
|
|
17
|
+
kwargs = Lighthouse::Support::Arguments.prepare(field_def, args, ctx)
|
|
18
|
+
klass.new(object: obj, context: ctx, field: field_def).resolve_with_support(**kwargs)
|
|
19
|
+
elsif klass.respond_to?(:call)
|
|
20
|
+
klass.call(obj, args, ctx)
|
|
21
|
+
else
|
|
22
|
+
instance = klass.new
|
|
23
|
+
instance.respond_to?(:call) ? instance.call(obj, args, ctx) : instance.resolve(obj, args, ctx)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
|
|
7
|
+
# Opt-in cache of the schema document Lighthouse builds from your SDL —
|
|
8
|
+
# PHP Lighthouse's schema cache, done the Bootsnap way: a content-addressed
|
|
9
|
+
# file in tmp/cache, no deploy step to remember and nothing to clear by hand.
|
|
10
|
+
#
|
|
11
|
+
# Building a schema parses the SDL, injects directive definitions and runs
|
|
12
|
+
# the manipulators (pagination types, whereConditions inputs, ...), parses
|
|
13
|
+
# the result and builds the executable schema. The cache stores the parsed,
|
|
14
|
+
# manipulated document, so a boot skips everything up to that last step —
|
|
15
|
+
# roughly 60% of the build time.
|
|
16
|
+
#
|
|
17
|
+
# The key covers everything that shapes that document: the SDL, the
|
|
18
|
+
# Lighthouse / graphql-ruby / Ruby versions, the registered directive and
|
|
19
|
+
# type definitions, the source of the manipulators, and the configuration
|
|
20
|
+
# they read. Change any of them and the next boot rebuilds — a stale schema
|
|
21
|
+
# cannot be served. Enable with `Lighthouse.configure { |c| c.schema_cache = true }`;
|
|
22
|
+
# `bin/rails lighthouse:cache` warms it at deploy (e.g. in a Docker build).
|
|
23
|
+
module Lighthouse::GraphQL::SchemaCache
|
|
24
|
+
PREFIX = 'schema-'
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def enabled?
|
|
29
|
+
Lighthouse::Config.schema_cache
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def directory
|
|
33
|
+
Lighthouse::Config.schema_cache_path ||
|
|
34
|
+
if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
|
|
35
|
+
::Rails.root.join('tmp/cache/lighthouse').to_s
|
|
36
|
+
else
|
|
37
|
+
File.join(Dir.tmpdir, 'lighthouse-graphql')
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The parsed document for `sdl` after manipulation. `build` turns the SDL
|
|
42
|
+
# into the manipulated SDL string; it only runs on a cache miss.
|
|
43
|
+
# `force:` caches even when the cache is off (warm!).
|
|
44
|
+
def document(sdl, force: false, &build)
|
|
45
|
+
return parse(yield(sdl)) unless force || enabled?
|
|
46
|
+
|
|
47
|
+
path = File.join(directory.to_s, "#{PREFIX}#{key(sdl)}.bin")
|
|
48
|
+
cached = read(path)
|
|
49
|
+
return cached if cached
|
|
50
|
+
|
|
51
|
+
parse(yield(sdl)).tap { |document| write(path, document) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def key(sdl)
|
|
55
|
+
Digest::SHA256.hexdigest(
|
|
56
|
+
[Lighthouse::GraphQL::VERSION, ::GraphQL::VERSION, RUBY_VERSION, sdl.to_s, fingerprint].join("\0")
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Everything besides the SDL that changes the manipulated document.
|
|
61
|
+
def fingerprint
|
|
62
|
+
registry = Lighthouse::DirectiveRegistry.default
|
|
63
|
+
manipulators = registry.sdl_manipulators.map { |manipulator| [manipulator.to_s, source_digest(manipulator)] }
|
|
64
|
+
[
|
|
65
|
+
registry.directive_definitions.sort,
|
|
66
|
+
registry.type_definitions.sort,
|
|
67
|
+
manipulators,
|
|
68
|
+
Lighthouse::Config.pagination_default_count
|
|
69
|
+
].inspect
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Build the schema document now (from Config.sdl_folder, unless given)
|
|
73
|
+
# and return the cache file's path.
|
|
74
|
+
def warm!(sdl = nil)
|
|
75
|
+
sdl ||= Lighthouse::GraphQL::SDLLoader.load_from_folder(Lighthouse::Config.sdl_folder.to_s)
|
|
76
|
+
document(sdl, force: true) { |source| Lighthouse::GraphQL::SchemaGenerator.manipulate(source) }
|
|
77
|
+
File.join(directory.to_s, "#{PREFIX}#{key(sdl)}.bin")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def clear!
|
|
81
|
+
Dir[File.join(directory.to_s, "#{PREFIX}*.bin")].each { |file| File.delete(file) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# graphql-ruby's parser for schema definitions, as from_definition uses.
|
|
85
|
+
def parse(sdl)
|
|
86
|
+
parser = ::GraphQL.default_parser
|
|
87
|
+
parser = parser::SchemaParser if parser.const_defined?(:SchemaParser, false)
|
|
88
|
+
parser.parse(sdl)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# A missing, truncated or incompatible file is a miss, never an error.
|
|
92
|
+
def read(path)
|
|
93
|
+
return nil unless File.exist?(path)
|
|
94
|
+
|
|
95
|
+
# Our own tmp/cache file, as Bootsnap reads its own; never anything a client sent.
|
|
96
|
+
document = Marshal.load(File.binread(path)) # rubocop:disable Security/MarshalLoad
|
|
97
|
+
document if document.is_a?(::GraphQL::Language::Nodes::Document)
|
|
98
|
+
rescue StandardError
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Write atomically (a concurrent boot never reads half a file), then drop
|
|
103
|
+
# the entries of earlier keys.
|
|
104
|
+
def write(path, document)
|
|
105
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
106
|
+
temporary = "#{path}.#{Process.pid}.tmp"
|
|
107
|
+
File.binwrite(temporary, Marshal.dump(document))
|
|
108
|
+
File.rename(temporary, path)
|
|
109
|
+
Dir[File.join(File.dirname(path), "#{PREFIX}*.bin")].each { |file| File.delete(file) unless file == path }
|
|
110
|
+
rescue SystemCallError => e
|
|
111
|
+
Lighthouse::Config.handle_error(e)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def source_digest(manipulator)
|
|
115
|
+
file, = manipulator.method(:manipulate).source_location
|
|
116
|
+
file && File.exist?(file) ? Digest::SHA256.file(file).hexdigest : nil
|
|
117
|
+
rescue NameError
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -1,46 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
module SchemaFactory
|
|
10
|
-
module_function
|
|
3
|
+
# Public entrypoint for building an executable schema from SDL.
|
|
4
|
+
#
|
|
5
|
+
# The returned value is a GraphQL-Ruby schema class (from `GraphQL::Schema.from_definition`)
|
|
6
|
+
# intended to be subclassed by the application schema class.
|
|
7
|
+
module Lighthouse::GraphQL::SchemaFactory
|
|
8
|
+
module_function
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
def build_base_schema(sdl_folder: nil, sdl: nil, base_types: {}, schema_options: {}, **extra_options)
|
|
11
|
+
# Absorb any extra keyword options (e.g. `federation: true`) into the
|
|
12
|
+
# schema options bag so callers can pass schema-level flags directly
|
|
13
|
+
# without the factory needing to enumerate every one.
|
|
14
|
+
schema_options = extra_options.merge(schema_options)
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
sdl
|
|
21
|
-
else
|
|
22
|
-
folder = sdl_folder || (Lighthouse::GraphQL::Config.sdl_folder if defined?(Lighthouse::GraphQL::Config))
|
|
23
|
-
if folder && Dir.exist?(folder)
|
|
24
|
-
Lighthouse::GraphQL::SDLLoader.load_from_folder(folder)
|
|
25
|
-
else
|
|
26
|
-
# fallback to apps/chatwoot/schema.graphql
|
|
27
|
-
path = File.expand_path('../../schema.graphql', __dir__)
|
|
28
|
-
File.read(path)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
16
|
+
sdl_string = sdl || load_sdl(sdl_folder)
|
|
17
|
+
schema_class = schema_options.delete(:schema_class)
|
|
31
18
|
|
|
32
|
-
|
|
19
|
+
Lighthouse::GraphQL::SchemaGenerator.build(
|
|
20
|
+
sdl_string,
|
|
21
|
+
base_types: base_types,
|
|
22
|
+
schema_class: schema_class,
|
|
23
|
+
**schema_options
|
|
24
|
+
)
|
|
25
|
+
end
|
|
33
26
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
)
|
|
27
|
+
# The SDL in `sdl_folder` (default: Config.sdl_folder), or the bundled
|
|
28
|
+
# schema.graphql when there is no such folder.
|
|
29
|
+
def load_sdl(sdl_folder)
|
|
30
|
+
folder = sdl_folder || (Lighthouse::GraphQL::Config.sdl_folder if defined?(Lighthouse::GraphQL::Config))
|
|
31
|
+
return Lighthouse::GraphQL::SDLLoader.load_from_folder(folder) if folder && Dir.exist?(folder)
|
|
40
32
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
end
|
|
33
|
+
# fallback to apps/chatwoot/schema.graphql
|
|
34
|
+
File.read(File.expand_path('../../schema.graphql', __dir__))
|
|
44
35
|
end
|
|
45
36
|
end
|
|
46
|
-
|