lighthouse-graphql 0.9.0 → 0.9.1
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 +17 -1
- data/docs/directives.md +4 -0
- data/docs/filtering-and-ordering.md +79 -0
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +2 -2
- data/lib/lighthouse/directive_resolvers/base.rb +17 -0
- data/lib/lighthouse/directive_resolvers/can_resolvers.rb +2 -2
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +2 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/rb_lighthouse.rb +1 -0
- data/lib/lighthouse/schema_generator.rb +2 -0
- data/lib/lighthouse/search/auto.rb +5 -0
- data/lib/lighthouse/search/enhancer.rb +25 -1
- data/lib/lighthouse/search/field_check.rb +3 -1
- data/lib/lighthouse/search/pg_search.rb +70 -0
- data/lib/lighthouse/search/query.rb +20 -1
- data/lib/lighthouse/search/result.rb +2 -2
- data/lib/lighthouse/search/searchkick.rb +47 -4
- data/lib/lighthouse/search/together_check.rb +137 -0
- data/lib/lighthouse/search.rb +23 -0
- data/sig/lighthouse/search.rbs +23 -2
- data/sig/lighthouse.rbs +8 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 16b866dbbae1192cb051baae53de5a7938f9bb16c1f176a5112d8b1893cef78c
|
|
4
|
+
data.tar.gz: b0a9ed4c1fdfe4e6d22b7ebb5d07ff192ea661dbaaf7751cc726854edbbdb4b7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 485c2dcd6ade1ff9f1c62be69544114784c18ebedf2e25609791d9113eccdf97ef6b41824fb7b6bf6d0fffa86a95d14aac6f43eab91c21109bf3ec592ffb8192
|
|
7
|
+
data.tar.gz: 14a7e8b8718f655c53e9c7594099ba9060b9b34c60eb23a08a258c36856fa459c4da5aaa38ec99c74f32248cc365131a06176928ace9aa46cfbf65cc0e8458ec
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,21 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.1] - 2026-10-30
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`@search` over several models**: a field of a union or an interface (`everything(search: String!
|
|
14
|
+
@search): [SearchResult!]! @paginate`) searches the models of its types together, one ranked list with an
|
|
15
|
+
exact total, each record answered as its model's type. Searchkick searches their indices in one request,
|
|
16
|
+
each model's `search_where` and hidden records holding in its own index only (`_index`), each model's hits
|
|
17
|
+
loading through its own scopes; pg_search searches its multisearch documents, each one counted only when
|
|
18
|
+
its model's relation selects its record. The schema is refused when the field is not a root `@all` or
|
|
19
|
+
`@paginate`, has no non-null `@search` argument, has a type that is no model's or models no one engine
|
|
20
|
+
searches together, or asks for what one model's search applies (`scopes:`, `@builder`, `@whereAuth`,
|
|
21
|
+
`@orderBy(column:)`, `@trashed`). An adapter of its own opts in with `searchable_together?` and
|
|
22
|
+
`together_error`. See [filtering-and-ordering.md](docs/filtering-and-ordering.md#searching-several-models-together).
|
|
23
|
+
|
|
9
24
|
## [0.9.0] - 2026-10-30
|
|
10
25
|
|
|
11
26
|
### Added
|
|
@@ -1016,7 +1031,8 @@ had been running inside a private monorepo as a path gem before being extracted.
|
|
|
1016
1031
|
- Config-driven extension seams: `Lighthouse.configure`, `resolver_namespaces`,
|
|
1017
1032
|
`on_error`, and `DirectiveRegistry.register` for app-defined directives.
|
|
1018
1033
|
|
|
1019
|
-
[Unreleased]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.9.
|
|
1034
|
+
[Unreleased]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.9.1...HEAD
|
|
1035
|
+
[0.9.1]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.9.0...v0.9.1
|
|
1020
1036
|
[0.9.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.8.2...v0.9.0
|
|
1021
1037
|
[0.8.2]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.8.1...v0.8.2
|
|
1022
1038
|
[0.8.1]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.8.0...v0.8.1
|
data/docs/directives.md
CHANGED
|
@@ -694,6 +694,10 @@ each engine up.
|
|
|
694
694
|
filters the database can apply too (`@trashed`'s, `@eq` on a column), so a stale index never
|
|
695
695
|
answers one that no longer matches.
|
|
696
696
|
|
|
697
|
+
On a field of a union or an interface (`[SearchResult!]! @paginate`), the search runs over the models
|
|
698
|
+
of its types together, one ranked list: [Searching several models
|
|
699
|
+
together](filtering-and-ordering.md#searching-several-models-together).
|
|
700
|
+
|
|
697
701
|
A model's `search_results` scope, when it has one, is what a search of it may return:
|
|
698
702
|
Searchkick loads the records it finds through it, so a record the index still holds after it
|
|
699
703
|
should have left is not returned. A search counts only what it can load: a Searchkick model
|
|
@@ -684,6 +684,85 @@ class Found
|
|
|
684
684
|
end
|
|
685
685
|
```
|
|
686
686
|
|
|
687
|
+
### Searching several models together
|
|
688
|
+
|
|
689
|
+
A field of a union or an interface searches the models of its types together, in one search: a
|
|
690
|
+
global search box that finds posts and users at once, the best matches of both first. PHP Lighthouse
|
|
691
|
+
has no such search (Scout searches one model).
|
|
692
|
+
|
|
693
|
+
```graphql
|
|
694
|
+
union SearchResult = Post | User
|
|
695
|
+
|
|
696
|
+
type Query {
|
|
697
|
+
everything(search: String! @search): [SearchResult!]! @paginate
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
```graphql
|
|
702
|
+
{
|
|
703
|
+
everything(search: "rails", first: 10) {
|
|
704
|
+
data { __typename ... on Post { title } ... on User { name } }
|
|
705
|
+
paginatorInfo { total }
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
The models are those of the union's types, or of the object types that implement the interface: each
|
|
711
|
+
type is a model's (`@model(class:)`, or its name). Each record answers as its model's type. One search
|
|
712
|
+
over all of them answers one ranked list, paged as any search (`@all`, `@paginate` of any type), with an
|
|
713
|
+
exact total.
|
|
714
|
+
|
|
715
|
+
- **Searchkick** searches the models' indices in one request (`Searchkick.search(models: [...])`). What
|
|
716
|
+
narrows a model holds in its own index only, through the engine's `_index` (which Elasticsearch and
|
|
717
|
+
OpenSearch match against the alias Searchkick names): its `search_where`, and the records its default
|
|
718
|
+
scope hides. Each model's hits load through its own default scope, `search_results` scope and the
|
|
719
|
+
conditions the database applies alike, as for a search of it alone, so a model with a default scope or
|
|
720
|
+
`search_results` still needs `search_where`.
|
|
721
|
+
- **pg_search** searches its [multisearch](https://github.com/Casecommons/pg_search#multi-search)
|
|
722
|
+
documents (`pg_search_documents`), which each `multisearchable` model keeps, ranked together by
|
|
723
|
+
`PgSearch.multisearch_options`. A document counts only when its record is one its model's relation
|
|
724
|
+
selects (its default scope, its `search_results` scope, the field's `@eq`), narrowed in the database, so
|
|
725
|
+
a page and its total hold only what loads, whatever documents are left behind (a record deleted without
|
|
726
|
+
its callbacks).
|
|
727
|
+
|
|
728
|
+
```ruby
|
|
729
|
+
class Post < ApplicationRecord
|
|
730
|
+
include PgSearch::Model
|
|
731
|
+
multisearchable against: %i[title body], if: :published?
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
class User < ApplicationRecord
|
|
735
|
+
include PgSearch::Model
|
|
736
|
+
multisearchable against: :name
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
# config/initializers/pg_search.rb
|
|
740
|
+
PgSearch.multisearch_options = { using: { tsearch: { prefix: true } } }
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
The field's other arguments narrow every model alike: `@eq` holds in each index (or each model's
|
|
744
|
+
relation), and matches nothing in one that does not have the column. `@builder` on an argument writes to
|
|
745
|
+
the search as for one model (with pg_search, it may not set Searchkick's options). `@canQuery` checks each
|
|
746
|
+
record the search finds by its own policy.
|
|
747
|
+
|
|
748
|
+
The schema is refused when it is built, as `nothing applies` would be, where such a field:
|
|
749
|
+
|
|
750
|
+
- is not a root field with `@all` or `@paginate` (a `model:` on the directive searches that one model,
|
|
751
|
+
as any field);
|
|
752
|
+
- has no search to run: there is no table to list without one, so it takes one `@search`, on a top-level,
|
|
753
|
+
non-null argument (`search: String!`), with no `within:`, which names one model's index or scope;
|
|
754
|
+
- has a type that is no model's, or models no one engine searches together (a Searchkick model and a
|
|
755
|
+
pg_search one; a pg_search model that is not `multisearchable`), or a model the engine cannot search as
|
|
756
|
+
it is (Searchkick's `search_where`, above);
|
|
757
|
+
- asks for what one model's search would apply, and a search of several cannot: `scopes:`, `@builder`,
|
|
758
|
+
`@whereAuth` or `@orderBy(column:)` on the field, `@trashed` (each model has its own soft deletes),
|
|
759
|
+
`scopes:` on `@canQuery`, or a default value for a directive the engine cannot apply.
|
|
760
|
+
|
|
761
|
+
An adapter of your own searches models together when it answers `searchable_together?(model)`,
|
|
762
|
+
`together_error(model)` (why the model cannot be searched with others as it is, or nil) and `page` for a
|
|
763
|
+
`Lighthouse::Search::Query` that has `models` (and no `model`); `query.for(model)` is the same search of
|
|
764
|
+
one of them.
|
|
765
|
+
|
|
687
766
|
### Choosing the engine
|
|
688
767
|
|
|
689
768
|
By default the engine is detected per model: Searchkick if it calls `searchkick`, else pg_search if it
|
|
@@ -33,10 +33,10 @@ class Lighthouse::GraphQL::DirectiveResolvers::AllDirectiveResolver < Lighthouse
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
35
|
def resolve_all(model_class, arguments, ctx)
|
|
36
|
+
args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
37
|
+
return search_records(search_together(args, ctx), args, ctx) if searched_together
|
|
36
38
|
return [] unless model_class
|
|
37
39
|
|
|
38
|
-
# apply simple pagination args if present
|
|
39
|
-
args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
40
40
|
relation = apply_scopes(model_class.all)
|
|
41
41
|
|
|
42
42
|
relation = apply_where_filters(relation, args, ctx, search: :model)
|
|
@@ -149,6 +149,23 @@ class Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
|
149
149
|
Lighthouse::Search::Enhancer.new(arguments, relation, search: search, context: ctx, field: field).enhance
|
|
150
150
|
end
|
|
151
151
|
|
|
152
|
+
public
|
|
153
|
+
|
|
154
|
+
# The models the field searches together (a union's or an interface's types': Search.together),
|
|
155
|
+
# or nil for a field of one model.
|
|
156
|
+
def searched_together = Lighthouse::Search.together(@field_def)
|
|
157
|
+
|
|
158
|
+
# The search of the models the field searches together, narrowed by the field's argument
|
|
159
|
+
# filters: a Search::Result. Its errors fail the field, as the filters' (Support::Failures).
|
|
160
|
+
def search_together(args, ctx)
|
|
161
|
+
Lighthouse::Support::Failures.strictly(ctx) do
|
|
162
|
+
builders = @field_def.ast_node ? arg_builders(args_hash(args)) : []
|
|
163
|
+
unconstrained!(apply_search(nil, builders, ctx, searched_together))
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
152
169
|
# Does the field have an argument or input field with @search? Known when the schema is built.
|
|
153
170
|
def searching?
|
|
154
171
|
@field_def.ast_node && Lighthouse::Search.searching?(@field_def)
|
|
@@ -67,7 +67,7 @@ module Lighthouse::GraphQL::DirectiveResolvers
|
|
|
67
67
|
# A field whose model cannot be told fails the build, not a query. On an
|
|
68
68
|
# argument, the model is looked for only when the client gives it a value:
|
|
69
69
|
# an input type may be shared with fields that have no model.
|
|
70
|
-
model_class if self.class.model? && !@opts[:argument]
|
|
70
|
+
model_class if self.class.model? && !@opts[:argument] && !searched_together
|
|
71
71
|
me = self
|
|
72
72
|
inner = capture_inner(target_type_class)
|
|
73
73
|
install(target_type_class, lambda do |obj, args, ctx|
|
|
@@ -225,7 +225,7 @@ module Lighthouse::GraphQL::DirectiveResolvers
|
|
|
225
225
|
# most, and more that a page could reach is an error (Search::Result#every_record).
|
|
226
226
|
# Answers the Search::Result it checked, else nil.
|
|
227
227
|
def authorize_before(_obj, args, ctx)
|
|
228
|
-
found = apply_where_filters(scoped(model_class.all), args, ctx, search: :model)
|
|
228
|
+
found = searched_together ? search_together(args, ctx) : apply_where_filters(scoped(model_class.all), args, ctx, search: :model)
|
|
229
229
|
searched = found.is_a?(Lighthouse::Search::Result)
|
|
230
230
|
authorize_each!(ctx, searched ? found.every_record : found.to_a, args)
|
|
231
231
|
found if searched
|
|
@@ -93,6 +93,8 @@ class Lighthouse::GraphQL::DirectiveResolvers::PaginateDirectiveResolver < Light
|
|
|
93
93
|
# the other parents' when the Dataloader can; otherwise, or with `model:`,
|
|
94
94
|
# the model's whole table.
|
|
95
95
|
def page_list(obj, args, ctx)
|
|
96
|
+
return search_together(args, ctx) if searched_together
|
|
97
|
+
|
|
96
98
|
name = Lighthouse::Support::Naming.attribute(@field_def.name)
|
|
97
99
|
return decorate_relation(model_class.all, args, ctx, search: :model) unless nested?(obj, name)
|
|
98
100
|
|
|
@@ -134,6 +134,8 @@ module Lighthouse::GraphQL::SchemaGenerator
|
|
|
134
134
|
# `bases`: { root kind => the base class its fields' classes inherit from } (ResolverBases).
|
|
135
135
|
def post_process(schema, _impl, bases: {}, **_opts)
|
|
136
136
|
root_kinds = root_kinds(schema)
|
|
137
|
+
# A field of a union or an interface with @search searches its types' models together.
|
|
138
|
+
Lighthouse::Search::TogetherCheck.check_schema!(schema, root_kinds)
|
|
137
139
|
|
|
138
140
|
# Inspect types and fields for directives and attach generated resolvers
|
|
139
141
|
schema.types.each_value do |type|
|
|
@@ -18,4 +18,9 @@ class Lighthouse::Search::Auto
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def searchable?(model, index = nil) = !adapter_for(model, index).nil?
|
|
21
|
+
|
|
22
|
+
# The first adapter that searches every one of `models` together, or nil.
|
|
23
|
+
def adapter_together(models)
|
|
24
|
+
@adapters.find { |adapter| adapter.respond_to?(:together_error) && models.all? { |model| adapter.searchable_together?(model) } }
|
|
25
|
+
end
|
|
21
26
|
end
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
# write to a Search::Query (Contracts::SearchBuilder), and the answer is a
|
|
12
12
|
# Search::Result instead of a relation. Any other argument directive the client
|
|
13
13
|
# sends is an error, as is a second @search.
|
|
14
|
+
# - For models searched together (`models`: a union's or an interface's), there is no
|
|
15
|
+
# relation: the search is one over all of them, a Search::Result, whatever the adapter.
|
|
14
16
|
class Lighthouse::Search::Enhancer
|
|
15
17
|
# An argument directive of the field: its AST nodes, the builder class (or :builder), the client's value.
|
|
16
18
|
Argument = Struct.new(:node, :directive, :builder, :value, :present)
|
|
@@ -28,18 +30,21 @@ class Lighthouse::Search::Enhancer
|
|
|
28
30
|
# nil, a relation only; :model, a Search::Result for the model's own relation it
|
|
29
31
|
# built (with the `scopes:` Search::FieldCheck refuses for an engine), searched
|
|
30
32
|
# whole without a check; :given, one for a relation the app's code gave, checked.
|
|
31
|
-
# `field`: "Type.field", for the client's errors.
|
|
33
|
+
# `field`: "Type.field", for the client's errors. `search` is the models searched together, in
|
|
34
|
+
# place of a relation (nil), for a field of a union's or an interface's (Search.together).
|
|
32
35
|
def initialize(arguments, relation, search:, context:, field: 'the field')
|
|
33
36
|
@arguments = arguments
|
|
34
37
|
@relation = relation
|
|
35
38
|
@search = search
|
|
36
39
|
@context = context
|
|
37
40
|
@field = field
|
|
41
|
+
@models = search if search.is_a?(Array)
|
|
38
42
|
end
|
|
39
43
|
|
|
40
44
|
# The relation, searched; or a Search::Result. The relation itself if the client sent no search.
|
|
41
45
|
def enhance
|
|
42
46
|
search = the_search
|
|
47
|
+
return together(search) if @models
|
|
43
48
|
return @relation unless search
|
|
44
49
|
|
|
45
50
|
model = @relation.klass
|
|
@@ -73,6 +78,25 @@ class Lighthouse::Search::Enhancer
|
|
|
73
78
|
|
|
74
79
|
searchable!(adapter, query.model)
|
|
75
80
|
whole_index!(adapter) if @search == :given
|
|
81
|
+
narrowed(adapter, query)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The search of the models, together: the field's argument is non-null (Search::FieldCheck), so
|
|
85
|
+
# the client always sends one.
|
|
86
|
+
def together(search)
|
|
87
|
+
raise ::GraphQL::ExecutionError, "#{@field} searches #{@models.map(&:name).join(', ')} together: it needs a search." unless search
|
|
88
|
+
|
|
89
|
+
adapter = Lighthouse::Search.adapter_together(@models) ||
|
|
90
|
+
raise(Lighthouse::DefinitionError, "@search: no engine searches #{@models.map(&:name).join(', ')} together.")
|
|
91
|
+
@models.each do |model|
|
|
92
|
+
error = adapter.together_error(model)
|
|
93
|
+
raise Lighthouse::DefinitionError, "@search: #{error}." if error
|
|
94
|
+
end
|
|
95
|
+
narrowed(adapter, Lighthouse::Search::Query.new(nil, search.value, models: @models))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The search, narrowed by the other arguments the client sent, which the engine must take.
|
|
99
|
+
def narrowed(adapter, query)
|
|
76
100
|
reject_incompatible
|
|
77
101
|
query = @arguments.reject { |argument| search?(argument) }.reduce(query) { |narrowed, argument| narrow(narrowed, argument) }
|
|
78
102
|
Lighthouse::Search::Result.checked(adapter, query, @field) || Lighthouse::Search::Result.new(adapter, query, context: @context, field: @field)
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
# user must not see. A relational adapter (pg_search) applies them.
|
|
19
19
|
# - An @canQuery, which runs the search to check the records it finds, searches
|
|
20
20
|
# a searchable model, and without `scopes:` when the engine cannot apply them.
|
|
21
|
+
#
|
|
22
|
+
# A field of a union or an interface whose models are searched together is TogetherCheck's.
|
|
21
23
|
class Lighthouse::Search::FieldCheck
|
|
22
24
|
HINT = 'it needs searchkick or pg_search, or an adapter in Config.search'
|
|
23
25
|
def self.check!(...) = new(...).check!
|
|
@@ -33,7 +35,7 @@ class Lighthouse::Search::FieldCheck
|
|
|
33
35
|
|
|
34
36
|
def check!
|
|
35
37
|
searches = Lighthouse::Search.directives(@field_def)
|
|
36
|
-
return if searches.empty?
|
|
38
|
+
return if searches.empty? || Lighthouse::Search.together(@field_def)
|
|
37
39
|
|
|
38
40
|
searches.each { |path, directive| within_string!(path, directive) }
|
|
39
41
|
applied!(searches.first.first)
|
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
# The model's `search_results` scope, if it has one, narrows the search too
|
|
14
14
|
# (Lighthouse::Search).
|
|
15
15
|
#
|
|
16
|
+
# Models searched together (Search::Together) are searched through pg_search's
|
|
17
|
+
# multisearch, the documents each `multisearchable` model keeps in one table
|
|
18
|
+
# (PgSearch::Document, `pg_search_documents`), ranked together. A document counts only
|
|
19
|
+
# when its record is one its model's relation selects (the model's default scope, its
|
|
20
|
+
# search_results scope, the field's @eq on a column of its): the documents are narrowed
|
|
21
|
+
# in the database, so pages and totals hold only what loads. Each page loads its records
|
|
22
|
+
# through the same relations, a query per model.
|
|
23
|
+
#
|
|
16
24
|
# The gem is not a dependency of Lighthouse: the adapter is used when the
|
|
17
25
|
# model includes PgSearch::Model.
|
|
18
26
|
class Lighthouse::Search::PgSearch
|
|
@@ -42,8 +50,70 @@ class Lighthouse::Search::PgSearch
|
|
|
42
50
|
orders.empty? ? searched : searched.reorder(*orders)
|
|
43
51
|
end
|
|
44
52
|
|
|
53
|
+
# Models searched together: those that include PgSearch::Model, which must keep multisearch
|
|
54
|
+
# documents (`multisearchable`), whether or not they have a pg_search scope of their own.
|
|
55
|
+
def searchable_together?(model) = !!(defined?(::PgSearch::Model) && model.include?(::PgSearch::Model))
|
|
56
|
+
|
|
57
|
+
def together_error(model)
|
|
58
|
+
return nil if model.respond_to?(:pg_search_multisearchable_options)
|
|
59
|
+
|
|
60
|
+
"#{model.name} keeps no pg_search multisearch documents, which models searched together are searched through: " \
|
|
61
|
+
'call `multisearchable against: [...]` in it'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Models searched together: [one entry per document at `offset`, `limit` of them, in rank
|
|
65
|
+
# order: its record, or nil for one that did not load; how many documents match].
|
|
66
|
+
def page(query, offset:, limit:)
|
|
67
|
+
options!(query)
|
|
68
|
+
documents = documents(query)
|
|
69
|
+
found = limit.zero? ? [] : documents.offset(offset).limit(limit).to_a
|
|
70
|
+
[records(query, found), documents.unscope(:order, :select).count]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# pg_search has no result window.
|
|
74
|
+
def result_window(_query) = nil
|
|
75
|
+
|
|
45
76
|
private
|
|
46
77
|
|
|
78
|
+
# The documents that match, in rank order, each one's record selected by its model's relation.
|
|
79
|
+
def documents(query)
|
|
80
|
+
kept = query.models.map do |model|
|
|
81
|
+
::PgSearch::Document.where(searchable_type: model.polymorphic_name, searchable_id: relation(query.for(model)).select(model.primary_key))
|
|
82
|
+
end
|
|
83
|
+
::PgSearch.multisearch(query.term).where(id: kept.reduce(:or).select(:id))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The model's records a search of it may answer: its relation and search_results scope, with the
|
|
87
|
+
# field's @eq conditions; none where a condition names a column the model does not have.
|
|
88
|
+
def relation(query)
|
|
89
|
+
model = query.model
|
|
90
|
+
query.wheres.reduce(Lighthouse::Search.results(model.all)) do |relation, (column, value)|
|
|
91
|
+
model.column_names.include?(column) ? relation.where(column => value) : relation.none
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The records of `documents`, in their order, nil for one its model's relation no longer selects.
|
|
96
|
+
# The models that share a document type (an STI class and its subclasses) each load theirs.
|
|
97
|
+
def records(query, documents)
|
|
98
|
+
loaded = documents.group_by(&:searchable_type).to_h do |type, typed|
|
|
99
|
+
ids = typed.map(&:searchable_id)
|
|
100
|
+
found = query.models.select { |model| model.polymorphic_name == type }.flat_map do |model|
|
|
101
|
+
relation(query.for(model)).where(model.primary_key => ids).to_a
|
|
102
|
+
end
|
|
103
|
+
[type, found.index_by { |record| record.id.to_s }]
|
|
104
|
+
end
|
|
105
|
+
documents.map { |document| loaded.dig(document.searchable_type, document.searchable_id.to_s) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# An @builder's Searchkick options, which multisearch cannot take: one that narrows what loads
|
|
109
|
+
# (`scope_results:`) would be dropped.
|
|
110
|
+
def options!(query)
|
|
111
|
+
return if query.options.empty?
|
|
112
|
+
|
|
113
|
+
raise Lighthouse::DefinitionError,
|
|
114
|
+
"@search: pg_search's multisearch takes no option of Searchkick's, which an @builder set: #{query.options.keys.join(', ')}"
|
|
115
|
+
end
|
|
116
|
+
|
|
47
117
|
def scope_name(index) = (index || DEFAULT_SCOPE).to_sym
|
|
48
118
|
|
|
49
119
|
# Is the model's class method `name` a pg_search scope? pg_search keeps no list
|
|
@@ -4,10 +4,16 @@
|
|
|
4
4
|
# what narrows it. The directives of the other arguments write to it
|
|
5
5
|
# (Contracts::SearchBuilder), and the adapter runs it (Search::Searchkick#page).
|
|
6
6
|
# Directives never know which engine is behind it.
|
|
7
|
+
#
|
|
8
|
+
# A search of several models together (a union's or an interface's: Search::Together) has
|
|
9
|
+
# `models` and no `model`; `for(model)` is the same search of one of them.
|
|
7
10
|
class Lighthouse::Search::Query
|
|
8
11
|
# The model searched, the `@search` argument's value, the `within:` index (nil: the model's own).
|
|
9
12
|
attr_reader :model, :term, :index
|
|
10
13
|
|
|
14
|
+
# The models searched together, or nil for a search of one.
|
|
15
|
+
attr_reader :models
|
|
16
|
+
|
|
11
17
|
# [[column, value], ...], from @eq (and @builder), in order: all of them hold,
|
|
12
18
|
# the same column twice too (a list of inputs), as the database applies them.
|
|
13
19
|
attr_reader :wheres
|
|
@@ -18,15 +24,28 @@ class Lighthouse::Search::Query
|
|
|
18
24
|
# :with, :only or :without, from @trashed; nil: the client did not say.
|
|
19
25
|
attr_accessor :trashed
|
|
20
26
|
|
|
21
|
-
def initialize(model, term, index = nil)
|
|
27
|
+
def initialize(model, term, index = nil, models: nil)
|
|
22
28
|
@model = model
|
|
23
29
|
@term = term
|
|
24
30
|
@index = index
|
|
31
|
+
@models = models
|
|
25
32
|
@wheres = []
|
|
26
33
|
@options = {}
|
|
27
34
|
@trashed = nil
|
|
28
35
|
end
|
|
29
36
|
|
|
37
|
+
# The model's name, or the names of the models searched together, for errors.
|
|
38
|
+
def name = models ? models.map(&:name).join(', ') : model.name
|
|
39
|
+
|
|
40
|
+
# The same search of `model` alone: its term, conditions and options.
|
|
41
|
+
def for(model)
|
|
42
|
+
self.class.new(model, term, index).tap do |one|
|
|
43
|
+
wheres.each { |column, value| one.where(column, value) }
|
|
44
|
+
one.options.merge!(options)
|
|
45
|
+
one.trashed = trashed
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
30
49
|
# The column equals the value, as Scout's `where`.
|
|
31
50
|
def where(column, value)
|
|
32
51
|
@wheres << [column.to_s, value]
|
|
@@ -142,7 +142,7 @@ class Lighthouse::Search::Result
|
|
|
142
142
|
# Is it the search of `field` that `adapter` runs for `query`? Its options aside,
|
|
143
143
|
# which may hold an @builder's procs, made again for each search.
|
|
144
144
|
def same_search?(adapter, query, field)
|
|
145
|
-
@adapter.equal?(adapter) && @field == field && query.model.equal?(@query.model) &&
|
|
145
|
+
@adapter.equal?(adapter) && @field == field && query.model.equal?(@query.model) && query.models == @query.models &&
|
|
146
146
|
[query.term, query.index, query.wheres, query.trashed] == [@query.term, @query.index, @query.wheres, @query.trashed]
|
|
147
147
|
end
|
|
148
148
|
|
|
@@ -176,6 +176,6 @@ class Lighthouse::Search::Result
|
|
|
176
176
|
|
|
177
177
|
raise ::GraphQL::ExecutionError,
|
|
178
178
|
"The search reads no further than its first #{result_window} matches (the index.max_result_window of " \
|
|
179
|
-
"#{@query.
|
|
179
|
+
"#{@query.name}), so offset #{offset} is past them. Narrow the search instead."
|
|
180
180
|
end
|
|
181
181
|
end
|
|
@@ -53,16 +53,20 @@ class Lighthouse::Search::Searchkick
|
|
|
53
53
|
# [one entry per hit at `offset`, `limit` of them, in relevance order: the
|
|
54
54
|
# record, or nil for a hit that did not load; the total that match].
|
|
55
55
|
def page(query, offset:, limit:)
|
|
56
|
-
response =
|
|
56
|
+
response = search(query, offset, limit)
|
|
57
57
|
exact_total!(query, response)
|
|
58
58
|
[entries(response), response.total_count]
|
|
59
59
|
rescue StandardError => e
|
|
60
60
|
raise unless past_window?(e)
|
|
61
61
|
|
|
62
62
|
# The index's window is smaller than the model's options say: set on the server.
|
|
63
|
-
raise ::GraphQL::ExecutionError, "The search reads no further than the index.max_result_window of #{query.
|
|
63
|
+
raise ::GraphQL::ExecutionError, "The search reads no further than the index.max_result_window of #{query.name}. Narrow the search instead."
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# Models searched together: those it searches alone, and why one cannot be as it is (model_error).
|
|
67
|
+
def searchable_together?(model) = searchable?(model)
|
|
68
|
+
def together_error(model) = model_error(model)
|
|
69
|
+
|
|
66
70
|
# How many of the matches the index the search reads pages through
|
|
67
71
|
# (Search::Result): its index.max_result_window, read from the engine
|
|
68
72
|
# (Searchkick::Index). When the settings cannot be read, the model's own index
|
|
@@ -70,6 +74,8 @@ class Lighthouse::Search::Searchkick
|
|
|
70
74
|
# its index `settings:`, whatever their keys; a billion with `deep_paging:
|
|
71
75
|
# true`), and any index the engines' default, 10,000.
|
|
72
76
|
def result_window(query)
|
|
77
|
+
return query.models.map { |model| result_window(query.for(model)) }.min if query.models
|
|
78
|
+
|
|
73
79
|
@index.result_window(query.model, index_name(query)) ||
|
|
74
80
|
(own_index?(query) && options_window(query.model)) || Lighthouse::Search::Result::MAX_WINDOW
|
|
75
81
|
end
|
|
@@ -133,7 +139,7 @@ class Lighthouse::Search::Searchkick
|
|
|
133
139
|
return unless total.is_a?(Hash) && total['relation'] && total['relation'] != 'eq'
|
|
134
140
|
|
|
135
141
|
raise Lighthouse::DefinitionError,
|
|
136
|
-
"@search on #{query.
|
|
142
|
+
"@search on #{query.name}: the engine counted #{total['relation']} #{total['value']} matches, not every one, " \
|
|
137
143
|
'which totals, pages and @canQuery need (track_total_hits)'
|
|
138
144
|
end
|
|
139
145
|
|
|
@@ -154,14 +160,51 @@ class Lighthouse::Search::Searchkick
|
|
|
154
160
|
defined?(::Searchkick::InvalidQueryError) && error.is_a?(::Searchkick::InvalidQueryError) && error.message.include?('max_result_window')
|
|
155
161
|
end
|
|
156
162
|
|
|
163
|
+
# The engine's response to the search, `limit` hits from `offset`: of the model, or of the
|
|
164
|
+
# models searched together, in one request over their indices.
|
|
165
|
+
def search(query, offset, limit)
|
|
166
|
+
return query.model.searchkick_search(query.term, **search_options(query), offset: offset, limit: limit) unless query.models
|
|
167
|
+
|
|
168
|
+
::Searchkick.search(query.term, models: query.models, **together_options(query), offset: offset, limit: limit)
|
|
169
|
+
end
|
|
170
|
+
|
|
157
171
|
def search_options(query)
|
|
158
172
|
options = Lighthouse::Search::Searchkick::Options.builder!(query)
|
|
159
173
|
wheres = query.wheres.map { |column, value| { column => value } }
|
|
160
174
|
options[:where] = all_of(options[:where], engine_where(query), *wheres, trashed_filter(query))
|
|
161
175
|
options[:index_name] = query.index if query.index
|
|
176
|
+
loaded(options, results_scope(query, options[:scope_results]))
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# The options of a search of several models, in one request over their indices. What narrows
|
|
180
|
+
# each model holds in its own index only (`_index`, which the engines match against the
|
|
181
|
+
# alias Searchkick names): its search_where, and the records its default scope hides; what the
|
|
182
|
+
# client sends (@eq) holds in every index, matching nothing in one that does not hold the
|
|
183
|
+
# column. Each model's records load through its own narrowing (results_scope), which Searchkick
|
|
184
|
+
# calls with each model's relation.
|
|
185
|
+
def together_options(query)
|
|
186
|
+
options = Lighthouse::Search::Searchkick::Options.builder!(query)
|
|
187
|
+
wheres = query.wheres.map { |column, value| { column => value } }
|
|
188
|
+
each_own = query.models.map { |model| own_where(query.for(model)) }
|
|
189
|
+
options[:where] = all_of(options[:where], *wheres, { _or: each_own })
|
|
190
|
+
loads = query.models.index_with { |model| results_scope(query.for(model), options[:scope_results]) }.compact
|
|
191
|
+
loaded(options, loads.empty? ? nil : ->(records) { (loads[model_of(records)] || :itself.to_proc).call(records) })
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# What Searchkick loads a model's hits through: the model itself, or a relation of it.
|
|
195
|
+
def model_of(records) = records.respond_to?(:klass) ? records.klass : records
|
|
196
|
+
|
|
197
|
+
# What one model's hits must hold in a search of several: being in its index, and what its own
|
|
198
|
+
# search would have the engine apply.
|
|
199
|
+
def own_where(query)
|
|
200
|
+
all_of({ _index: query.model.searchkick_index.name }, engine_where(query), trashed_filter(query))
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# The options Lighthouse sets whatever the search: the records load, through `loading` (the
|
|
204
|
+
# `scope_results:` that narrows them) if any, and every match is counted.
|
|
205
|
+
def loaded(options, loading)
|
|
162
206
|
options[:load] = true
|
|
163
207
|
options[:body_options] = Lighthouse::Search::Searchkick::Options.counting(options)
|
|
164
|
-
loading = results_scope(query, options[:scope_results])
|
|
165
208
|
options[:scope_results] = loading if loading
|
|
166
209
|
options
|
|
167
210
|
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A field of a union or an interface (with @all or @paginate, and no `model:`) whose @search
|
|
4
|
+
# searches the models of its types together, in one search, checked when the schema is built
|
|
5
|
+
# (SchemaGenerator, before FieldCheck, which leaves the field to it), as FieldCheck checks a
|
|
6
|
+
# field of one model. It keeps the models for the field's resolvers (Search.together!).
|
|
7
|
+
#
|
|
8
|
+
# Each type is a model's (@model, or its name), one engine searches them all
|
|
9
|
+
# (`adapter_together`, `together_error`), the @search argument is a top-level, non-null one,
|
|
10
|
+
# with no `within:`, on a root field; and nothing a model would apply alone, but the engine
|
|
11
|
+
# across them cannot, is asked for: `scopes:`, @builder, @whereAuth, @orderBy(column:),
|
|
12
|
+
# @trashed, a default value the engine cannot apply, an @canQuery's `scopes:`.
|
|
13
|
+
class Lighthouse::Search::TogetherCheck < Lighthouse::Search::FieldCheck
|
|
14
|
+
# Every field of the schema's object types (`root_kinds`: SchemaGenerator#root_kinds).
|
|
15
|
+
def self.check_schema!(schema, root_kinds)
|
|
16
|
+
schema.types.each_value do |type|
|
|
17
|
+
next unless type.kind.object?
|
|
18
|
+
|
|
19
|
+
type.fields.each_value { |field_def| check!(schema, type, field_def, root_kinds[type.graphql_name]) if field_def.owner == type }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.check!(schema, type, field_def, root_kind)
|
|
24
|
+
return unless field_def.ast_node
|
|
25
|
+
|
|
26
|
+
new(schema, type, field_def, root_kind).check!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(schema, type, field_def, root_kind)
|
|
30
|
+
super(type, field_def, field_def.ast_node.directives || [], root_kind)
|
|
31
|
+
@schema = schema
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def check!
|
|
35
|
+
searches = Lighthouse::Search.directives(@field_def)
|
|
36
|
+
together!(searches) if searches.any? && together?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Does the field search the models of a union's or an interface's types together? One that a
|
|
42
|
+
# data directive resolves without a `model:` of its own.
|
|
43
|
+
def together?
|
|
44
|
+
!resolving_directive.nil? && returned&.kind&.abstract? &&
|
|
45
|
+
Lighthouse::Support::DirectiveArgs.literal(resolving_directive, 'model').nil?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The type of what the field returns: of its items, for a paginated field (`PostPaginator`'s Post).
|
|
49
|
+
def returned
|
|
50
|
+
type = @field_def.type.unwrap
|
|
51
|
+
return type unless resolving_directive.name == 'paginate'
|
|
52
|
+
|
|
53
|
+
@schema.get_type(type.graphql_name.sub(Lighthouse::Support::ModelResolver::PAGINATION_SUFFIX, ''))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def together!(searches)
|
|
57
|
+
path = searches.first.first
|
|
58
|
+
searches.each { |each_path, directive| within_string!(each_path, directive) }
|
|
59
|
+
applied!(path)
|
|
60
|
+
together_field!(path, searches)
|
|
61
|
+
models = together_models(path)
|
|
62
|
+
adapter = together_adapter!(path, models)
|
|
63
|
+
together_constraints!(path, adapter)
|
|
64
|
+
can_queries.each { |can_query| together_can_query!(path, can_query) }
|
|
65
|
+
Lighthouse::Search.together!(@field_def, models)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# On a root field, with @all or @paginate, and one @search: a top-level, non-null argument
|
|
69
|
+
# (with no models to list, a field of several models answers a search, always), with no `within:`.
|
|
70
|
+
def together_field!(path, searches)
|
|
71
|
+
unless @root_kind && %w[all paginate].include?(resolving_directive.name)
|
|
72
|
+
fail!(path, "#{abstract_name} has several models, which only @all or @paginate on a root field search together " \
|
|
73
|
+
'(or give the directive the `model:` it searches)')
|
|
74
|
+
end
|
|
75
|
+
one_search!(path, searches)
|
|
76
|
+
return if Lighthouse::Support::DirectiveArgs.literal(searches.first.last, 'within').nil?
|
|
77
|
+
|
|
78
|
+
fail!(path, "`within:` names one model's index or scope, and #{abstract_name}'s models are searched together")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def one_search!(path, searches)
|
|
82
|
+
return if searches.size == 1 && @field_def.arguments[path]&.type&.non_null?
|
|
83
|
+
|
|
84
|
+
fail!(path, "a field that searches #{abstract_name}'s models together needs its search: one @search, on a non-null argument " \
|
|
85
|
+
'(`search: String!`), which there is no table to list without')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The models of the union's or the interface's object types: each must be a model's.
|
|
89
|
+
def together_models(path)
|
|
90
|
+
types = @schema.possible_types(returned).sort_by(&:graphql_name)
|
|
91
|
+
types.map do |type|
|
|
92
|
+
Lighthouse::GraphQL::Directives::ModelDirective.model_for(type) ||
|
|
93
|
+
fail!(path, "#{abstract_name}'s type #{type.graphql_name} is no model's: give it @model(class:), or remove it")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def together_adapter!(path, models)
|
|
98
|
+
adapter = Lighthouse::Search.adapter_together(models) || no_engine!(path, models)
|
|
99
|
+
models.each do |model|
|
|
100
|
+
error = adapter.together_error(model)
|
|
101
|
+
fail!(path, error) if error
|
|
102
|
+
end
|
|
103
|
+
adapter
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# What one model's search would apply, and a search of several cannot: the field's own query
|
|
107
|
+
# directives and `scopes:`, @trashed (each model has its own soft-delete column) and default
|
|
108
|
+
# values the engine cannot apply.
|
|
109
|
+
def together_constraints!(path, adapter)
|
|
110
|
+
name = adapter.class.name.demodulize
|
|
111
|
+
dropped = dropped_constraints
|
|
112
|
+
fail!(path, "#{name} cannot apply #{dropped.join(', ')} to models searched together; remove them") unless dropped.empty?
|
|
113
|
+
trashed = Lighthouse::Support::ArgumentWalk.field_definitions(@field_def).find do |_, argument|
|
|
114
|
+
Lighthouse::Support::ArgumentWalk.directives(argument).any? { |directive| directive.name == 'trashed' }
|
|
115
|
+
end
|
|
116
|
+
fail!(path, "@trashed on #{trashed.first} cannot filter models searched together, each with its own soft deletes") if trashed
|
|
117
|
+
defaulted_constraints.each do |constraint|
|
|
118
|
+
fail!(path, "#{name} cannot apply #{constraint}, whose default value would apply to every search; remove the default")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def no_engine!(path, models)
|
|
123
|
+
engines = models.map { |model| "#{model.name} (#{engine_name(model)})" }
|
|
124
|
+
fail!(path, "no engine searches #{abstract_name}'s models together: #{engines.join(', ')}. One engine searches them " \
|
|
125
|
+
'all: Searchkick, or pg_search through its multisearch documents')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def engine_name(model) = Lighthouse::Search.adapter_for(model)&.class&.name&.demodulize || 'not searchable'
|
|
129
|
+
|
|
130
|
+
def together_can_query!(path, can_query)
|
|
131
|
+
return if Lighthouse::Support::DirectiveArgs.literal(can_query, 'scopes').blank?
|
|
132
|
+
|
|
133
|
+
fail!(path, 'the `scopes:` of @canQuery cannot narrow models searched together; remove them')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def abstract_name = returned.graphql_name
|
|
137
|
+
end
|
data/lib/lighthouse/search.rb
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
# relational? is the search a scope on the model's relation (pg_search)?
|
|
9
9
|
# scope(relation, query) relational: the relation narrowed to the matches, ranked
|
|
10
10
|
# page(query, offset:, limit:) otherwise: [records, total] of the engine's response
|
|
11
|
+
# searchable_together?(model) can it search the model with others (optional, with together_error)?
|
|
12
|
+
# together_error(model) why it cannot search the model with others as it is, or nil
|
|
13
|
+
#
|
|
14
|
+
# Models searched together (a field of a union or an interface: FieldCheck) are one search
|
|
15
|
+
# over all of them, a Query with `models`, which the adapter answers with `page`, whatever it
|
|
16
|
+
# is: Searchkick over their indices, pg_search through its multisearch documents.
|
|
11
17
|
#
|
|
12
18
|
# `query` is a Search::Query. A relational adapter lets every other directive of
|
|
13
19
|
# the field apply on top of the scope; the others (Searchkick) answer on their own
|
|
@@ -75,4 +81,21 @@ module Lighthouse::Search
|
|
|
75
81
|
|
|
76
82
|
# Does the field have an argument or input field with @search?
|
|
77
83
|
def searching?(field_def) = directives(field_def).any?
|
|
84
|
+
|
|
85
|
+
# The models the field searches together (Search::FieldCheck keeps them when the schema is
|
|
86
|
+
# built: the models of its union's or interface's types), or nil for a field of one model.
|
|
87
|
+
def together(field_def) = Lighthouse::Support::Memo.on(field_def, :searched_together) { nil }
|
|
88
|
+
|
|
89
|
+
def together!(field_def, models)
|
|
90
|
+
field_def.instance_variable_set(:@lighthouse_searched_together, models.freeze)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The adapter that searches `models` together (`searchable_together?` each of them, and
|
|
94
|
+
# `together_error`), or nil when none does.
|
|
95
|
+
def adapter_together(models)
|
|
96
|
+
adapter = self.adapter
|
|
97
|
+
return adapter.adapter_together(models) if adapter.respond_to?(:adapter_together)
|
|
98
|
+
|
|
99
|
+
adapter if adapter.respond_to?(:together_error) && models.all? { |model| adapter.searchable_together?(model) }
|
|
100
|
+
end
|
|
78
101
|
end
|
data/sig/lighthouse/search.rbs
CHANGED
|
@@ -21,9 +21,18 @@ module Lighthouse
|
|
|
21
21
|
# Does the field have an argument or input field with @search? Walked once per field.
|
|
22
22
|
def self.searching?: (_FieldDefinition field_def) -> bool
|
|
23
23
|
|
|
24
|
+
# The models the field searches together (a union's or an interface's), kept when the schema is built; nil for one model.
|
|
25
|
+
def self.together: (_FieldDefinition field_def) -> Array[Class]?
|
|
26
|
+
def self.together!: (_FieldDefinition field_def, Array[Class] models) -> Array[Class]
|
|
27
|
+
# The adapter that searches the models together, or nil.
|
|
28
|
+
def self.adapter_together: (Array[Class] models) -> _TogetherSearchAdapter?
|
|
29
|
+
|
|
24
30
|
# What a search holds: the model, the term and what narrows it.
|
|
25
31
|
class Query
|
|
26
|
-
|
|
32
|
+
# nil for models searched together.
|
|
33
|
+
attr_reader model: Class?
|
|
34
|
+
# The models searched together, or nil for a search of one.
|
|
35
|
+
attr_reader models: Array[Class]?
|
|
27
36
|
attr_reader term: String
|
|
28
37
|
attr_reader index: String?
|
|
29
38
|
# [column, value] pairs, from @eq, all of which hold.
|
|
@@ -33,8 +42,12 @@ module Lighthouse
|
|
|
33
42
|
# :with, :only or :without, from @trashed; nil: not given.
|
|
34
43
|
attr_accessor trashed: (:with | :only | :without)?
|
|
35
44
|
|
|
36
|
-
def initialize: (Class model, String term, ?String? index) -> void
|
|
45
|
+
def initialize: (Class? model, String term, ?String? index, ?models: Array[Class]?) -> void
|
|
37
46
|
def where: (String | Symbol column, untyped value) -> self
|
|
47
|
+
# The model's name, or the names of the models searched together.
|
|
48
|
+
def name: () -> String
|
|
49
|
+
# The same search of one model.
|
|
50
|
+
def for: (Class model) -> Query
|
|
38
51
|
end
|
|
39
52
|
|
|
40
53
|
# A search an engine answers: the page it is asked for and its total.
|
|
@@ -74,6 +87,7 @@ module Lighthouse
|
|
|
74
87
|
# Searchkick (Elasticsearch / OpenSearch), outside the database.
|
|
75
88
|
class Searchkick
|
|
76
89
|
include _EngineSearchAdapter
|
|
90
|
+
include _TogetherSearchAdapter
|
|
77
91
|
|
|
78
92
|
DEFAULT_TRASHED_COLUMN: String
|
|
79
93
|
DEEP_PAGING_WINDOW: Integer
|
|
@@ -114,9 +128,14 @@ module Lighthouse
|
|
|
114
128
|
# pg_search (PostgreSQL full-text search), in the database.
|
|
115
129
|
class PgSearch
|
|
116
130
|
include _RelationalSearchAdapter
|
|
131
|
+
include _EngineSearchAdapter
|
|
132
|
+
include _TogetherSearchAdapter
|
|
117
133
|
|
|
118
134
|
DEFAULT_SCOPE: Symbol
|
|
119
135
|
|
|
136
|
+
# Models searched together, through multisearch's documents (page), which have no result window.
|
|
137
|
+
def result_window: (Query query) -> nil
|
|
138
|
+
|
|
120
139
|
# Does `index` name one of the model's pg_search scopes?
|
|
121
140
|
def named?: (Class model, String? index) -> bool
|
|
122
141
|
end
|
|
@@ -127,6 +146,8 @@ module Lighthouse
|
|
|
127
146
|
|
|
128
147
|
def initialize: (?Array[_SearchAdapter]? adapters) -> void
|
|
129
148
|
def searchable?: (Class model, ?String? index) -> bool
|
|
149
|
+
# The first adapter that searches every one of the models together, or nil.
|
|
150
|
+
def adapter_together: (Array[Class] models) -> _TogetherSearchAdapter?
|
|
130
151
|
end
|
|
131
152
|
end
|
|
132
153
|
end
|
data/sig/lighthouse.rbs
CHANGED
|
@@ -280,6 +280,14 @@ module Lighthouse
|
|
|
280
280
|
def adapter_for: (Class model, ?String? index) -> _SearchAdapter?
|
|
281
281
|
end
|
|
282
282
|
|
|
283
|
+
# An adapter that searches models together (a union's or an interface's): its `page` (as
|
|
284
|
+
# _EngineSearchAdapter's) answers a Search::Query with `models`, and `together_error(model)`
|
|
285
|
+
# why one cannot be searched with others as it is (a schema error).
|
|
286
|
+
interface _TogetherSearchAdapter
|
|
287
|
+
def searchable_together?: (Class model) -> boolish
|
|
288
|
+
def together_error: (Class model) -> String?
|
|
289
|
+
end
|
|
290
|
+
|
|
283
291
|
# What DirectiveRegistry#register_manipulator takes: it transforms the SDL
|
|
284
292
|
# before the schema is built.
|
|
285
293
|
interface _SdlManipulator
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lighthouse-graphql
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vaz Innovation
|
|
@@ -245,6 +245,7 @@ files:
|
|
|
245
245
|
- lib/lighthouse/search/searchkick.rb
|
|
246
246
|
- lib/lighthouse/search/searchkick_index.rb
|
|
247
247
|
- lib/lighthouse/search/searchkick_options.rb
|
|
248
|
+
- lib/lighthouse/search/together_check.rb
|
|
248
249
|
- lib/lighthouse/server_directives.rb
|
|
249
250
|
- lib/lighthouse/subscription.rb
|
|
250
251
|
- lib/lighthouse/support/argument_walk.rb
|