lighthouse-graphql 0.5.0 → 0.7.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +212 -1
  3. data/README.md +38 -22
  4. data/docs/best-practices.md +6 -4
  5. data/docs/caching.md +4 -3
  6. data/docs/configuration.md +20 -26
  7. data/docs/custom-directives.md +2 -1
  8. data/docs/directives.md +79 -14
  9. data/docs/federation.md +1 -1
  10. data/docs/filtering-and-ordering.md +330 -30
  11. data/docs/generators.md +42 -40
  12. data/docs/getting-started.md +128 -58
  13. data/docs/motivation.md +3 -2
  14. data/docs/performance.md +5 -4
  15. data/docs/relationships.md +7 -5
  16. data/docs/relay.md +63 -0
  17. data/docs/resolvers.md +92 -39
  18. data/docs/schema-organisation.md +177 -0
  19. data/docs/security.md +64 -16
  20. data/docs/subscriptions.md +16 -9
  21. data/docs/types.md +16 -17
  22. data/docs/uploads.md +41 -14
  23. data/lib/generators/lighthouse/base_classes.rb +9 -14
  24. data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
  25. data/lib/generators/lighthouse/install/install_generator.rb +125 -99
  26. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +2 -1
  27. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +14 -13
  28. data/lib/lighthouse/ast_builder.rb +124 -0
  29. data/lib/lighthouse/caching.rb +9 -3
  30. data/lib/lighthouse/config.rb +27 -25
  31. data/lib/lighthouse/contracts/arg_manipulator.rb +9 -5
  32. data/lib/lighthouse/contracts/field_builder.rb +13 -0
  33. data/lib/lighthouse/controller.rb +28 -5
  34. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +1 -2
  35. data/lib/lighthouse/directive_resolvers/all_resolver.rb +0 -2
  36. data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +18 -18
  37. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +9 -6
  38. data/lib/lighthouse/directive_resolvers/base.rb +6 -37
  39. data/lib/lighthouse/directive_resolvers/builder_applier.rb +21 -6
  40. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
  41. data/lib/lighthouse/directive_resolvers/count_resolver.rb +0 -1
  42. data/lib/lighthouse/directive_resolvers/first_resolver.rb +0 -2
  43. data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +3 -1
  44. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +2 -3
  45. data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +85 -26
  46. data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +12 -6
  47. data/lib/lighthouse/directives/arguments/more_filters.rb +1 -1
  48. data/lib/lighthouse/directives/arguments/order_by.rb +190 -26
  49. data/lib/lighthouse/directives/arguments/where_conditions.rb +25 -0
  50. data/lib/lighthouse/directives/arguments/where_conditions_base.rb +84 -0
  51. data/lib/lighthouse/directives/arguments/where_has_conditions.rb +56 -0
  52. data/lib/lighthouse/directives/definitions.rb +0 -1
  53. data/lib/lighthouse/directives/generated_types.rb +103 -0
  54. data/lib/lighthouse/directives/registry.rb +39 -10
  55. data/lib/lighthouse/document_ast.rb +111 -0
  56. data/lib/lighthouse/field_resolver_stack.rb +9 -6
  57. data/lib/lighthouse/global_id.rb +29 -3
  58. data/lib/lighthouse/graphql/version.rb +1 -1
  59. data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
  60. data/lib/lighthouse/ide_helper.rb +80 -23
  61. data/lib/lighthouse/namespaces.rb +1 -0
  62. data/lib/lighthouse/order_by/types.rb +137 -0
  63. data/lib/lighthouse/railtie.rb +9 -2
  64. data/lib/lighthouse/rb_lighthouse.rb +16 -5
  65. data/lib/lighthouse/resolve_info.rb +5 -5
  66. data/lib/lighthouse/resolver_bases.rb +18 -4
  67. data/lib/lighthouse/resolver_dispatch.rb +16 -1
  68. data/lib/lighthouse/root_types.rb +117 -0
  69. data/lib/lighthouse/schema_cache.rb +8 -4
  70. data/lib/lighthouse/schema_factory.rb +6 -9
  71. data/lib/lighthouse/schema_generator.rb +8 -18
  72. data/lib/lighthouse/schema_stitcher.rb +64 -0
  73. data/lib/lighthouse/support/naming.rb +0 -12
  74. data/lib/lighthouse/upload.rb +4 -2
  75. data/lib/lighthouse/upload_store.rb +39 -14
  76. data/lib/lighthouse/where_conditions/columns.rb +36 -0
  77. data/lib/lighthouse/where_conditions/handler.rb +152 -0
  78. data/lib/lighthouse/where_conditions/json_column.rb +99 -0
  79. data/lib/lighthouse/where_conditions/operator_map.rb +1 -4
  80. data/lib/lighthouse/where_conditions/types.rb +110 -0
  81. data/sig/lighthouse/directives.rbs +16 -3
  82. data/sig/lighthouse/graphql.rbs +48 -4
  83. data/sig/lighthouse.rbs +8 -12
  84. metadata +17 -7
  85. data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -29
  86. data/lib/generators/lighthouse/mutation_generator.rb +0 -31
  87. data/lib/generators/lighthouse/query_generator.rb +0 -30
  88. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +0 -170
  89. data/lib/lighthouse/directives/where_conditions_directive.rb +0 -244
  90. data/lib/lighthouse/sdl_loader.rb +0 -22
@@ -13,7 +13,13 @@ require 'fileutils'
13
13
  # ...), which exist in the executable schema but in no .graphql file.
14
14
  #
15
15
  # Point your IDE's graphql-config at both files next to your own SDL. Run it
16
- # with `bin/rails lighthouse:ide_helper`.
16
+ # with `bin/rails lighthouse:ide_helper`; in development, an app with a
17
+ # graphql.config.yml also gets them rewritten on boot and on every reload
18
+ # (Config.ide_helper).
19
+ #
20
+ # The IDE reads the three together as one schema, so no type may be in two of
21
+ # them: graphql-js (the VS Code and JetBrains plugins) rejects a type defined
22
+ # twice, even where graphql-ruby keeps the last definition.
17
23
  module Lighthouse::IdeHelper
18
24
  DIRECTIVES_FILE = 'schema-directives.graphql'
19
25
  PROGRAMMATIC_TYPES_FILE = 'programmatic-types.graphql'
@@ -27,23 +33,66 @@ module Lighthouse::IdeHelper
27
33
  ::GraphQL::Language::Nodes::ScalarTypeDefinition
28
34
  ].freeze
29
35
 
30
- PLACEHOLDER_SCALAR = %("Placeholder for an argument type Lighthouse generates, e.g. with @whereConditions."\nscalar _)
36
+ PLACEHOLDER_SCALAR = %("Placeholder for an argument type Lighthouse generates, e.g. with @whereConditions or @orderBy."\nscalar _)
31
37
 
32
38
  module_function
33
39
 
34
- # Writes both files into `output_dir` and returns their paths.
35
- def write(output_dir:, sdl_folder: Lighthouse::Config.sdl_folder, registry: Lighthouse::DirectiveRegistry.default)
36
- sdl = sdl_folder && Dir.exist?(sdl_folder.to_s) ? Lighthouse::GraphQL::SDLLoader.load_from_folder(sdl_folder.to_s) : ''
40
+ # Writes both files into `output_dir` and returns their paths. A file whose
41
+ # content has not changed is left alone, so the IDE does not reload for nothing.
42
+ #
43
+ # A schema that does not build (a type defined twice, half-written, ...)
44
+ # still gets its directives, and keeps the programmatic types it had, or
45
+ # gets the `_` placeholder alone: the IDE never loses a type it was given.
46
+ def write(output_dir:, schema_path: Lighthouse::Config.schema_path, registry: Lighthouse::DirectiveRegistry.default)
47
+ sdl = ide_sdl(schema_path)
48
+ directives = directives_sdl(sdl, registry: registry)
37
49
 
38
50
  FileUtils.mkdir_p(output_dir.to_s)
39
- {
40
- DIRECTIVES_FILE => directives_sdl(sdl, registry: registry),
41
- PROGRAMMATIC_TYPES_FILE => programmatic_types_sdl(sdl, registry: registry)
42
- }.map do |name, content|
43
- path = File.join(output_dir.to_s, name)
44
- File.write(path, content)
45
- path
51
+ directives_path = write_file(output_dir, DIRECTIVES_FILE, directives)
52
+ types_path = File.join(output_dir.to_s, PROGRAMMATIC_TYPES_FILE)
53
+ begin
54
+ write_file(output_dir, PROGRAMMATIC_TYPES_FILE, programmatic_types_sdl(sdl, registry: registry, directives: directives))
55
+ rescue StandardError => e
56
+ warn("Lighthouse: #{PROGRAMMATIC_TYPES_FILE} kept as it was, the schema does not build: #{e.message}")
57
+ write_file(output_dir, PROGRAMMATIC_TYPES_FILE, placeholder_only(sdl)) unless File.exist?(types_path)
46
58
  end
59
+ [directives_path, types_path]
60
+ end
61
+
62
+ # The schema as the IDE reads it: the entrypoint and its imports, or, when
63
+ # they do not load, every .graphql file under the entrypoint's folder.
64
+ def ide_sdl(schema_path)
65
+ return '' unless schema_path
66
+
67
+ return Lighthouse::GraphQL::SchemaFactory.load_sdl(schema_path) if File.file?(schema_path.to_s)
68
+
69
+ raise Lighthouse::DefinitionError, "no schema at #{schema_path}"
70
+ rescue Lighthouse::DefinitionError
71
+ Dir[File.join(File.dirname(schema_path.to_s), '**', '*.graphql')].map { |file| File.read(file) }.join("\n")
72
+ end
73
+
74
+ def write_file(output_dir, name, content)
75
+ path = File.join(output_dir.to_s, name)
76
+ File.write(path, content) unless File.exist?(path) && File.read(path) == content
77
+ path
78
+ end
79
+
80
+ def placeholder_only(sdl)
81
+ declared = sdl.to_s.match?(/^\s*scalar\s+_\b/) ? Set['_'] : Set.new
82
+ render('The schema does not build: only the `_` placeholder is declared until it does.',
83
+ undeclared_placeholder?(sdl, declared) ? [PLACEHOLDER_SCALAR] : [])
84
+ end
85
+
86
+ # Config.ide_helper's refresh: the files in the Rails app's root, when the
87
+ # schema loads. A schema that does not is the next request's error to raise;
88
+ # here it only warns.
89
+ def refresh(output_dir: ::Rails.root)
90
+ return [] unless Lighthouse::Config.ide_helper
91
+
92
+ write(output_dir: output_dir)
93
+ rescue StandardError => e
94
+ warn("Lighthouse: the IDE helper files were not refreshed: #{e.message}")
95
+ []
47
96
  end
48
97
 
49
98
  def directives_sdl(sdl = '', registry: Lighthouse::DirectiveRegistry.default)
@@ -56,13 +105,12 @@ module Lighthouse::IdeHelper
56
105
  )
57
106
  end
58
107
 
59
- def programmatic_types_sdl(sdl, registry: Lighthouse::DirectiveRegistry.default)
60
- original = type_names(sdl)
61
- generated = parse(registry.manipulate_sdl(sdl)).definitions.select do |definition|
62
- type_definition?(definition) && original.exclude?(definition.name)
63
- end
64
- generated_sdl = generated.sort_by(&:name).map(&:to_query_string)
65
- generated_sdl.unshift(PLACEHOLDER_SCALAR) if undeclared_placeholder?(sdl, original, generated)
108
+ # `directives`: the schema-directives.graphql written next to it, whose types
109
+ # (`CanAction`, `Mixed`, ...) some manipulators inject too and are left out here.
110
+ def programmatic_types_sdl(sdl, registry: Lighthouse::DirectiveRegistry.default, directives: directives_sdl(sdl, registry: registry))
111
+ declared = type_names(sdl) | type_names(directives)
112
+ generated_sdl = generated_types(sdl, declared, registry).sort_by(&:name).map(&:to_query_string)
113
+ generated_sdl.unshift(PLACEHOLDER_SCALAR) if undeclared_placeholder?(sdl, declared)
66
114
 
67
115
  render(
68
116
  'Types the manipulation phase generates (Connection/Paginator types, whereConditions inputs, ...).',
@@ -70,10 +118,19 @@ module Lighthouse::IdeHelper
70
118
  )
71
119
  end
72
120
 
73
- # The manipulation phase replaces the `_` placeholder (`where: _ @whereConditions`),
74
- # but the IDE reads the SDL as written, so it needs the placeholder declared.
75
- def undeclared_placeholder?(sdl, original, generated)
76
- sdl.to_s.match?(/:\s*\[?\s*_\b/) && original.exclude?('_') && generated.none? { |d| d.name == '_' }
121
+ # The types of the manipulated schema that neither the SDL nor the directives file defines.
122
+ def generated_types(sdl, declared, registry)
123
+ manipulated = Lighthouse::GraphQL::ASTBuilder.build(Lighthouse::GraphQL::SchemaGenerator.inject_directive_definitions(sdl), registry: registry)
124
+ parse(manipulated).definitions.select do |definition|
125
+ type_definition?(definition) && declared.exclude?(definition.name) && definition.name != '_'
126
+ end
127
+ end
128
+
129
+ # The manipulation phase replaces the `_` placeholder (`where: _ @whereConditions`,
130
+ # `orderBy: _ @orderBy`), but the IDE reads the SDL as written, so it needs
131
+ # the placeholder declared.
132
+ def undeclared_placeholder?(sdl, declared)
133
+ sdl.to_s.match?(/:\s*\[?\s*_\b/) && declared.exclude?('_')
77
134
  end
78
135
 
79
136
  def type_names(sdl)
@@ -8,6 +8,7 @@ module Lighthouse
8
8
  module Contracts; end
9
9
  module Events; end
10
10
  module Generators; end
11
+ module OrderBy; end
11
12
  module Support; end
12
13
  module WhereConditions; end
13
14
 
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The types @orderBy generates for an argument written with the `_`
4
+ # placeholder, as PHP Lighthouse's OrderByDirective and OrderByServiceProvider
5
+ # define them. For Query.posts(orderBy:), the prefix is QueryPostsOrderBy:
6
+ #
7
+ # no columns:/columnsEnum:/relations: [OrderByClause!] column: String!, order: SortOrder!
8
+ # columns: / columnsEnum: [<Prefix>OrderByClause!] column: <Prefix>Column! (or the columnsEnum)
9
+ # relations: [<Prefix>RelationOrderByClause!]
10
+ # column: <Prefix>Column (or String), order: SortOrder!, and per relation
11
+ # <relation>: <Prefix><Relation> { aggregate: COUNT } or, with the relation's
12
+ # own columns:, { aggregate: AVG|MIN|MAX|SUM|COUNT, column: <Prefix><Relation>Column }
13
+ class Lighthouse::OrderBy::Types
14
+ GeneratedTypes = Lighthouse::GraphQL::Directives::GeneratedTypes
15
+
16
+ DEFAULT_CLAUSE = 'OrderByClause'
17
+ SORT_ORDER = 'SortOrder'
18
+ AGGREGATE = 'OrderByRelationAggregateFunction'
19
+ AGGREGATE_WITH_COLUMN = 'OrderByRelationWithColumnAggregateFunction'
20
+
21
+ SDL = {
22
+ SORT_ORDER => <<~GRAPHQL,
23
+ "Directions for ordering a list of records."
24
+ enum SortOrder {
25
+ "Sort records in ascending order."
26
+ ASC
27
+
28
+ "Sort records in descending order."
29
+ DESC
30
+ }
31
+ GRAPHQL
32
+ AGGREGATE => <<~GRAPHQL,
33
+ "Aggregate functions when ordering by a relation without specifying a column."
34
+ enum OrderByRelationAggregateFunction {
35
+ "Amount of items."
36
+ COUNT @enum(value: "count")
37
+ }
38
+ GRAPHQL
39
+ AGGREGATE_WITH_COLUMN => <<~GRAPHQL
40
+ "Aggregate functions when ordering by a relation that may specify a column."
41
+ enum OrderByRelationWithColumnAggregateFunction {
42
+ "Average."
43
+ AVG @enum(value: "avg")
44
+
45
+ "Minimum."
46
+ MIN @enum(value: "min")
47
+
48
+ "Maximum."
49
+ MAX @enum(value: "max")
50
+
51
+ "Sum."
52
+ SUM @enum(value: "sum")
53
+
54
+ "Amount of items."
55
+ COUNT @enum(value: "count")
56
+ }
57
+ GRAPHQL
58
+ }.freeze
59
+
60
+ def initialize(document_ast, directive, arg, field, type)
61
+ @document_ast = document_ast
62
+ @directive = directive
63
+ @relations = Array(GeneratedTypes.directive_value(directive, 'relations')).map { |relation| relation.transform_keys(&:to_s) }
64
+ @arg = arg
65
+ @path = "#{type.name}.#{field.name}.#{arg.name}"
66
+ @prefix = GeneratedTypes.qualified_arg_type(arg, field, type)
67
+ @columns_enum = -> { GeneratedTypes.columns_enum(document_ast, directive, arg, field, type) }
68
+ end
69
+
70
+ # The argument, typed with the clauses generated for it.
71
+ def argument
72
+ @document_ast.define_type(SORT_ORDER, SDL[SORT_ORDER])
73
+ arg = GeneratedTypes.describe(@arg, "Order by clause for #{@path}.")
74
+ allowed = GeneratedTypes.allowed_columns?(@directive)
75
+ column_type = allowed ? @columns_enum.call : 'String'
76
+
77
+ clause =
78
+ if @relations.any? then relation_clause(column_type)
79
+ elsif allowed then column_clause("#{@prefix}OrderByClause", column_type, "Order by clause for #{@path}.")
80
+ else column_clause(DEFAULT_CLAUSE, 'String', 'Allows ordering a list of records.')
81
+ end
82
+ GeneratedTypes.replace_placeholder(arg, "[#{clause}!]")
83
+ end
84
+
85
+ private
86
+
87
+ def column_clause(name, column_type, description)
88
+ define(name, description, ["column: #{column_type}!", 'The column that is used for ordering.'], order_field)
89
+ end
90
+
91
+ # A column, or one relation's aggregate.
92
+ def relation_clause(column_type)
93
+ aggregates = @relations.map { |relation| ["#{relation['relation']}: #{aggregate_input(relation)}", 'Aggregate specification.'] }
94
+ define("#{@prefix}RelationOrderByClause", "Order by clause for #{@path}.",
95
+ ["column: #{column_type}", 'The column that is used for ordering.'], order_field, *aggregates)
96
+ end
97
+
98
+ # COUNT, or, with the relation's `columns:` / `columnsEnum:`, an aggregate of one of its columns.
99
+ def aggregate_input(relation)
100
+ name = "#{@prefix}#{GeneratedTypes.studly(relation['relation'])}"
101
+ columns_enum = relation_columns_enum(relation, name)
102
+ aggregate = columns_enum ? AGGREGATE_WITH_COLUMN : AGGREGATE
103
+ @document_ast.define_type(aggregate, SDL[aggregate])
104
+
105
+ fields = if columns_enum
106
+ [["aggregate: #{aggregate}!", 'The aggregate function to apply to the column.'],
107
+ ["column: #{columns_enum}", 'Name of the column to use.']]
108
+ else
109
+ [["aggregate: #{aggregate}!", 'Always COUNT.']]
110
+ end
111
+ define(name, "Aggregate specification for #{@path}.#{relation['relation']}.", *fields)
112
+ end
113
+
114
+ def relation_columns_enum(relation, name)
115
+ if relation['columns'] && relation['columnsEnum']
116
+ raise Lighthouse::DefinitionError,
117
+ "The relation #{relation['relation']} of @orderBy on #{@path} can only have one of the following arguments: `columns`, `columnsEnum`."
118
+ end
119
+ return relation['columnsEnum'] if relation['columnsEnum']
120
+ return nil unless relation['columns']
121
+
122
+ GeneratedTypes.define_columns_enum(@document_ast, "#{name}Column", relation['columns'], "#{@path}.#{relation['relation']}")
123
+ "#{name}Column"
124
+ end
125
+
126
+ def order_field
127
+ ["order: #{SORT_ORDER}!", 'The direction that is used for ordering.']
128
+ end
129
+
130
+ # Defines the input `name` with `fields`, each [SDL, description]; returns its name.
131
+ def define(name, description, *fields)
132
+ @document_ast.define_type(name) do
133
+ %("#{description}"\ninput #{name} {\n#{fields.map { |sdl, doc| %( "#{doc}"\n #{sdl}) }.join("\n\n")}\n}\n)
134
+ end
135
+ name
136
+ end
137
+ end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Hooks Lighthouse into a Rails host: exposes the `lighthouse:*` rake tasks,
4
- # and — with `config.watch_sdl` — reloads the app when a .graphql file changes.
4
+ # with `config.watch_sdl` reloads the app when a .graphql file changes, and
5
+ # with `config.ide_helper` (on in development when the app has a
6
+ # graphql.config.yml) rewrites the IDE helper files on boot and every reload.
5
7
  # Generators (`rails g lighthouse:install`) are found through lib/generators.
6
8
  class Lighthouse::Railtie < Rails::Railtie
7
9
  rake_tasks do
@@ -14,10 +16,15 @@ class Lighthouse::Railtie < Rails::Railtie
14
16
  Lighthouse::Railtie.watch_sdl(app.config)
15
17
  end
16
18
 
19
+ initializer 'lighthouse.ide_helper', after: :load_config_initializers do |app|
20
+ app.reloader.to_prepare { Lighthouse::IdeHelper.refresh(output_dir: app.root) }
21
+ end
22
+
17
23
  # Rails' reloader then treats a .graphql edit like a Ruby one: the app's
18
24
  # constants — the schema among them — are reloaded on the next request.
25
+ # Every .graphql under the entrypoint's folder is watched, imported or not.
19
26
  def self.watch_sdl(config)
20
- folder = Lighthouse::Config.sdl_folder
27
+ folder = Lighthouse::Config.schema_directory
21
28
  return false unless Lighthouse::Config.watch_sdl && folder
22
29
 
23
30
  config.watchable_dirs[folder.to_s] = %w[graphql graphqls]
@@ -38,7 +38,9 @@ require_relative 'namespaces'
38
38
  contracts/field_manipulator
39
39
  contracts/arg_manipulator
40
40
  contracts/arg_builder
41
- sdl_loader
41
+ contracts/field_builder
42
+ schema_stitcher
43
+ document_ast
42
44
  generated_resolvers
43
45
  default_resolve_dispatcher
44
46
  federation_directives
@@ -48,6 +50,8 @@ require_relative 'namespaces'
48
50
  events/execution_trace
49
51
  events/schema_build
50
52
  resolver_bases
53
+ root_types
54
+ ast_builder
51
55
  schema_generator
52
56
  schema_cache
53
57
  persisted_queries
@@ -78,14 +82,18 @@ require_relative 'namespaces'
78
82
  relation_counts
79
83
  nested_mutations
80
84
  directives/pagination_manipulator
81
- directives/where_conditions_directive
85
+ directives/generated_types
86
+ order_by/types
87
+ where_conditions/json_column
88
+ where_conditions/columns
89
+ where_conditions/types
90
+ where_conditions/handler
82
91
  directives/soft_deletes_directive
83
92
  directives/node_directive
84
93
  directives/model_directive
85
94
  directives/visibility_directive
86
95
  directive_resolvers/aggregate_resolver
87
96
  directive_resolvers/field_resolver
88
- directive_resolvers/where_conditions_applier
89
97
  directive_resolvers/all_resolver
90
98
  directive_resolvers/find_resolver
91
99
  directive_resolvers/paginate_resolver
@@ -123,6 +131,9 @@ require_relative 'namespaces'
123
131
  directives/arguments/filters
124
132
  directives/arguments/more_filters
125
133
  directives/arguments/order_by
134
+ directives/arguments/where_conditions_base
135
+ directives/arguments/where_conditions
136
+ directives/arguments/where_has_conditions
126
137
  directives/arguments/soft_deletes
127
138
  directives/definitions
128
139
  directives/registry
@@ -147,12 +158,12 @@ require_relative 'railtie' if defined?(Rails::Railtie)
147
158
 
148
159
  module Lighthouse::GraphQL::RbLightHouse
149
160
  class << self
150
- def get_schema(sdl_folder: nil, options: {})
161
+ def get_schema(schema_path: nil, options: {})
151
162
  base_types = options.delete(:base_types) || {}
152
163
  schema_class = options.delete(:base_schema)
153
164
 
154
165
  Lighthouse::GraphQL::SchemaFactory.build_base_schema(
155
- sdl_folder: sdl_folder,
166
+ schema_path: schema_path,
156
167
  base_types: base_types,
157
168
  schema_options: options.merge(schema_class: schema_class)
158
169
  )
@@ -6,7 +6,7 @@
6
6
  # directives to a query the resolver builds.
7
7
  #
8
8
  # type Query {
9
- # posts(title: String @eq, orderBy: [PostOrderByClause!] @orderBy): [Post!]! @builder(class: "Published")
9
+ # posts(title: String @eq, orderBy: _ @orderBy(columns: ["title"])): [Post!]! @builder(class: "Published")
10
10
  # }
11
11
  #
12
12
  # class Resolvers::Posts < Resolvers::BaseResolver
@@ -51,14 +51,14 @@ class Lighthouse::GraphQL::ResolveInfo
51
51
  context.respond_to?(:[]) ? context[:current_path] : nil
52
52
  end
53
53
 
54
- # `relation` with the field's model `scopes`, its @builder classes, and the
55
- # argument directives the client used (@eq, @where, @in, @like, @orderBy,
56
- # @whereConditions, @trashed, ...), as @all / @paginate apply them.
54
+ # `relation` with the field's model `scopes`, the argument directives the
55
+ # client used (@eq, @where, @in, @like, @orderBy, @whereConditions, @trashed,
56
+ # ...), then the field's @builder, @whereAuth and @orderBy(column:), as
57
+ # @all / @paginate apply them and as PHP Lighthouse's enhanceBuilder does.
57
58
  def enhance_builder(relation, scopes: [])
58
59
  resolver = Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver.new(field, parent_type, nil)
59
60
  args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
60
61
  relation = Array(scopes).reduce(relation) { |acc, scope| acc.public_send(scope) }
61
- relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, field.ast_node, args, context, resolver)
62
62
  resolver.apply_where_filters(relation, args, context)
63
63
  end
64
64
 
@@ -18,6 +18,11 @@
18
18
  # it is first used, so none misses what the base shares;
19
19
  # - for subscriptions, also resolves a Subscription field that has no class of
20
20
  # its own, instead of Lighthouse::GraphQL::Subscription::Default.
21
+ #
22
+ # The code-first types graphql-ruby's install writes are bases too (RootTypes):
23
+ # `query_type:` / `mutation_type:` / `subscription_type:` (Types::QueryType, ...)
24
+ # are what the SDL's root types are built on, so their `field`s are the
25
+ # schema's too, and `node:` (Types::NodeType) is the SDL's Node interface.
21
26
  module Lighthouse::GraphQL::ResolverBases
22
27
  # base_types key => the graphql-ruby class the base must inherit from.
23
28
  GRAPHQL_CLASSES = {
@@ -35,19 +40,28 @@ module Lighthouse::GraphQL::ResolverBases
35
40
  # (Types::PostType, Types::DateTimeType, ...) inherit from too.
36
41
  TYPE_KINDS = %i[object union interface scalar].freeze
37
42
 
38
- # Take the three keys out of opts[:base_types] (the caller's Hash is not
43
+ # Take Lighthouse's keys out of opts[:base_types] (the caller's Hash is not
39
44
  # changed) and return { root kind => base class } for those given, plus
40
- # :types => { kind => base } for graphql-ruby's own, which stay in opts.
45
+ # :types => { kind => base } for graphql-ruby's own, which stay in opts, and
46
+ # :roots => { query_type: ..., node: ... } for the code-first types (RootTypes).
41
47
  def extract!(opts)
42
48
  base_types = opts[:base_types]
43
49
  return {} unless base_types.is_a?(Hash)
44
50
 
45
51
  given = base_types.slice(*GRAPHQL_CLASSES.keys).compact
46
- opts[:base_types] = base_types.except(*GRAPHQL_CLASSES.keys)
52
+ opts[:base_types] = base_types.except(*GRAPHQL_CLASSES.keys, *Lighthouse::GraphQL::RootTypes::KEYS)
47
53
  given.each { |key, klass| validate!(key, klass) }
48
54
  bases = KEYS.filter_map { |kind, key| [kind, given[key]] if given[key] }.to_h
49
55
  type_bases = base_types.slice(*TYPE_KINDS).compact
50
- type_bases.empty? ? bases : bases.merge(types: type_bases)
56
+ bases = bases.merge(types: type_bases) unless type_bases.empty?
57
+ with_roots(bases, base_types)
58
+ end
59
+
60
+ # `bases` with the code-first root types given, validated, under :roots.
61
+ def with_roots(bases, base_types)
62
+ roots = base_types.slice(*Lighthouse::GraphQL::RootTypes::KEYS).compact
63
+ roots.each { |key, type| Lighthouse::GraphQL::RootTypes.validate!(key, type) }
64
+ roots.empty? ? bases : bases.merge(roots: roots)
51
65
  end
52
66
 
53
67
  def validate!(key, klass)
@@ -32,7 +32,22 @@ module Lighthouse::GraphQL::ResolverDispatch
32
32
  kwargs = Lighthouse::Support::Arguments.prepare(info.field, info.arguments, info.context)
33
33
  instance = Lighthouse::GraphQL::ResolveInfo.attach(klass.new(object: info.object, context: info.context, field: info.field), info)
34
34
  instance.define_singleton_method(:resolve) { |**values| public_send(method, **values) } if method
35
- instance.resolve_with_support(**kwargs)
35
+ single_input?(instance, kwargs) ? instance.resolve_with_support(**kwargs) : resolve_as_mutation(instance, kwargs)
36
+ end
37
+
38
+ # graphql-ruby's RelayClassicMutation (the base mutation its install writes)
39
+ # takes one `input:` argument. A field that declares its arguments one by one
40
+ # calls it as a plain GraphQL::Schema::Mutation, with them as keywords: past
41
+ # HasSingleInputArgument's unwrapping of `input:`, and its authorization of
42
+ # the arguments of `input:`.
43
+ def single_input?(instance, kwargs)
44
+ !instance.is_a?(::GraphQL::Schema::HasSingleInputArgument) || kwargs.key?(:input)
45
+ end
46
+
47
+ def resolve_as_mutation(instance, kwargs)
48
+ instance.define_singleton_method(:authorize_arguments, ::GraphQL::Schema::Resolver.instance_method(:authorize_arguments))
49
+ instance.singleton_class.send(:private, :authorize_arguments)
50
+ ::GraphQL::Schema::Mutation.instance_method(:resolve_with_support).bind_call(instance, **kwargs)
36
51
  end
37
52
 
38
53
  # The class itself when it answers `name` as a class method, else an instance.
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The code-first types graphql-ruby's install writes, which a schema builds on:
4
+ #
5
+ # base_types: { query_type: Types::QueryType, mutation_type: Types::MutationType,
6
+ # subscription_type: Types::SubscriptionType, node: Types::NodeType }
7
+ #
8
+ # - A root type given is what the SDL's root type of that kind is built on,
9
+ # instead of base_types[:object]: the SDL's `type Query` is a subclass of
10
+ # Types::QueryType, so the `field`s the class declares (graphql-ruby's
11
+ # `node` / `nodes`, what `graphql:mutation` adds to Types::MutationType)
12
+ # are the schema's too, beside the SDL's, and resolve as in graphql-ruby. An
13
+ # SDL field of the same name wins. With no such root type in the SDL, the
14
+ # class itself is the root, once it declares a field.
15
+ # - `node:` is the SDL's Node interface: `interface Node`, which @node adds and
16
+ # `implements Node` names, is that module, so graphql-ruby's Relay
17
+ # (Query.node / nodes, the schema's id_from_object / object_from_id) and the
18
+ # SDL's @node types share one Node. The `node` field @node adds to Query then
19
+ # gives way to the root type class's own.
20
+ #
21
+ # graphql-ruby's builder knows one base per kind, so the hook into it is a
22
+ # patch (graphql_ruby_patches.rb), which reads the bases of the build in
23
+ # progress from here.
24
+ module Lighthouse::GraphQL::RootTypes
25
+ KEYS = %i[query_type mutation_type subscription_type node].freeze
26
+
27
+ ROOTS = { query_type: :query, mutation_type: :mutation, subscription_type: :subscription }.freeze
28
+
29
+ # The resolver of the `node` field @node adds to Query.
30
+ NODE_RESOLVER = 'Lighthouse::GraphQL::NodeRegistry@resolve'
31
+
32
+ module_function
33
+
34
+ def validate!(key, type)
35
+ valid = key == :node ? Lighthouse::GraphQL::Type.kind_of(type) == :interface : type.is_a?(Class) && type < ::GraphQL::Schema::Object
36
+ return if valid
37
+
38
+ expected = key == :node ? 'a module including GraphQL::Schema::Interface' : 'a subclass of GraphQL::Schema::Object'
39
+ raise Lighthouse::DefinitionError, "base_types[:#{key}] must be #{expected}, not #{type.inspect}."
40
+ end
41
+
42
+ # The root type class the SDL's root type of `root_kind` (:queries,
43
+ # :mutations) is built on, when it is Lighthouse's (Lighthouse::GraphQL::Type):
44
+ # its methods resolve the root fields of their name.
45
+ def type_class(root_kind, roots)
46
+ klass = roots&.[]({ queries: :query_type, mutations: :mutation_type }[root_kind])
47
+ klass if klass&.include?(Lighthouse::GraphQL::Type)
48
+ end
49
+
50
+ # Run the block (the build of `document`) with `roots` as the bases of the
51
+ # types they name.
52
+ def with(document, roots)
53
+ previous = Thread.current[:lighthouse_root_types]
54
+ Thread.current[:lighthouse_root_types] = by_type_name(document, roots || {})
55
+ yield
56
+ ensure
57
+ Thread.current[:lighthouse_root_types] = previous
58
+ end
59
+
60
+ # The class an SDL type of `name` is built on, or nil.
61
+ def base_for(name)
62
+ Thread.current[:lighthouse_root_types]&.[](name)
63
+ end
64
+
65
+ # `definition` without the fields `base` declares itself that @node added.
66
+ def definition_on(definition, base)
67
+ names = field_names(base)
68
+ fields = definition.fields.reject { |field| names.include?(field.name) && added_by_node?(field) }
69
+ fields.size == definition.fields.size ? definition : definition.merge(fields: fields)
70
+ end
71
+
72
+ # The names of the fields a class and its ancestors declare. Not `fields`,
73
+ # which warns about a type with none (graphql-ruby's install leaves
74
+ # Types::MutationType empty once its example field goes).
75
+ def field_names(type)
76
+ type.ancestors.select { |ancestor| ancestor.respond_to?(:own_fields) }.flat_map { |ancestor| ancestor.own_fields.keys }.uniq
77
+ end
78
+
79
+ # The schema the block builds from `document`, with `roots` as the bases of
80
+ # the types they name; then the root type classes the SDL has no type for,
81
+ # and the subscriptions backend an app schema class `use`s (graphql_ruby_patches.rb).
82
+ def build(document, roots, &block)
83
+ schema = with(document, roots, &block)
84
+ apply_missing(schema, roots)
85
+ schema.extend(Lighthouse::GraphQL::InheritedSubscriptionRoot) if schema.respond_to?(:add_subscription_extension_if_necessary, true)
86
+ schema
87
+ end
88
+
89
+ # A root type class the SDL has no type for is the root itself, once it
90
+ # declares a field (graphql-ruby's install writes them with an example one).
91
+ def apply_missing(schema, roots)
92
+ ROOTS.each do |key, root|
93
+ type = roots&.[](key)
94
+ next if type.nil? || schema.public_send(root) || field_names(type).empty?
95
+
96
+ schema.public_send(root, type)
97
+ end
98
+ end
99
+
100
+ def by_type_name(document, roots)
101
+ names = root_names(document)
102
+ mapped = ROOTS.each_with_object({}) { |(key, root), out| out[names[root]] = roots[key] if roots[key] }
103
+ roots[:node] ? mapped.merge('Node' => roots[:node]) : mapped
104
+ end
105
+
106
+ # { query: 'Query', ... }, as the document's `schema { }` names them.
107
+ def root_names(document)
108
+ schema = document.definitions.find { |definition| definition.is_a?(::GraphQL::Language::Nodes::SchemaDefinition) }
109
+ { query: schema&.query || 'Query', mutation: schema&.mutation || 'Mutation', subscription: schema&.subscription || 'Subscription' }
110
+ end
111
+
112
+ def added_by_node?(field)
113
+ field.directives.any? do |directive|
114
+ directive.name == 'field' && Lighthouse::Support::DirectiveArgs.literal(directive, 'resolver') == NODE_RESOLVER
115
+ end
116
+ end
117
+ end
@@ -68,6 +68,10 @@ module Lighthouse::GraphQL::SchemaCache
68
68
  def fingerprint
69
69
  registry = Lighthouse::DirectiveRegistry.default
70
70
  manipulators = registry.sdl_manipulators.map { |manipulator| [manipulator.to_s, source_digest(manipulator)] }
71
+ phase = { Lighthouse::GraphQL::ASTBuilder => :build, Lighthouse::GraphQL::Directives::GeneratedTypes => :columns_enum,
72
+ Lighthouse::WhereConditions::Types => :define }
73
+ phase.merge!(registry.arg_manipulator_classes.index_with(:manipulate_arg_definition))
74
+ manipulators += phase.map { |manipulator, method_name| [manipulator.to_s, source_digest(manipulator, method_name)] }
71
75
  [
72
76
  registry.directive_definitions.sort,
73
77
  registry.type_definitions.sort,
@@ -77,10 +81,10 @@ module Lighthouse::GraphQL::SchemaCache
77
81
  ].inspect
78
82
  end
79
83
 
80
- # Build the schema document now (from Config.sdl_folder, unless given)
84
+ # Build the schema document now (from Config.schema_path, unless given)
81
85
  # and return the cache file's path.
82
86
  def warm!(sdl = nil)
83
- sdl ||= Lighthouse::GraphQL::SDLLoader.load_from_folder(Lighthouse::Config.sdl_folder.to_s)
87
+ sdl ||= Lighthouse::GraphQL::SchemaFactory.load_sdl
84
88
  Lighthouse::GraphQL::SchemaGenerator.build_document(sdl, force: true)
85
89
  File.join(directory.to_s, "#{PREFIX}#{key(sdl)}.bin")
86
90
  end
@@ -119,8 +123,8 @@ module Lighthouse::GraphQL::SchemaCache
119
123
  Lighthouse::Config.handle_error(e)
120
124
  end
121
125
 
122
- def source_digest(manipulator)
123
- file, = manipulator.method(:manipulate).source_location
126
+ def source_digest(manipulator, method_name = :manipulate)
127
+ file, = (manipulator.respond_to?(method_name) ? manipulator.method(method_name) : manipulator.instance_method(method_name)).source_location
124
128
  file && File.exist?(file) ? Digest::SHA256.file(file).hexdigest : nil
125
129
  rescue NameError
126
130
  nil
@@ -7,13 +7,13 @@
7
7
  module Lighthouse::GraphQL::SchemaFactory
8
8
  module_function
9
9
 
10
- def build_base_schema(sdl_folder: nil, sdl: nil, base_types: {}, schema_options: {}, **extra_options)
10
+ def build_base_schema(schema_path: nil, sdl: nil, base_types: {}, schema_options: {}, **extra_options)
11
11
  # Absorb any extra keyword options (e.g. `federation: true`) into the
12
12
  # schema options bag so callers can pass schema-level flags directly
13
13
  # without the factory needing to enumerate every one.
14
14
  schema_options = extra_options.merge(schema_options)
15
15
 
16
- sdl_string = sdl || load_sdl(sdl_folder)
16
+ sdl_string = sdl || load_sdl(schema_path)
17
17
  schema_class = schema_options.delete(:schema_class)
18
18
 
19
19
  Lighthouse::GraphQL::SchemaGenerator.build(
@@ -24,12 +24,9 @@ module Lighthouse::GraphQL::SchemaFactory
24
24
  )
25
25
  end
26
26
 
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)
32
-
33
- File.read(File.expand_path('../../schema.graphql', __dir__))
27
+ # The schema read from `schema_path` (default: Config.schema_path) and the
28
+ # files it imports.
29
+ def load_sdl(schema_path = nil)
30
+ Lighthouse::GraphQL::SchemaStitcher.new(schema_path || Lighthouse::Config.schema_path).schema_string
34
31
  end
35
32
  end