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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +212 -1
- data/README.md +38 -22
- data/docs/best-practices.md +6 -4
- data/docs/caching.md +4 -3
- data/docs/configuration.md +20 -26
- data/docs/custom-directives.md +2 -1
- data/docs/directives.md +79 -14
- data/docs/federation.md +1 -1
- data/docs/filtering-and-ordering.md +330 -30
- data/docs/generators.md +42 -40
- data/docs/getting-started.md +128 -58
- data/docs/motivation.md +3 -2
- data/docs/performance.md +5 -4
- data/docs/relationships.md +7 -5
- data/docs/relay.md +63 -0
- data/docs/resolvers.md +92 -39
- data/docs/schema-organisation.md +177 -0
- data/docs/security.md +64 -16
- data/docs/subscriptions.md +16 -9
- data/docs/types.md +16 -17
- data/docs/uploads.md +41 -14
- data/lib/generators/lighthouse/base_classes.rb +9 -14
- data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
- data/lib/generators/lighthouse/install/install_generator.rb +125 -99
- data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +2 -1
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +14 -13
- data/lib/lighthouse/ast_builder.rb +124 -0
- data/lib/lighthouse/caching.rb +9 -3
- data/lib/lighthouse/config.rb +27 -25
- data/lib/lighthouse/contracts/arg_manipulator.rb +9 -5
- data/lib/lighthouse/contracts/field_builder.rb +13 -0
- data/lib/lighthouse/controller.rb +28 -5
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +1 -2
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +0 -2
- data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +18 -18
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +9 -6
- data/lib/lighthouse/directive_resolvers/base.rb +6 -37
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +21 -6
- data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +0 -1
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +0 -2
- data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +3 -1
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +2 -3
- data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +85 -26
- data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +12 -6
- data/lib/lighthouse/directives/arguments/more_filters.rb +1 -1
- data/lib/lighthouse/directives/arguments/order_by.rb +190 -26
- data/lib/lighthouse/directives/arguments/where_conditions.rb +25 -0
- data/lib/lighthouse/directives/arguments/where_conditions_base.rb +84 -0
- data/lib/lighthouse/directives/arguments/where_has_conditions.rb +56 -0
- data/lib/lighthouse/directives/definitions.rb +0 -1
- data/lib/lighthouse/directives/generated_types.rb +103 -0
- data/lib/lighthouse/directives/registry.rb +39 -10
- data/lib/lighthouse/document_ast.rb +111 -0
- data/lib/lighthouse/field_resolver_stack.rb +9 -6
- data/lib/lighthouse/global_id.rb +29 -3
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
- data/lib/lighthouse/ide_helper.rb +80 -23
- data/lib/lighthouse/namespaces.rb +1 -0
- data/lib/lighthouse/order_by/types.rb +137 -0
- data/lib/lighthouse/railtie.rb +9 -2
- data/lib/lighthouse/rb_lighthouse.rb +16 -5
- data/lib/lighthouse/resolve_info.rb +5 -5
- data/lib/lighthouse/resolver_bases.rb +18 -4
- data/lib/lighthouse/resolver_dispatch.rb +16 -1
- data/lib/lighthouse/root_types.rb +117 -0
- data/lib/lighthouse/schema_cache.rb +8 -4
- data/lib/lighthouse/schema_factory.rb +6 -9
- data/lib/lighthouse/schema_generator.rb +8 -18
- data/lib/lighthouse/schema_stitcher.rb +64 -0
- data/lib/lighthouse/support/naming.rb +0 -12
- data/lib/lighthouse/upload.rb +4 -2
- data/lib/lighthouse/upload_store.rb +39 -14
- data/lib/lighthouse/where_conditions/columns.rb +36 -0
- data/lib/lighthouse/where_conditions/handler.rb +152 -0
- data/lib/lighthouse/where_conditions/json_column.rb +99 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +1 -4
- data/lib/lighthouse/where_conditions/types.rb +110 -0
- data/sig/lighthouse/directives.rbs +16 -3
- data/sig/lighthouse/graphql.rbs +48 -4
- data/sig/lighthouse.rbs +8 -12
- metadata +17 -7
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -29
- data/lib/generators/lighthouse/mutation_generator.rb +0 -31
- data/lib/generators/lighthouse/query_generator.rb +0 -30
- data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +0 -170
- data/lib/lighthouse/directives/where_conditions_directive.rb +0 -244
- 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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
generated_sdl
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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)
|
|
@@ -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
|
data/lib/lighthouse/railtie.rb
CHANGED
|
@@ -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
|
-
#
|
|
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.
|
|
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
|
-
|
|
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/
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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`,
|
|
55
|
-
#
|
|
56
|
-
#
|
|
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
|
|
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
|
-
|
|
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.
|
|
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::
|
|
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(:
|
|
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(
|
|
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(
|
|
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
|
|
28
|
-
#
|
|
29
|
-
def load_sdl(
|
|
30
|
-
|
|
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
|