@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 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
@@ -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,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
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
@@ -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"}
@@ -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,EAiBL,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;AAmHD;;;;;;;;;;;;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"}
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; a surfaced (`output`) inline reference survives, but its own
36
- * internal helper fields are pruned from the nested document. That keeps the
37
- * *a field without a role reaches neither the engine nor the API* invariant
38
- * true at every depth of the reference graph, not just at the root.
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 ref = field.ref;
52
- if (ref?.strategy !== 'inline') {
53
- continue;
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 (referenceType === undefined || nested === undefined) {
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, referenceType, schema);
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, referenceType, schema, context))
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 is longer than {@link MAX_JOIN_DEPTH}. */
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
- /** The `on` path does not resolve: a name that is not a field, a reference
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
@@ -1 +1 @@
1
- {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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"}
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
- if (on.length > MAX_JOIN_DEPTH) {
112
- issues.push({ part: 'where', field, reason: 'join-too-deep' });
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
- const target = joins?.resolve(searchType, on);
116
- if (target === undefined) {
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
- if (filterOperator(criterion) !== 'in') {
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
- if (declared === undefined) {
137
- issues.push({ part: 'where', field, reason: 'unknown-field' });
138
- }
139
- else if (declared.filterable !== true) {
140
- issues.push({ part: 'where', field, reason: 'not-filterable' });
141
- }
142
- else if (filterOperatorFor(declared.kind) !== filterOperator(criterion)) {
143
- issues.push({
144
- part: 'where',
145
- field,
146
- reason: 'operator-mismatch',
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: each key names a `lookup` reference on the
192
- * type that level belongs to, each `fields` entry an `output` field of the
193
- * target it resolves against, and each nested `resolve` repeats against that
194
- * target. A level whose reference is unresolvable stops there – its subtree
195
- * belongs to a target that does not exist, so reporting it too would bury the
196
- * one mistake under its consequences.
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;