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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +154 -1
  3. data/README.md +42 -2
  4. data/docs/authorization.md +116 -57
  5. data/docs/best-practices.md +16 -3
  6. data/docs/caching.md +161 -0
  7. data/docs/configuration.md +83 -4
  8. data/docs/custom-directives.md +5 -1
  9. data/docs/directives.md +117 -38
  10. data/docs/getting-started.md +84 -0
  11. data/docs/mutations.md +260 -0
  12. data/docs/performance.md +138 -0
  13. data/docs/relationships.md +80 -33
  14. data/docs/resolvers.md +179 -0
  15. data/docs/validation.md +132 -0
  16. data/lib/generators/lighthouse/builder_generator.rb +30 -0
  17. data/lib/generators/lighthouse/class_generator.rb +61 -0
  18. data/lib/generators/lighthouse/install/install_generator.rb +127 -0
  19. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
  20. data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
  21. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
  22. data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
  23. data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
  24. data/lib/generators/lighthouse/mutation_generator.rb +30 -0
  25. data/lib/generators/lighthouse/query_generator.rb +29 -0
  26. data/lib/generators/lighthouse/validator_generator.rb +36 -0
  27. data/lib/lighthouse/cache_control.rb +126 -0
  28. data/lib/lighthouse/caching.rb +68 -0
  29. data/lib/lighthouse/config.rb +85 -19
  30. data/lib/lighthouse/contracts/arg_builder.rb +8 -12
  31. data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
  32. data/lib/lighthouse/contracts/directive.rb +10 -14
  33. data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
  34. data/lib/lighthouse/contracts/field_middleware.rb +7 -11
  35. data/lib/lighthouse/contracts/field_resolver.rb +9 -13
  36. data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
  37. data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
  38. data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
  39. data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
  40. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
  41. data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
  42. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
  43. data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
  44. data/lib/lighthouse/directive_resolvers/base.rb +142 -86
  45. data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
  46. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
  47. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
  48. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
  49. data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
  50. data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
  51. data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
  52. data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
  53. data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
  54. data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
  55. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
  56. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
  57. data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
  58. data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
  59. data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
  60. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
  61. data/lib/lighthouse/directives/arguments/base.rb +23 -31
  62. data/lib/lighthouse/directives/arguments/filters.rb +113 -93
  63. data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
  64. data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
  65. data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
  66. data/lib/lighthouse/directives/registry.rb +210 -141
  67. data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
  68. data/lib/lighthouse/errors.rb +27 -0
  69. data/lib/lighthouse/federation_directives.rb +53 -0
  70. data/lib/lighthouse/field_resolver_stack.rb +56 -0
  71. data/lib/lighthouse/generated_resolvers.rb +82 -0
  72. data/lib/lighthouse/graphql/version.rb +1 -1
  73. data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
  74. data/lib/lighthouse/ide_helper.rb +98 -0
  75. data/lib/lighthouse/namespaces.rb +21 -0
  76. data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
  77. data/lib/lighthouse/nested_mutations/executor.rb +149 -0
  78. data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
  79. data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
  80. data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
  81. data/lib/lighthouse/nested_mutations.rb +210 -0
  82. data/lib/lighthouse/pagination.rb +221 -0
  83. data/lib/lighthouse/persisted_queries.rb +167 -0
  84. data/lib/lighthouse/railtie.rb +26 -0
  85. data/lib/lighthouse/rb_lighthouse.rb +50 -29
  86. data/lib/lighthouse/reference_resolver.rb +13 -15
  87. data/lib/lighthouse/relation_batch.rb +156 -0
  88. data/lib/lighthouse/resolver_dispatch.rb +26 -0
  89. data/lib/lighthouse/schema_cache.rb +120 -0
  90. data/lib/lighthouse/schema_factory.rb +27 -37
  91. data/lib/lighthouse/schema_generator.rb +196 -239
  92. data/lib/lighthouse/schema_implementation.rb +51 -48
  93. data/lib/lighthouse/sdl_loader.rb +17 -20
  94. data/lib/lighthouse/support/arguments.rb +119 -0
  95. data/lib/lighthouse/support/authorization.rb +18 -19
  96. data/lib/lighthouse/support/batching.rb +16 -0
  97. data/lib/lighthouse/support/directive_args.rb +26 -24
  98. data/lib/lighthouse/support/model_resolver.rb +56 -56
  99. data/lib/lighthouse/support/naming.rb +55 -59
  100. data/lib/lighthouse/tasks/lighthouse.rake +33 -0
  101. data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
  102. metadata +51 -63
  103. data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
  104. data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
  105. data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
  106. data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
  107. data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
  108. 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
- directives/relation_directive
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/can_resolver
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
- begin
61
- require_relative rel
62
- rescue LoadError => e
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
- module Lighthouse
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
- Lighthouse::GraphQL::SchemaFactory.build_base_schema(
76
- sdl_folder: sdl_folder,
77
- base_types: base_types,
78
- schema_options: options.merge(schema_class: schema_class)
79
- )
80
- end
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
- def configure(&block)
83
- Lighthouse::GraphQL::Config.configure(&block) if defined?(Lighthouse::GraphQL::Config)
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
- module Lighthouse
4
- class ReferenceResolver
5
- @resolvers = {}
3
+ class Lighthouse::ReferenceResolver
4
+ @resolvers = {}
6
5
 
7
- class << self
8
- def register(typename, &block)
9
- @resolvers[typename.to_s] = block
10
- end
6
+ class << self
7
+ def register(typename, &block)
8
+ @resolvers[typename.to_s] = block
9
+ end
11
10
 
12
- def resolve(typename, reference, context)
13
- resolver = @resolvers[typename.to_s]
14
- return nil unless resolver
11
+ def resolve(typename, reference, context)
12
+ resolver = @resolvers[typename.to_s]
13
+ return nil unless resolver
15
14
 
16
- resolver.call(reference, context)
17
- end
15
+ resolver.call(reference, context)
16
+ end
18
17
 
19
- def clear!
20
- @resolvers.clear
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
- module Lighthouse
4
- module GraphQL
5
- # Public entrypoint for building an executable schema from SDL.
6
- #
7
- # The returned value is a GraphQL-Ruby schema class (from `GraphQL::Schema.from_definition`)
8
- # intended to be subclassed by the application schema class.
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
- def build_base_schema(sdl_folder: nil, sdl: nil, base_types: {}, schema_options: {}, **extra_options)
13
- # Absorb any extra keyword options (e.g. `federation: true`) into the
14
- # schema options bag so callers can pass schema-level flags directly
15
- # without the factory needing to enumerate every one.
16
- schema_options = extra_options.merge(schema_options)
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
- sdl_string =
19
- if sdl
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
- schema_class = schema_options.delete(:schema_class)
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
- schema = Lighthouse::GraphQL::SchemaGenerator.build(
35
- sdl_string,
36
- base_types: base_types,
37
- schema_class: schema_class,
38
- **schema_options
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
- schema
42
- end
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
-