@lde/search 0.23.2 → 0.24.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.
- package/dist/adapter.d.ts +3 -3
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +130 -17
- package/dist/query.d.ts +84 -2
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +145 -44
- package/dist/schema.d.ts +147 -15
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +419 -66
- package/package.json +2 -2
package/dist/adapter.d.ts
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* deployment author declaring a schema and running searches needs none of
|
|
7
7
|
* this; the main entry point carries that (small) authoring surface.
|
|
8
8
|
*/
|
|
9
|
-
export { assertTypeInSchema, physicalFields, irAlias, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, isInlineReference, nestedReferenceType, nestedFieldName, referenceFields, referenceTypeNamed, rootTypeNamed, inlineFramingDepth, fieldNamed, datasetField, ID_FIELD, AND_KEY, OR_KEY, labelFieldOf, labelFieldNameOf, labelSourceNameOf, documentKeyOf, DEFAULT_LABEL_FIELD, isRangeFacet, isAbsoluteIri, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
|
|
9
|
+
export { assertTypeInSchema, physicalFields, irAlias, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, isInlineReference, nestedReferenceType, nestedFieldName, localLookupTypeOf, identityFieldOf, identityFieldName, referenceFields, referenceTypeNamed, rootTypeNamed, inlineFramingDepth, fieldNamed, datasetField, ID_FIELD, AND_KEY, OR_KEY, labelFieldOf, labelFieldNameOf, labelSourceNameOf, labelTargetNameOf, documentKeyOf, DEFAULT_LABEL_FIELD, isRangeFacet, isAbsoluteIri, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
|
|
10
10
|
export type { PhysicalFields } from './schema.js';
|
|
11
11
|
export { physicalNameTokens } from './physical-name.js';
|
|
12
12
|
export { joinGraph, MAX_JOIN_DEPTH } from './join-graph.js';
|
|
13
13
|
export type { JoinGraph } from './join-graph.js';
|
|
14
|
-
export { filterOperatorFor, filterOperator, filterOn, isUnsatisfiable, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
|
|
15
|
-
export type { FilterOperator, QueryIssue } from './query.js';
|
|
14
|
+
export { filterOperatorFor, filterOperator, filterOn, isWelded, isUnsatisfiable, validateQuery, assertValidQuery, resolvePath, pageForOffset, } from './query.js';
|
|
15
|
+
export type { FilterOperator, QueryIssue, ResolvedPath, PathFailure, } from './query.js';
|
|
16
16
|
//# sourceMappingURL=adapter.d.ts.map
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,OAAO,EACP,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,OAAO,EACP,MAAM,EACN,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC5D,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,QAAQ,EACR,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,OAAO,EACP,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,OAAO,EACP,MAAM,EACN,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACb,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC5D,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,QAAQ,EACR,QAAQ,EACR,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,cAAc,EACd,UAAU,EACV,YAAY,EACZ,WAAW,GACZ,MAAM,YAAY,CAAC"}
|
package/dist/adapter.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* deployment author declaring a schema and running searches needs none of
|
|
7
7
|
* this; the main entry point carries that (small) authoring surface.
|
|
8
8
|
*/
|
|
9
|
-
export { assertTypeInSchema, physicalFields, irAlias, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, isInlineReference, nestedReferenceType, nestedFieldName, referenceFields, referenceTypeNamed, rootTypeNamed, inlineFramingDepth, fieldNamed, datasetField, ID_FIELD, AND_KEY, OR_KEY, labelFieldOf, labelFieldNameOf, labelSourceNameOf, documentKeyOf, DEFAULT_LABEL_FIELD, isRangeFacet, isAbsoluteIri, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
|
|
9
|
+
export { assertTypeInSchema, physicalFields, irAlias, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, isInlineReference, nestedReferenceType, nestedFieldName, localLookupTypeOf, identityFieldOf, identityFieldName, referenceFields, referenceTypeNamed, rootTypeNamed, inlineFramingDepth, fieldNamed, datasetField, ID_FIELD, AND_KEY, OR_KEY, labelFieldOf, labelFieldNameOf, labelSourceNameOf, labelTargetNameOf, documentKeyOf, DEFAULT_LABEL_FIELD, isRangeFacet, isAbsoluteIri, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
|
|
10
10
|
export { physicalNameTokens } from './physical-name.js';
|
|
11
11
|
export { joinGraph, MAX_JOIN_DEPTH } from './join-graph.js';
|
|
12
|
-
export { filterOperatorFor, filterOperator, filterOn, isUnsatisfiable, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
|
|
12
|
+
export { filterOperatorFor, filterOperator, filterOn, isWelded, isUnsatisfiable, validateQuery, assertValidQuery, resolvePath, pageForOffset, } from './query.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type { FieldKind, SearchField, SearchFieldBase, TextField, KeywordField,
|
|
|
5
5
|
export { joinGraph, MAX_JOIN_DEPTH } from './join-graph.js';
|
|
6
6
|
export type { JoinGraph } from './join-graph.js';
|
|
7
7
|
export { filterOn } from './query.js';
|
|
8
|
-
export type { SearchQuery, ReferenceProjection, Filter, Criterion, CriterionBase, Sort, } from './query.js';
|
|
8
|
+
export type { SearchQuery, ReferenceProjection, Filter, Criterion, CriterionBase, WeldedCriterion, Sort, } from './query.js';
|
|
9
9
|
export type { SearchEngine, SearchResult, SearchHit, ResultDocument, SearchValue, LocalizedValue, Reference, NestedDocument, FacetBucket, FacetMap, FacetsOutcome, FacetFieldsOf, OutputFieldsOf, } from './engine.js';
|
|
10
10
|
export type { FramedNode } from './frame-by-type.js';
|
|
11
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,cAAc,GACf,MAAM,cAAc,CAAC;AAKtB,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,qBAAqB,EACrB,QAAQ,EACR,OAAO,EACP,MAAM,EAMN,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,SAAS,EACT,WAAW,EACX,eAAe,EACf,SAAS,EACT,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,cAAc,EACd,QAAQ,EACR,aAAa,EACb,UAAU,EACV,eAAe,EACf,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,UAAU,EACV,eAAe,GAChB,MAAM,aAAa,CAAC;AAKrB,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC5D,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAOjD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EACV,WAAW,EACX,mBAAmB,EACnB,MAAM,EACN,SAAS,EACT,aAAa,EACb,IAAI,GACL,MAAM,YAAY,CAAC;AAIpB,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,cAAc,EACd,WAAW,EACX,cAAc,EACd,SAAS,EACT,cAAc,EACd,WAAW,EACX,QAAQ,EACR,aAAa,EACb,aAAa,EACb,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,cAAc,GACf,MAAM,cAAc,CAAC;AAKtB,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,qBAAqB,EACrB,QAAQ,EACR,OAAO,EACP,MAAM,EAMN,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,SAAS,EACT,WAAW,EACX,eAAe,EACf,SAAS,EACT,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,cAAc,EACd,QAAQ,EACR,aAAa,EACb,UAAU,EACV,eAAe,EACf,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,UAAU,EACV,eAAe,GAChB,MAAM,aAAa,CAAC;AAKrB,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC5D,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAOjD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EACV,WAAW,EACX,mBAAmB,EACnB,MAAM,EACN,SAAS,EACT,aAAa,EACb,eAAe,EACf,IAAI,GACL,MAAM,YAAY,CAAC;AAIpB,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,cAAc,EACd,WAAW,EACX,cAAc,EACd,SAAS,EACT,cAAc,EACd,WAAW,EACX,QAAQ,EACR,aAAa,EACb,aAAa,EACb,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/project.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,
|
|
1
|
+
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAkBL,KAAK,eAAe,EAEpB,KAAK,QAAQ,EAEb,KAAK,YAAY,EACjB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEpD,+DAA+D;AAC/D,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CAAC;AAE5D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,iBAAkB,SAAQ,OAAO,CAChD,QAAQ,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAC1C;IACC,4CAA4C;IAC5C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,UAAU,EACtB,MAAM,CAAC,EAAE,YAAY,EACrB,OAAO,GAAE,iBAAyC,GACjD,cAAc,CAgBhB;AAsHD;;;;;;;;;;;;GAYG;AACH,wBAAuB,YAAY,CACjC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,QAAQ,EACpB,OAAO,GAAE,iBAAyC,GACjD,aAAa,CAAC,cAAc,CAAC,CAc/B"}
|
package/dist/project.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fold } from '@lde/text-normalization';
|
|
2
2
|
import { buildSubjectIndex, frameSubjects, } from './frame-by-type.js';
|
|
3
|
-
import { assertTypeInSchema, displayFieldName, documentKeyOf, fieldNamed, inheritedFacetKeys, inlineFramingDepth, irAlias, isAbsoluteIri, isInternalField, isInlineReference, isoToUnixSeconds, labelSourceNameOf, physicalFields, referenceTypeNamed, rootTypeNamed, } from './schema.js';
|
|
3
|
+
import { assertTypeInSchema, displayFieldName, documentKeyOf, fieldNamed, inheritedFacetKeys, inlineFramingDepth, irAlias, isAbsoluteIri, isInternalField, isInlineReference, isoToUnixSeconds, labelSourceNameOf, localLookupTypeOf, physicalFields, referenceTypeNamed, rootTypeNamed, } from './schema.js';
|
|
4
4
|
/** The context a projection runs with when its caller supplied none. */
|
|
5
5
|
const NO_PROJECTION_CONTEXT = {};
|
|
6
6
|
/**
|
|
@@ -32,10 +32,16 @@ export function projectDocument(node, searchType, schema, context = NO_PROJECTIO
|
|
|
32
32
|
* only after all projection – so every derive that might read an internal
|
|
33
33
|
* field, at any depth, has already run – which makes this single post-order
|
|
34
34
|
* pass safe. A no-role inline reference is itself internal, so it is deleted
|
|
35
|
-
* whole here;
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* whole here; one that carries a Role survives, but its own internal helper
|
|
36
|
+
* fields are pruned from the nested document. That keeps the *a field without a
|
|
37
|
+
* role reaches neither the engine nor the API* invariant true at every depth of
|
|
38
|
+
* the reference graph, not just at the root.
|
|
39
|
+
*
|
|
40
|
+
* Both kinds of nesting are walked, because both store projected nodes: an
|
|
41
|
+
* inline reference’s Reference Type, and the Root Type a
|
|
42
|
+
* {@link ReferenceStrategy.local local} lookup projects its endpoint through.
|
|
43
|
+
* The second matters more than it looks – a Root Type’s reading-device fields
|
|
44
|
+
* exist for its OWN derives, and nothing in a referring document wants them.
|
|
39
45
|
*/
|
|
40
46
|
function pruneInternalFields(document, searchType, schema) {
|
|
41
47
|
for (const field of searchType.fields) {
|
|
@@ -43,22 +49,18 @@ function pruneInternalFields(document, searchType, schema) {
|
|
|
43
49
|
delete document[field.name];
|
|
44
50
|
continue;
|
|
45
51
|
}
|
|
46
|
-
// A surfaced inline reference nests its referent(s) as projected node(s);
|
|
47
|
-
// prune those too, by their reference type.
|
|
48
52
|
if (schema === undefined || field.kind !== 'reference') {
|
|
49
53
|
continue;
|
|
50
54
|
}
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
const referenceType = referenceTypeNamed(schema, ref.typeName);
|
|
55
|
+
const nestedType = field.ref?.strategy === 'inline'
|
|
56
|
+
? referenceTypeNamed(schema, field.ref.typeName)
|
|
57
|
+
: localLookupTypeOf(field, schema);
|
|
56
58
|
const nested = document[field.name];
|
|
57
|
-
if (
|
|
59
|
+
if (nestedType === undefined || nested === undefined) {
|
|
58
60
|
continue;
|
|
59
61
|
}
|
|
60
62
|
for (const referent of Array.isArray(nested) ? nested : [nested]) {
|
|
61
|
-
pruneInternalFields(referent,
|
|
63
|
+
pruneInternalFields(referent, nestedType, schema);
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
}
|
|
@@ -203,6 +205,22 @@ function applyField(document, node, field, searchType, schema, context) {
|
|
|
203
205
|
}
|
|
204
206
|
return;
|
|
205
207
|
}
|
|
208
|
+
// A `local` lookup stores nested documents too, shaped by the TARGET’s own
|
|
209
|
+
// declaration rather than by a reference type – so an unidentified referent
|
|
210
|
+
// is an entry like any other, minus its `id`. Same reason as above for
|
|
211
|
+
// needing a schema: without one there is no target to project through.
|
|
212
|
+
if (field.kind === 'reference' && schema !== undefined) {
|
|
213
|
+
const localType = localLookupTypeOf(field, schema);
|
|
214
|
+
if (localType !== undefined) {
|
|
215
|
+
const endpoints = applyNestedReferents(document, valuesOf(node, alias), field, localType, schema, context);
|
|
216
|
+
// A `local` lookup stores an object, so its own id is a level deeper than
|
|
217
|
+
// a filter can reach – an engine welds conditions on an entry's LEAF
|
|
218
|
+
// fields only. `filterable` therefore fans out the id beside the object,
|
|
219
|
+
// exactly as an inline reference's identity companion does.
|
|
220
|
+
applyLocalIdentity(document, endpoints, field, schema);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
206
224
|
switch (field.kind) {
|
|
207
225
|
case 'text':
|
|
208
226
|
return applyText(document, langValuesOf(node, alias), field);
|
|
@@ -430,9 +448,103 @@ function applyInlineReference(document, node, alias, field, schema, context) {
|
|
|
430
448
|
if (referenceType === undefined) {
|
|
431
449
|
return;
|
|
432
450
|
}
|
|
433
|
-
const referents = valuesOf(node, alias)
|
|
451
|
+
const referents = applyNestedReferents(document, valuesOf(node, alias), field, referenceType, schema, context);
|
|
452
|
+
applyIdentityCompanion(document, referents, field, schema);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Write the {@link ReferenceStrategy.identity identity companion} of an inline
|
|
456
|
+
* reference: the ids its entries reference, flattened out of the nested
|
|
457
|
+
* documents just projected, under the flat physical name an engine filters and
|
|
458
|
+
* facets ({@link physicalFields}).
|
|
459
|
+
*
|
|
460
|
+
* Harvested from the entries rather than read from a second path, so what the
|
|
461
|
+
* companion holds is exactly what the entries hold – including the re-keying
|
|
462
|
+
* the nested reference already applied, which a separately-read path would have
|
|
463
|
+
* to repeat and could get wrong.
|
|
464
|
+
*
|
|
465
|
+
* The identity field stores either bare ids or, when it is a
|
|
466
|
+
* {@link ReferenceStrategy.local local} lookup, nested documents carrying an
|
|
467
|
+
* `id`. Both are read here, because which one a deployment declares is about
|
|
468
|
+
* how much of the referent it wants to display – not about what identifies it.
|
|
469
|
+
*/
|
|
470
|
+
function applyIdentityCompanion(document, referents, field, schema) {
|
|
471
|
+
const names = physicalFields(field, schema);
|
|
472
|
+
if (names.identity === undefined) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const identity = field.ref.identity;
|
|
476
|
+
// Only the entries the document actually STORES: a single-valued reference
|
|
477
|
+
// keeps the first referent and drops the rest, and a companion holding an id
|
|
478
|
+
// from a dropped one would match a filter whose hit shows no such entry.
|
|
479
|
+
const stored = field.array === true ? referents : referents.slice(0, 1);
|
|
480
|
+
const ids = dedupe(stored.flatMap((referent) => (Array.isArray(referent[identity])
|
|
481
|
+
? referent[identity]
|
|
482
|
+
: [referent[identity]])
|
|
483
|
+
.map(identityValue)
|
|
484
|
+
.filter((id) => id !== undefined)));
|
|
485
|
+
if (ids.length === 0) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
setArray(document, names.identity, ids);
|
|
489
|
+
// Same rule as every other facetable reference: where the target declares a
|
|
490
|
+
// facet policy, the facet reads a narrowed companion of its own, so an
|
|
491
|
+
// excluded id is never seen by the engine rather than merely unlabelled.
|
|
492
|
+
const policy = inheritedFacetKeys(field, schema);
|
|
493
|
+
if (policy !== undefined) {
|
|
494
|
+
setArray(document, names.facet, ids.filter(policy.only));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Write the identity companion of a {@link ReferenceStrategy.local local}
|
|
499
|
+
* lookup: the ids of the endpoints just stored, under the flat physical name an
|
|
500
|
+
* engine filters on ({@link physicalFields}).
|
|
501
|
+
*
|
|
502
|
+
* Nothing to do where the field is not `filterable`: without that Role there is
|
|
503
|
+
* no companion, and the endpoint is display weight like every other nesting.
|
|
504
|
+
*/
|
|
505
|
+
function applyLocalIdentity(document, endpoints, field, schema) {
|
|
506
|
+
const names = physicalFields(field, schema);
|
|
507
|
+
if (names.identity === undefined) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// Only the endpoints the field actually STORES: a single-valued reference
|
|
511
|
+
// keeps the first and drops the rest, and a companion holding an id from a
|
|
512
|
+
// dropped one would match a filter whose hit shows a different endpoint.
|
|
513
|
+
const stored = field.array === true ? endpoints : endpoints.slice(0, 1);
|
|
514
|
+
const ids = dedupe(stored
|
|
515
|
+
.map((endpoint) => endpoint.id)
|
|
516
|
+
.filter((id) => typeof id === 'string'));
|
|
517
|
+
if (ids.length > 0) {
|
|
518
|
+
setArray(document, names.identity, ids);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/** The id a value under an identity field carries: the value itself when the
|
|
522
|
+
* field stores bare ids, its `id` when it stores nested documents. */
|
|
523
|
+
function identityValue(value) {
|
|
524
|
+
if (typeof value === 'string') {
|
|
525
|
+
return value;
|
|
526
|
+
}
|
|
527
|
+
const id = isObject(value) ? value.id : undefined;
|
|
528
|
+
return typeof id === 'string' ? id : undefined;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Project raw framed values as nested document(s) of `nestedType` and attach
|
|
532
|
+
* them under the field’s name – the shared body of an inline reference and a
|
|
533
|
+
* {@link ReferenceStrategy.local local} lookup, which differ only in which type
|
|
534
|
+
* shapes the referent: a declared Reference Type for the one, the lookup’s own
|
|
535
|
+
* target Root Type for the other. Sharing it is what makes the two reconstruct
|
|
536
|
+
* through one path, so a consumer cannot tell a nested referent from a resolved
|
|
537
|
+
* one.
|
|
538
|
+
*
|
|
539
|
+
* Projecting through a Root Type also gives a `local` referent its `id` for
|
|
540
|
+
* free, re-keyed through that type’s {@link RootType.key} exactly as a
|
|
541
|
+
* top-level document of that type would be – so the stored id is the one the
|
|
542
|
+
* target’s collection files it under, and the lookup can find it.
|
|
543
|
+
*/
|
|
544
|
+
function applyNestedReferents(document, values, field, nestedType, schema, context) {
|
|
545
|
+
const referents = values
|
|
434
546
|
.filter(isObject)
|
|
435
|
-
.map((referent) => projectFields(referent,
|
|
547
|
+
.map((referent) => projectFields(referent, nestedType, schema, context))
|
|
436
548
|
// Fields, not identity, are what makes something a referent: a literal
|
|
437
549
|
// value object under the alias (dirty source data), or a node this
|
|
438
550
|
// reference type reads nothing from, projects nothing and is no referent.
|
|
@@ -440,9 +552,10 @@ function applyInlineReference(document, node, alias, field, schema, context) {
|
|
|
440
552
|
// single-valued reference, let it win the slot over a real referent.
|
|
441
553
|
.filter((referent) => Object.keys(referent).length > 0);
|
|
442
554
|
if (referents.length === 0) {
|
|
443
|
-
return;
|
|
555
|
+
return referents;
|
|
444
556
|
}
|
|
445
557
|
document[field.name] = field.array === true ? referents : referents[0];
|
|
558
|
+
return referents;
|
|
446
559
|
}
|
|
447
560
|
function langValuesOf(node, key) {
|
|
448
561
|
return valuesOf(node, key)
|
package/dist/query.d.ts
CHANGED
|
@@ -95,7 +95,33 @@ export type Criterion = (CriterionBase & {
|
|
|
95
95
|
};
|
|
96
96
|
}) | (CriterionBase & {
|
|
97
97
|
readonly is: boolean;
|
|
98
|
+
}) | (CriterionBase & {
|
|
99
|
+
readonly entry: readonly Criterion[];
|
|
98
100
|
});
|
|
101
|
+
/**
|
|
102
|
+
* The conditions a criterion welds to **one entry** of the nested reference its
|
|
103
|
+
* {@link CriterionBase.field} names – *this endpoint in this role*, rather than
|
|
104
|
+
* this endpoint somewhere and that role somewhere.
|
|
105
|
+
*
|
|
106
|
+
* Welding is the whole reason an edge is worth indexing. Stated as two ordinary
|
|
107
|
+
* criteria, `role = etser` and `creator = rembrandt` are satisfied by
|
|
108
|
+
* *different* entries of the same document, which silently answers a wider
|
|
109
|
+
* question – the parallel-array defect nesting exists to remove.
|
|
110
|
+
*
|
|
111
|
+
* It stays an **atom**, so `where` remains the flat conjunction of disjunctions
|
|
112
|
+
* of [ADR 18](../../docs/decisions/0018-filter-across-several-fields-with-one-clause.md):
|
|
113
|
+
* the conjunction lives *inside* one criterion rather than nesting clause
|
|
114
|
+
* structure, and skip-own-filter still scans one level and finds one field.
|
|
115
|
+
*
|
|
116
|
+
* Each welded condition names a field of the reference type and carries no
|
|
117
|
+
* `on` of its own: they are all conditions on the same entry, which is what
|
|
118
|
+
* welding means.
|
|
119
|
+
*/
|
|
120
|
+
export type WeldedCriterion = CriterionBase & {
|
|
121
|
+
readonly entry: readonly Criterion[];
|
|
122
|
+
};
|
|
123
|
+
/** Whether a criterion welds conditions to one entry ({@link WeldedCriterion}). */
|
|
124
|
+
export declare function isWelded(criterion: Criterion): criterion is WeldedCriterion;
|
|
99
125
|
/**
|
|
100
126
|
* One `where` clause: a disjunction, matching a document that satisfies **any**
|
|
101
127
|
* of its criteria. An ordinary single-field filter is the one-criterion case,
|
|
@@ -169,13 +195,32 @@ export interface QueryIssue {
|
|
|
169
195
|
* the bound of an `unparseable-bound`. Absent on every other reason. */
|
|
170
196
|
readonly value?: string;
|
|
171
197
|
readonly reason: 'unknown-field' | 'not-filterable' | 'operator-mismatch' | 'not-facetable'
|
|
172
|
-
/** The criterion’s `on` path
|
|
198
|
+
/** The criterion’s `on` path crosses more than {@link MAX_JOIN_DEPTH}
|
|
199
|
+
* *joins*. Nested hops cost no round trip and do not count. */
|
|
173
200
|
| 'join-too-deep'
|
|
174
|
-
/**
|
|
201
|
+
/** A join hop does not resolve: a name that is not a field, a reference
|
|
175
202
|
* that is not `joinable`, or no join graph to resolve it against. */
|
|
176
203
|
| 'unknown-join'
|
|
204
|
+
/**
|
|
205
|
+
* A hop *inside* an inline reference’s entries does not walk into a nested
|
|
206
|
+
* reference – an unknown name, or a field that is not one. Reported apart
|
|
207
|
+
* from `unknown-join` so “this field is not joinable” and “this field is
|
|
208
|
+
* not nested” are not one message.
|
|
209
|
+
*
|
|
210
|
+
* Joining out of a nesting lands here too, deliberately: a join addresses
|
|
211
|
+
* another collection and there is none to address from inside an array
|
|
212
|
+
* element, so from in here a joinable name is as unwalkable as an unknown
|
|
213
|
+
* one. A join may contain a nesting; never the reverse.
|
|
214
|
+
*/
|
|
215
|
+
| 'unknown-nesting'
|
|
177
216
|
/** A projected reference is not a `lookup`, so nothing resolves it. */
|
|
178
217
|
| 'not-resolvable'
|
|
218
|
+
/**
|
|
219
|
+
* A criterion welds conditions to one entry of a field that has no entries
|
|
220
|
+
* – it is not an inline reference. Welding means *one entry satisfies all
|
|
221
|
+
* of these*, so there has to be something to weld them to.
|
|
222
|
+
*/
|
|
223
|
+
| 'not-weldable'
|
|
179
224
|
/**
|
|
180
225
|
* A `date` range bound the storage codec ({@link isoToUnixSeconds}) cannot
|
|
181
226
|
* read, so there is no stored value to compare against – reported per
|
|
@@ -188,6 +233,43 @@ export interface QueryIssue {
|
|
|
188
233
|
*/
|
|
189
234
|
| 'unparseable-bound';
|
|
190
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* A resolved {@link CriterionBase.on} path: which type the leaf field belongs
|
|
238
|
+
* to, and how the criterion reaches it.
|
|
239
|
+
*
|
|
240
|
+
* The two hop kinds are genuinely different operations, so they are separated
|
|
241
|
+
* here once rather than re-derived by every adapter. A **join** hop crosses
|
|
242
|
+
* into another collection and costs a round trip; a **nested** hop stays inside
|
|
243
|
+
* the same document and costs nothing, walking into an inline reference’s
|
|
244
|
+
* entries.
|
|
245
|
+
*/
|
|
246
|
+
export interface ResolvedPath {
|
|
247
|
+
/** The type the criterion’s leaf field is declared by. */
|
|
248
|
+
readonly leafType: SearchType;
|
|
249
|
+
/** The joinable references to cross, in order. Counts against
|
|
250
|
+
* {@link MAX_JOIN_DEPTH}; a nested hop deliberately does not. */
|
|
251
|
+
readonly joinPath: readonly string[];
|
|
252
|
+
/** The inline references to walk into, in order, after the joins. */
|
|
253
|
+
readonly nestedPath: readonly string[];
|
|
254
|
+
}
|
|
255
|
+
/** Why an {@link CriterionBase.on} path did not resolve. */
|
|
256
|
+
export type PathFailure = 'join-too-deep' | 'unknown-join' | 'unknown-nesting';
|
|
257
|
+
/**
|
|
258
|
+
* Resolve an {@link CriterionBase.on} path against the schema, classifying each
|
|
259
|
+
* hop by what the declaration says it is: a {@link ReferenceField.joinable}
|
|
260
|
+
* reference is a join, an inline reference is a nesting.
|
|
261
|
+
*
|
|
262
|
+
* **Nested hops must come last.** A join is a clause about another collection
|
|
263
|
+
* and a nesting is a clause about a field of this document, so a join can
|
|
264
|
+
* contain a nesting (`$people(…)` around `edge.role`) but a nesting cannot
|
|
265
|
+
* contain a join – there is no collection to address from inside an array
|
|
266
|
+
* element. Once inside a nesting, therefore, a joinable name is as unwalkable
|
|
267
|
+
* as an unknown one and both report `unknown-nesting`.
|
|
268
|
+
*
|
|
269
|
+
* Only join hops count against {@link MAX_JOIN_DEPTH}: the cap exists to bound
|
|
270
|
+
* round trips, and a nesting hop costs none.
|
|
271
|
+
*/
|
|
272
|
+
export declare function resolvePath(searchType: SearchType, path: readonly string[], schema: SearchSchema, joins?: JoinGraph): ResolvedPath | PathFailure;
|
|
191
273
|
/**
|
|
192
274
|
* Structurally validate a query against its search type: **every criterion of
|
|
193
275
|
* every `where` clause** names a declared, `filterable` field whose kind accepts
|
package/dist/query.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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"}
|
package/dist/query.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { fieldNamed, filterOperatorFor, ID_FIELD, isoToUnixSeconds, } from './schema.js';
|
|
1
|
+
import { fieldNamed, filterOperatorFor, ID_FIELD, isoToUnixSeconds, nestedReferenceType, } from './schema.js';
|
|
2
2
|
import { MAX_JOIN_DEPTH } from './join-graph.js';
|
|
3
|
+
/** Whether a criterion welds conditions to one entry ({@link WeldedCriterion}). */
|
|
4
|
+
export function isWelded(criterion) {
|
|
5
|
+
return 'entry' in criterion;
|
|
6
|
+
}
|
|
3
7
|
// The kind→operator table lives with the field model in schema.ts (one source
|
|
4
8
|
// for the query IR, the surfaces, the adapters AND declaration validation);
|
|
5
9
|
// re-exported here so the query-IR module stays the natural import site.
|
|
@@ -50,6 +54,96 @@ export function isUnsatisfiable(query) {
|
|
|
50
54
|
criterion.in.length === 0);
|
|
51
55
|
});
|
|
52
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* The checks every leaf criterion answers to, wherever it sits: the field is
|
|
59
|
+
* declared and `filterable` on the type it constrains, its operator matches
|
|
60
|
+
* that field's kind, and a `date` range's bounds are readable.
|
|
61
|
+
*
|
|
62
|
+
* Shared by an ordinary criterion and by each condition welded to an entry, so
|
|
63
|
+
* a welded condition is held to exactly the same rules – there is no second,
|
|
64
|
+
* weaker validator for the inside of an edge. `field` is the path to report
|
|
65
|
+
* under, which differs between the two.
|
|
66
|
+
*/
|
|
67
|
+
function collectLeafIssues(criterion, constrained, field, issues) {
|
|
68
|
+
const declared = fieldNamed(constrained, criterion.field);
|
|
69
|
+
if (declared === undefined) {
|
|
70
|
+
issues.push({ part: 'where', field, reason: 'unknown-field' });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (declared.filterable !== true) {
|
|
74
|
+
issues.push({ part: 'where', field, reason: 'not-filterable' });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (filterOperatorFor(declared.kind) !== filterOperator(criterion)) {
|
|
78
|
+
issues.push({ part: 'where', field, reason: 'operator-mismatch' });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (declared.kind === 'date' && 'range' in criterion) {
|
|
82
|
+
// A `date` bound is ISO 8601 at the edge and Unix seconds in storage, so a
|
|
83
|
+
// bound the codec cannot read has nothing to compare against. Both bounds
|
|
84
|
+
// are checked, so a caller who wrote two bad ones is told about both
|
|
85
|
+
// instead of fixing one to be told about the other.
|
|
86
|
+
for (const bound of [criterion.range.min, criterion.range.max]) {
|
|
87
|
+
if (typeof bound === 'string' && isoToUnixSeconds(bound) === undefined) {
|
|
88
|
+
issues.push({
|
|
89
|
+
part: 'where',
|
|
90
|
+
field,
|
|
91
|
+
value: bound,
|
|
92
|
+
reason: 'unparseable-bound',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolve an {@link CriterionBase.on} path against the schema, classifying each
|
|
100
|
+
* hop by what the declaration says it is: a {@link ReferenceField.joinable}
|
|
101
|
+
* reference is a join, an inline reference is a nesting.
|
|
102
|
+
*
|
|
103
|
+
* **Nested hops must come last.** A join is a clause about another collection
|
|
104
|
+
* and a nesting is a clause about a field of this document, so a join can
|
|
105
|
+
* contain a nesting (`$people(…)` around `edge.role`) but a nesting cannot
|
|
106
|
+
* contain a join – there is no collection to address from inside an array
|
|
107
|
+
* element. Once inside a nesting, therefore, a joinable name is as unwalkable
|
|
108
|
+
* as an unknown one and both report `unknown-nesting`.
|
|
109
|
+
*
|
|
110
|
+
* Only join hops count against {@link MAX_JOIN_DEPTH}: the cap exists to bound
|
|
111
|
+
* round trips, and a nesting hop costs none.
|
|
112
|
+
*/
|
|
113
|
+
export function resolvePath(searchType, path, schema, joins) {
|
|
114
|
+
const joinPath = [];
|
|
115
|
+
const nestedPath = [];
|
|
116
|
+
let current = searchType;
|
|
117
|
+
for (const name of path) {
|
|
118
|
+
const field = fieldNamed(current, name);
|
|
119
|
+
const nested = field === undefined ? undefined : nestedReferenceType(schema, field);
|
|
120
|
+
if (nested !== undefined) {
|
|
121
|
+
nestedPath.push(name);
|
|
122
|
+
current = nested;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (nestedPath.length > 0) {
|
|
126
|
+
// Already inside a document, where the only hop that means anything is
|
|
127
|
+
// another nesting. A join addresses a collection and there is none to
|
|
128
|
+
// address from inside an array element, so a joinable name here is as
|
|
129
|
+
// unwalkable as an unknown one – one reason covers both.
|
|
130
|
+
return 'unknown-nesting';
|
|
131
|
+
}
|
|
132
|
+
// Checked as the budget is spent, not after the walk: a path over the cap
|
|
133
|
+
// is too deep whether or not its last hop would have resolved, and saying
|
|
134
|
+
// so is more use than reporting the hop that happened to fail first.
|
|
135
|
+
if (joinPath.length === MAX_JOIN_DEPTH) {
|
|
136
|
+
return 'join-too-deep';
|
|
137
|
+
}
|
|
138
|
+
const target = joins?.resolve(current, [name]);
|
|
139
|
+
if (target === undefined) {
|
|
140
|
+
return 'unknown-join';
|
|
141
|
+
}
|
|
142
|
+
joinPath.push(name);
|
|
143
|
+
current = target;
|
|
144
|
+
}
|
|
145
|
+
return { leafType: current, joinPath, nestedPath };
|
|
146
|
+
}
|
|
53
147
|
/** The `field` a joined issue is reported under: the path and the leaf name
|
|
54
148
|
* together, so a message says which hop and which field, not just which
|
|
55
149
|
* field. `dataset.publisher.id` reads the way the criterion was written. */
|
|
@@ -107,23 +201,29 @@ export function validateQuery(query, searchType, schema, joins) {
|
|
|
107
201
|
// supplied none cannot serve a joined criterion at all.
|
|
108
202
|
const on = criterion.on ?? [];
|
|
109
203
|
let constrained = searchType;
|
|
204
|
+
let nested = false;
|
|
110
205
|
if (on.length > 0) {
|
|
111
|
-
|
|
112
|
-
|
|
206
|
+
const resolved = resolvePath(searchType, on, schema, joins);
|
|
207
|
+
if (typeof resolved === 'string') {
|
|
208
|
+
issues.push({ part: 'where', field, reason: resolved });
|
|
113
209
|
continue;
|
|
114
210
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
issues.push({ part: 'where', field, reason: 'unknown-join' });
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
constrained = target;
|
|
211
|
+
constrained = resolved.leafType;
|
|
212
|
+
nested = resolved.nestedPath.length > 0;
|
|
121
213
|
}
|
|
122
214
|
// `id` is filterable on every type without being declared by any: it is
|
|
123
215
|
// the document’s IRI, so the lookup exists wherever documents do.
|
|
124
216
|
// Membership only – an IRI has no range and no truth value.
|
|
125
217
|
if (name === ID_FIELD) {
|
|
126
|
-
|
|
218
|
+
// …but only where documents do. An entry inside a nested reference is
|
|
219
|
+
// read, not addressed: it has no document key, and the `id` a `local`
|
|
220
|
+
// lookup stores is the ENDPOINT’s, which the identity companion is
|
|
221
|
+
// what filters on. Accepting it here would compile to the ROOT
|
|
222
|
+
// document’s `id` and silently answer a different question.
|
|
223
|
+
if (nested) {
|
|
224
|
+
issues.push({ part: 'where', field, reason: 'unknown-nesting' });
|
|
225
|
+
}
|
|
226
|
+
else if (filterOperator(criterion) !== 'in') {
|
|
127
227
|
issues.push({
|
|
128
228
|
part: 'where',
|
|
129
229
|
field,
|
|
@@ -133,36 +233,23 @@ export function validateQuery(query, searchType, schema, joins) {
|
|
|
133
233
|
continue;
|
|
134
234
|
}
|
|
135
235
|
const declared = fieldNamed(constrained, name);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
part: 'where',
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
else if (declared.kind === 'date' && 'range' in criterion) {
|
|
150
|
-
// A `date` bound is ISO 8601 at the edge and Unix seconds in storage,
|
|
151
|
-
// so a bound the codec cannot read has nothing to compare against.
|
|
152
|
-
// Both bounds are checked, so a caller who wrote two bad ones is told
|
|
153
|
-
// about both instead of fixing one to be told about the other.
|
|
154
|
-
for (const bound of [criterion.range.min, criterion.range.max]) {
|
|
155
|
-
if (typeof bound === 'string' &&
|
|
156
|
-
isoToUnixSeconds(bound) === undefined) {
|
|
157
|
-
issues.push({
|
|
158
|
-
part: 'where',
|
|
159
|
-
field,
|
|
160
|
-
value: bound,
|
|
161
|
-
reason: 'unparseable-bound',
|
|
162
|
-
});
|
|
163
|
-
}
|
|
236
|
+
// A welded criterion names the NESTED REFERENCE and constrains fields of
|
|
237
|
+
// the type it nests, so each condition is checked one level in – against
|
|
238
|
+
// that reference type, and reported under the path a consumer wrote.
|
|
239
|
+
if (isWelded(criterion)) {
|
|
240
|
+
const entryType = declared === undefined
|
|
241
|
+
? undefined
|
|
242
|
+
: nestedReferenceType(schema, declared);
|
|
243
|
+
if (entryType === undefined) {
|
|
244
|
+
issues.push({ part: 'where', field, reason: 'not-weldable' });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
for (const welded of criterion.entry) {
|
|
248
|
+
collectLeafIssues(welded, entryType, [...on, name, welded.field].join('.'), issues);
|
|
164
249
|
}
|
|
250
|
+
continue;
|
|
165
251
|
}
|
|
252
|
+
collectLeafIssues(criterion, constrained, field, issues);
|
|
166
253
|
}
|
|
167
254
|
}
|
|
168
255
|
for (const name of query.facets) {
|
|
@@ -188,12 +275,19 @@ export function validateQuery(query, searchType, schema, joins) {
|
|
|
188
275
|
return issues;
|
|
189
276
|
}
|
|
190
277
|
/**
|
|
191
|
-
* Walk a projection level by level
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
278
|
+
* Walk a projection level by level. A level is one of two things, and the
|
|
279
|
+
* difference is what it costs rather than how it is written:
|
|
280
|
+
*
|
|
281
|
+
* - a **lookup** level names a `lookup` reference, resolves against that
|
|
282
|
+
* target’s collection, and costs one batched round trip;
|
|
283
|
+
* - a **nested** level names an inline reference and descends into its entries,
|
|
284
|
+
* costing nothing – the entries are already in the hit. Its `fields` name
|
|
285
|
+
* nothing to fetch, because the entries carry what they carry; only the
|
|
286
|
+
* levels *below* it can ask for anything.
|
|
287
|
+
*
|
|
288
|
+
* A level whose reference is unresolvable stops there: its subtree belongs to a
|
|
289
|
+
* target that does not exist, so reporting it too would bury the one mistake
|
|
290
|
+
* under its consequences.
|
|
197
291
|
*/
|
|
198
292
|
function collectProjectionIssues(projection, searchType, schema, issues) {
|
|
199
293
|
for (const [name, level] of Object.entries(projection ?? {})) {
|
|
@@ -202,6 +296,13 @@ function collectProjectionIssues(projection, searchType, schema, issues) {
|
|
|
202
296
|
issues.push({ part: 'resolve', field: name, reason: 'unknown-field' });
|
|
203
297
|
continue;
|
|
204
298
|
}
|
|
299
|
+
// Descending into an inline reference is free, so a nested level is valid
|
|
300
|
+
// wherever the entries exist – what it is *for* is the lookup below it.
|
|
301
|
+
const nested = nestedReferenceType(schema, field);
|
|
302
|
+
if (nested !== undefined) {
|
|
303
|
+
collectProjectionIssues(level.resolve, nested, schema, issues);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
205
306
|
if (field.kind !== 'reference' || field.ref?.strategy !== 'lookup') {
|
|
206
307
|
issues.push({ part: 'resolve', field: name, reason: 'not-resolvable' });
|
|
207
308
|
continue;
|