@lde/search 0.28.1 → 0.30.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,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, 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';
9
+ export { assertTypeInSchema, physicalFields, irAlias, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, isInlineReference, nestedReferenceType, nestedFieldName, localLookupTargetsOf, referencedTargetsOf, storedTargetOf, TARGET_FIELD, identityFieldOf, identityFieldName, referenceFields, referenceTypeNamed, rootTypeNamed, inlineFramingDepth, fieldNamed, datasetField, ID_FIELD, AND_KEY, OR_KEY, labelFieldOf, labelFieldNameOf, labelSourceNamesOf, labelTargetNamesOf, documentKeyOf, DEFAULT_LABEL_FIELD, isRangeFacet, isBinnedRangeFacet, 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 { matchLanguage } from './language.js';
@@ -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,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,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,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"}
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,oBAAoB,EACpB,mBAAmB,EACnB,cAAc,EACd,YAAY,EACZ,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,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,YAAY,EACZ,kBAAkB,EAClB,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,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,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, 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';
9
+ export { assertTypeInSchema, physicalFields, irAlias, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, isInlineReference, nestedReferenceType, nestedFieldName, localLookupTargetsOf, referencedTargetsOf, storedTargetOf, TARGET_FIELD, identityFieldOf, identityFieldName, referenceFields, referenceTypeNamed, rootTypeNamed, inlineFramingDepth, fieldNamed, datasetField, ID_FIELD, AND_KEY, OR_KEY, labelFieldOf, labelFieldNameOf, labelSourceNamesOf, labelTargetNamesOf, documentKeyOf, DEFAULT_LABEL_FIELD, isRangeFacet, isBinnedRangeFacet, isAbsoluteIri, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
10
10
  export { physicalNameTokens } from './physical-name.js';
11
11
  export { matchLanguage } from './language.js';
12
12
  export { joinGraph, MAX_JOIN_DEPTH } from './join-graph.js';
package/dist/engine.d.ts CHANGED
@@ -41,7 +41,7 @@ export interface SearchEngine<Types extends readonly SearchType[] = readonly Sea
41
41
  * Typesense `multi_search`), returning one {@link FacetsOutcome} per query,
42
42
  * positionally aligned with `queries`. This is what keeps a faceted
43
43
  * listing – one skip-own-filter query variant per filtered facet – from
44
- * fanning out into per-facet engine calls. Only facet buckets come back: an
44
+ * fanning out into per-facet engine calls. Only facets come back: an
45
45
  * engine answers every query in the batch facet-only (as if `limit: 0` and
46
46
  * without ordering, whatever the query carries), so hits are never
47
47
  * transferred. Reference-facet buckets carry resolved labels exactly as in
@@ -56,7 +56,7 @@ export interface SearchEngine<Types extends readonly SearchType[] = readonly Sea
56
56
  }
57
57
  /**
58
58
  * One query’s outcome in a {@link SearchEngine.searchFacets} batch – its
59
- * facet buckets, or the error that failed it. The `allSettled`-style shape
59
+ * facets, or the error that failed it. The `allSettled`-style shape
60
60
  * keeps one failed query (e.g. one failed `multi_search` entry) from
61
61
  * discarding its siblings’ results: a caller degrades exactly the facets of
62
62
  * the failed query. Discriminate with `'error' in outcome`.
@@ -73,10 +73,36 @@ export interface SearchResult<FacetField extends string = string, OutputField ex
73
73
  readonly facets: FacetMap<FacetField>;
74
74
  }
75
75
  /**
76
- * Facet buckets keyed by facet field name. `Partial` because a result carries
77
- * buckets only for the fields the query asked for, not every facetable field.
76
+ * Facets keyed by facet field name. `Partial` because a result carries a facet
77
+ * only for the fields the query asked for, not every facetable field.
78
78
  */
79
- export type FacetMap<FacetField extends string = string> = Readonly<Partial<Record<FacetField, readonly FacetBucket[]>>>;
79
+ export type FacetMap<FacetField extends string = string> = Readonly<Partial<Record<FacetField, Facet>>>;
80
+ /**
81
+ * One facet over the documents a query matches: its buckets and, for a
82
+ * numeric (`integer`/`number`/`date`) field, the extent of the values those
83
+ * documents hold – the bounds a range slider needs.
84
+ *
85
+ * A facet buckets per distinct value, or – a numeric field declaring
86
+ * `facetRanges` – into those bins. Only a numeric facet has an extent.
87
+ */
88
+ export interface Facet {
89
+ readonly buckets: readonly FacetBucket[];
90
+ /**
91
+ * The lowest and highest value among the matching documents, for a numeric
92
+ * facet. Absent on any other facet, and when no matching document holds a
93
+ * value.
94
+ */
95
+ readonly extent?: FacetExtent;
96
+ }
97
+ /**
98
+ * The lowest and highest value a range facet’s documents hold, stored terms
99
+ * like every other value at this seam: a `date` in Unix seconds, which a
100
+ * surface serves as ISO 8601 the way it serves the field itself.
101
+ */
102
+ export interface FacetExtent {
103
+ readonly min: number;
104
+ readonly max: number;
105
+ }
80
106
  /**
81
107
  * The facet-field-name union of a search type – the keys a {@link SearchResult}’s
82
108
  * `facets` can hold. Narrow for a declaration captured as a literal (via
@@ -133,7 +159,22 @@ export type SearchValue = string | number | boolean | readonly string[] | Locali
133
159
  */
134
160
  export interface NestedDocument {
135
161
  readonly [field: string]: SearchValue | undefined;
162
+ /**
163
+ * The `name` of the Root Type this document is read through, where the
164
+ * field that carries it is a `lookup` naming **several** targets – the
165
+ * declaration it was resolved or stored against. Such a lookup serves
166
+ * referents of any of its targets under one field, and this is what tells a
167
+ * surface which one each referent is, so it can type the value per referent
168
+ * instead of claiming one kind for all of them. Absent where there is
169
+ * nothing to tell apart: a single-target lookup, or an inline reference’s
170
+ * entry. Keyed by a symbol so it can never collide with a declared field
171
+ * name.
172
+ */
173
+ readonly [NESTED_DOCUMENT_TYPE]?: string;
136
174
  }
175
+ /** The key under which a {@link NestedDocument} carries the `name` of the
176
+ * Root Type it is read through. */
177
+ export declare const NESTED_DOCUMENT_TYPE: unique symbol;
137
178
  /**
138
179
  * A JSON-LD-style language map (`@container: @language`, `@set` arrays); the key
139
180
  * `und` carries untagged (`@none`) values. The surface flattens it to a
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
package/dist/engine.js CHANGED
@@ -1 +1,3 @@
1
- export {};
1
+ /** The key under which a {@link NestedDocument} carries the `name` of the
2
+ * Root Type it is read through. */
3
+ export const NESTED_DOCUMENT_TYPE = Symbol('lde.search.nestedDocumentType');
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ 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
8
  export type { SearchQuery, ReferenceProjection, Filter, Criterion, CriterionBase, WeldedCriterion, Sort, } from './query.js';
9
- export type { SearchEngine, SearchResult, SearchHit, ResultDocument, SearchValue, LocalizedValue, Reference, NestedDocument, FacetBucket, FacetMap, FacetsOutcome, FacetFieldsOf, OutputFieldsOf, } from './engine.js';
9
+ export { NESTED_DOCUMENT_TYPE } from './engine.js';
10
+ export type { SearchEngine, SearchResult, SearchHit, ResultDocument, SearchValue, LocalizedValue, Reference, NestedDocument, Facet, FacetBucket, FacetExtent, FacetMap, FacetsOutcome, FacetFieldsOf, OutputFieldsOf, } from './engine.js';
10
11
  export type { FramedNode } from './frame-by-type.js';
11
12
  //# 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,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
+ {"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,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,cAAc,EACd,WAAW,EACX,cAAc,EACd,SAAS,EACT,cAAc,EACd,KAAK,EACL,WAAW,EACX,WAAW,EACX,QAAQ,EACR,aAAa,EACb,aAAa,EACb,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -27,3 +27,6 @@ export { joinGraph, MAX_JOIN_DEPTH } from './join-graph.js';
27
27
  // policy (`queryDefaults`) appends to `where`, so it belongs on the authoring
28
28
  // surface and not only on the adapter one.
29
29
  export { filterOn } from './query.js';
30
+ // Engine port + the logical result document returned across it. An engine is
31
+ // bound to the whole SearchSchema at construction by its adapter factory.
32
+ export { NESTED_DOCUMENT_TYPE } from './engine.js';
@@ -1,4 +1,4 @@
1
- import { labelSourceNameOf, referenceFields, } from './schema.js';
1
+ import { labelSourceNamesOf, referenceFields, } from './schema.js';
2
2
  /**
3
3
  * The deepest `on` path a query may state: `dataset → publisher → country` is
4
4
  * three hops. Fixed rather than configurable, and enforced in the query IR
@@ -103,7 +103,9 @@ function declaredEdges(searchType, byName) {
103
103
  // field, which a Reference Type cannot carry (a nested field is `output`
104
104
  // only). So a joinable edge always has a collection at the far end without
105
105
  // a rule of its own.
106
- const target = byName.get(labelSourceNameOf(field));
106
+ // One target, never several: `validateSearchType` refuses `joinable` on a
107
+ // reference naming more than one.
108
+ const target = byName.get(labelSourceNamesOf(field)[0]);
107
109
  const claimed = claimedBy.get(target.name);
108
110
  if (claimed !== undefined) {
109
111
  throw new Error(`Search type “${searchType.name}” declares two joinable references to “${target.name}” (“${claimed}” and “${field.name}”); an engine addresses a join by collection, not by field, so only one reference per target can be joinable. Drop “joinable” from one of them – it keeps its labels, facets and id filtering.`);
@@ -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;AAGzC,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;AAwKD;;;;;;;;;;;;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;AAGzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAoBL,KAAK,eAAe,EAIpB,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;AAmLD;;;;;;;;;;;;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,7 +1,7 @@
1
1
  import { fold } from '@lde/text-normalization';
2
2
  import { matchLanguage } from './language.js';
3
3
  import { buildSubjectIndex, frameSubjects, } from './frame-by-type.js';
4
- import { assertTypeInSchema, displayFieldName, documentKeyOf, fieldNamed, inheritedFacetKeys, inlineFramingDepth, irAlias, isAbsoluteIri, isInternalField, isInlineReference, isoToUnixSeconds, labelSourceNameOf, localLookupTypeOf, physicalFields, referenceTypeNamed, rootTypeNamed, } from './schema.js';
4
+ import { assertTypeInSchema, displayFieldName, documentKeyOf, fieldNamed, identityFieldOf, inheritedFacetPolicies, inlineFramingDepth, irAlias, isAbsoluteIri, isInternalField, isInlineReference, isoToUnixSeconds, localLookupTargetsOf, physicalFields, referencedTargetsOf, referenceTypeNamed, storedTargetOf, TARGET_FIELD, } from './schema.js';
5
5
  /** The context a projection runs with when its caller supplied none. */
6
6
  const NO_PROJECTION_CONTEXT = {};
7
7
  /**
@@ -65,19 +65,21 @@ function pruneInternalFields(document, searchType, schema) {
65
65
  if (schema === undefined || field.kind !== 'reference') {
66
66
  continue;
67
67
  }
68
- const nestedType = field.ref?.strategy === 'inline'
69
- ? referenceTypeNamed(schema, field.ref.typeName)
70
- : localLookupTypeOf(field, schema);
68
+ const nestedTypes = field.ref?.strategy === 'inline'
69
+ ? [referenceTypeNamed(schema, field.ref.typeName)].filter((type) => type !== undefined)
70
+ : localLookupTargetsOf(field, schema);
71
71
  const nested = document[field.name];
72
- if (nestedType === undefined || nested === undefined) {
72
+ if (nestedTypes.length === 0 || nested === undefined) {
73
73
  continue;
74
74
  }
75
75
  for (const referent of Array.isArray(nested) ? nested : [nested]) {
76
- pruneInternalFields(referent, nestedType, schema);
76
+ pruneInternalFields(referent, nestedTypes.length === 1
77
+ ? nestedTypes[0]
78
+ : storedTargetOf(referent, nestedTypes), schema);
77
79
  }
78
80
  // Two entries that differed only in their node IRI are one entry now that
79
81
  // it is gone – two edge nodes stating the same fact.
80
- if (nestedType.class === undefined && Array.isArray(nested)) {
82
+ if (nestedTypes[0].class === undefined && Array.isArray(nested)) {
81
83
  document[field.name] = dedupeBy(nested, (referent) => JSON.stringify(referent));
82
84
  }
83
85
  }
@@ -254,9 +256,9 @@ function applyField(document, node, field, searchType, schema, context, nested)
254
256
  // is an entry like any other, minus its `id`. Same reason as above for
255
257
  // needing a schema: without one there is no target to project through.
256
258
  if (field.kind === 'reference' && schema !== undefined) {
257
- const localType = localLookupTypeOf(field, schema);
258
- if (localType !== undefined) {
259
- const endpoints = applyNestedReferents(document, valuesOf(node, alias), field, localType, schema, context);
259
+ const localTargets = localLookupTargetsOf(field, schema);
260
+ if (localTargets.length > 0) {
261
+ const endpoints = applyNestedReferents(document, valuesOf(node, alias), field, localTargets, schema, context);
260
262
  // A `local` lookup stores an object, so its own id is a level deeper than
261
263
  // a filter can reach – an engine welds conditions on an entry's LEAF
262
264
  // fields only. `filterable` therefore fans out the id beside the object,
@@ -270,8 +272,12 @@ function applyField(document, node, field, searchType, schema, context, nested)
270
272
  return applyText(document, langValuesOf(node, alias), field);
271
273
  case 'keyword':
272
274
  return applyFacet(document, literalsOf(node, alias), field, schema);
273
- case 'reference':
274
- return applyFacet(document, referenceValues(node, alias, field, schema), field, schema);
275
+ case 'reference': {
276
+ const targetsById = new Map();
277
+ const values = referenceValues(node, alias, field, schema, targetsById);
278
+ rememberTargets(document, targetsById);
279
+ return applyFacet(document, values, field, schema, targetsById);
280
+ }
275
281
  case 'integer':
276
282
  return setNumber(document, field.name, toInteger(firstLiteralOf(node, alias)));
277
283
  case 'number':
@@ -316,27 +322,87 @@ function applyField(document, node, field, searchType, schema, context, nested)
316
322
  * referent’s node IRIs therefore sees a key instead, which is why the two are
317
323
  * worth declaring together only deliberately.
318
324
  */
319
- function referenceValues(node, alias, field, schema) {
320
- const targetName = labelSourceNameOf(field);
321
- if (schema === undefined || targetName === undefined) {
322
- return irisOf(node, alias);
323
- }
325
+ function referenceValues(node, alias, field, schema, targetsById) {
324
326
  // Guaranteed declared: `searchSchema` validates that every named target
325
327
  // resolves, and the projection only ever runs against a type of its schema.
326
- const target = rootTypeNamed(schema, targetName);
327
- if (target.key === undefined) {
328
+ const targets = referencedTargetsOf(field, schema);
329
+ if (targets.length === 0) {
328
330
  return irisOf(node, alias);
329
331
  }
330
- const keyFieldName = target.key.field;
331
332
  return valuesOf(node, alias)
332
333
  .map((value) => {
333
334
  const iri = iriString(value);
334
- return iri === undefined
335
- ? undefined
336
- : documentKeyOf(target, iri, isObject(value) ? keyCandidatesOf(value, target, keyFieldName) : []);
335
+ if (iri === undefined) {
336
+ return undefined;
337
+ }
338
+ // Each referent is re-keyed through the target IT belongs to – the one
339
+ // its `rdf:type` matches – so a person keyed on an authority IRI and an
340
+ // organization keyed on nothing store, under one field, the ids their
341
+ // own collections file them under.
342
+ const target = targetOfReferent(value, targets);
343
+ const keyed = target.key === undefined
344
+ ? iri
345
+ : documentKeyOf(target, iri, isObject(value)
346
+ ? keyCandidatesOf(value, target, target.key.field)
347
+ : []);
348
+ // Remembered only where there was a choice: a single-target reference
349
+ // stays byte-identical to what it was, symbol keys included. Keyed by
350
+ // what the field STORES – the value after its own `transform`, which
351
+ // `applyFacet` applies before it reads this map.
352
+ if (keyed !== undefined && targets.length > 1) {
353
+ targetsById.set(field.transform === undefined ? keyed : field.transform(keyed), target);
354
+ }
355
+ return keyed;
337
356
  })
338
357
  .filter((value) => value !== undefined);
339
358
  }
359
+ /**
360
+ * The Root Type a framed referent belongs to, among the targets its reference
361
+ * names: the first whose `class` is among the referent’s `rdf:type`s, which
362
+ * the extraction emits and framing carries as `@type`; else the first target
363
+ * declared. One target needs no evidence at all. This is the one reading of
364
+ * *which of several targets is this*, so re-keying, the facet policy and the
365
+ * stored discriminator ({@link TARGET_FIELD}) cannot disagree.
366
+ */
367
+ function targetOfReferent(value, targets) {
368
+ if (targets.length === 1 || !isObject(value)) {
369
+ return targets[0];
370
+ }
371
+ const raw = value['@type'];
372
+ const classes = new Set((Array.isArray(raw) ? raw : [raw]).filter((type) => typeof type === 'string'));
373
+ return targets.find((target) => classes.has(target.class)) ?? targets[0];
374
+ }
375
+ /**
376
+ * The targets the reference values of a projected node were resolved through,
377
+ * by id – kept beside the node under a symbol, so it never reaches the
378
+ * writer, for the one reader that needs it after projection: the identity
379
+ * companion of an inline reference, which admits an entry’s id to a facet by
380
+ * the policy of the target that id belongs to. Only a reference naming
381
+ * several targets has anything to remember.
382
+ */
383
+ const REFERENT_TARGETS = Symbol('referentTargets');
384
+ function rememberTargets(document, targetsById) {
385
+ if (targetsById.size === 0) {
386
+ return;
387
+ }
388
+ const node = document;
389
+ const remembered = (node[REFERENT_TARGETS] ??= new Map());
390
+ for (const [id, target] of targetsById) {
391
+ remembered.set(id, target);
392
+ }
393
+ }
394
+ /**
395
+ * Whether an id deserves a facet bucket, by the {@link FacetKeys facet policy}
396
+ * of the target it belongs to: admitted where that target declares none, and
397
+ * by its `only` where it does. `targetName` is the target the id was resolved
398
+ * or stored against; an id whose target is unknown falls back to the first
399
+ * declared, the same precedence every other reading of several targets uses.
400
+ */
401
+ function admitsFacet(policies, targets, targetName, id) {
402
+ // Policies are inherited from targets, so where any exists a target does.
403
+ const policy = policies.get(targetName ?? targets[0].name);
404
+ return policy === undefined || policy.only(id);
405
+ }
340
406
  /**
341
407
  * Project a text field. **Display** (when `output`) preserves *every* language
342
408
  * present – one label per language, or every label of it for an `array` field
@@ -450,7 +516,7 @@ function foldedSearchValue(values) {
450
516
  * already-read raw values).
451
517
  *
452
518
  * A reference inheriting a {@link FacetKeys facet policy} from the type it
453
- * names ({@link inheritedFacetKeys}) also writes the `${name}_facet` companion
519
+ * names ({@link inheritedFacetPolicies}) also writes the `${name}_facet` companion
454
520
  * the engine facets instead of the field: the subset of **what the field
455
521
  * stores** that the policy admits. So it is written after the field’s own
456
522
  * `transform` and the IRI filter, from the same `values` – for a keyed target
@@ -469,7 +535,7 @@ function foldedSearchValue(values) {
469
535
  * engine rejects, or – where it does not type-check, as inside a nested
470
536
  * document – a value the API cannot serialize.
471
537
  */
472
- function applyFacet(document, raw, field, schema) {
538
+ function applyFacet(document, raw, field, schema, targetsById) {
473
539
  // A `reference` stores identity, so what it stores must be an IRI whatever
474
540
  // route the value arrived by. {@link iriString} guards the graph path, but a
475
541
  // `transform` runs after it and a `from` projection value never passes it at
@@ -481,21 +547,25 @@ function applyFacet(document, raw, field, schema) {
481
547
  const folded = dedupe(values.map((value) => fold(value)));
482
548
  const names = physicalFields(field, schema);
483
549
  const searchField = names.search[0];
484
- const policy = inheritedFacetKeys(field, schema);
550
+ const policies = inheritedFacetPolicies(field, schema);
551
+ const inherited = policies.size > 0;
485
552
  // `names.facet` is the companion exactly when a policy is inherited –
486
- // physicalFields reads the same `inheritedFacetKeys`.
553
+ // physicalFields reads the same `inheritedFacetPolicies`.
487
554
  const facetField = names.facet;
488
555
  // The companion is a subset of what the field STORES – for a single-valued
489
556
  // field its first value, not the first admitted of all of them, or the facet
490
557
  // would count a value no filter on the field can reproduce.
491
558
  const stored = field.array === true ? values : values.slice(0, 1);
492
- const admitted = policy === undefined ? [] : stored.filter(policy.only);
559
+ const targets = referencedTargetsOf(field, schema);
560
+ const admitted = !inherited
561
+ ? []
562
+ : stored.filter((id) => admitsFacet(policies, targets, targetsById?.get(id)?.name, id));
493
563
  if (field.array === true) {
494
564
  setArray(document, field.name, stored);
495
565
  if (field.searchable) {
496
566
  setArray(document, searchField, folded);
497
567
  }
498
- if (policy !== undefined) {
568
+ if (inherited) {
499
569
  setArray(document, facetField, admitted);
500
570
  }
501
571
  return;
@@ -504,7 +574,7 @@ function applyFacet(document, raw, field, schema) {
504
574
  if (field.searchable) {
505
575
  setString(document, searchField, folded[0]);
506
576
  }
507
- if (policy !== undefined) {
577
+ if (inherited) {
508
578
  setString(document, facetField, admitted[0]);
509
579
  }
510
580
  }
@@ -555,7 +625,7 @@ function applyInlineReference(document, node, alias, field, schema, context, nes
555
625
  if (referenceType === undefined) {
556
626
  return;
557
627
  }
558
- const referents = applyNestedReferents(document, valuesOf(node, alias), field, referenceType, schema, context);
628
+ const referents = applyNestedReferents(document, valuesOf(node, alias), field, [referenceType], schema, context);
559
629
  applyIdentityCompanion(document, referents, field, schema, nested);
560
630
  }
561
631
  /**
@@ -584,11 +654,24 @@ function applyIdentityCompanion(document, referents, field, schema, nested) {
584
654
  // keeps the first referent and drops the rest, and a companion holding an id
585
655
  // from a dropped one would match a filter whose hit shows no such entry.
586
656
  const stored = field.array === true ? referents : referents.slice(0, 1);
587
- const ids = dedupe(stored.flatMap((referent) => (Array.isArray(referent[identity])
588
- ? referent[identity]
589
- : [referent[identity]])
590
- .map(identityValue)
591
- .filter((id) => id !== undefined)));
657
+ // Each id with the target it was resolved through, for the facet policy
658
+ // below: a stored endpoint says so itself ({@link TARGET_FIELD}), a bare id
659
+ // was remembered when the entry was projected ({@link rememberTargets}).
660
+ const targetById = new Map();
661
+ for (const referent of stored) {
662
+ const remembered = referent[REFERENT_TARGETS];
663
+ const raw = referent[identity];
664
+ for (const value of Array.isArray(raw) ? raw : [raw]) {
665
+ const id = identityValue(value);
666
+ if (id === undefined || targetById.has(id)) {
667
+ continue;
668
+ }
669
+ targetById.set(id, isObject(value) && typeof value[TARGET_FIELD] === 'string'
670
+ ? value[TARGET_FIELD]
671
+ : remembered?.get(id)?.name);
672
+ }
673
+ }
674
+ const ids = [...targetById.keys()];
592
675
  if (ids.length === 0) {
593
676
  return;
594
677
  }
@@ -596,9 +679,10 @@ function applyIdentityCompanion(document, referents, field, schema, nested) {
596
679
  // Same rule as every other facetable reference: where the target declares a
597
680
  // facet policy, the facet reads a narrowed companion of its own, so an
598
681
  // excluded id is never seen by the engine rather than merely unlabelled.
599
- const policy = inheritedFacetKeys(field, schema);
600
- if (policy !== undefined) {
601
- setIdentity(document, names.facet, ids.filter(policy.only), field, nested);
682
+ const policies = inheritedFacetPolicies(field, schema);
683
+ if (policies.size > 0) {
684
+ const targets = referencedTargetsOf(identityFieldOf(field, schema), schema);
685
+ setIdentity(document, names.facet, ids.filter((id) => admitsFacet(policies, targets, targetById.get(id), id)), field, nested);
602
686
  }
603
687
  }
604
688
  /**
@@ -676,14 +760,27 @@ function identityValue(value) {
676
760
  * top-level document of that type would be – so the stored id is the one the
677
761
  * target’s collection files it under, and the lookup can find it.
678
762
  */
679
- function applyNestedReferents(document, values, field, nestedType, schema, context) {
763
+ function applyNestedReferents(document, values, field, nestedTypes, schema, context) {
680
764
  // One node may stand for several entries: a weldable leaf is single-valued,
681
765
  // so an edge the graph gave several roles or several endpoints fans out into
682
766
  // one entry per combination BEFORE it is projected (ADR 26).
683
767
  const referents = values
684
768
  .filter(isObject)
685
- .flatMap((value) => tuplesOf(value, nestedType, field))
686
- .map((tuple) => projectFields(tuple, nestedType, schema, context, true))
769
+ .flatMap((value) => {
770
+ // A lookup naming several targets projects each referent through the
771
+ // one it belongs to, and stores which – so an engine adapter can read
772
+ // an entry no collection answers for through the right declaration.
773
+ const nestedType = nestedTypes.length === 1
774
+ ? nestedTypes[0]
775
+ : targetOfReferent(value, nestedTypes);
776
+ return tuplesOf(value, nestedType, field).map((tuple) => {
777
+ const projected = projectFields(tuple, nestedType, schema, context, true);
778
+ if (nestedTypes.length > 1 && Object.keys(projected).length > 0) {
779
+ projected[TARGET_FIELD] = nestedType.name;
780
+ }
781
+ return projected;
782
+ });
783
+ })
687
784
  // Fields, not identity, are what makes something a referent: a literal
688
785
  // value object under the alias (dirty source data), or a node this
689
786
  // reference type reads nothing from, projects nothing and is no referent.
@@ -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,4 +1,4 @@
1
- import { fieldNamed, filterOperatorFor, ID_FIELD, isoToUnixSeconds, nestedReferenceType, } from './schema.js';
1
+ import { fieldNamed, filterOperatorFor, ID_FIELD, isoToUnixSeconds, nestedReferenceType, referencedTargetsOf, } from './schema.js';
2
2
  import { MAX_JOIN_DEPTH } from './join-graph.js';
3
3
  /** Whether a criterion welds conditions to one entry ({@link WeldedCriterion}). */
4
4
  export function isWelded(criterion) {
@@ -261,7 +261,7 @@ export function validateQuery(query, searchType, schema, joins) {
261
261
  issues.push({ part: 'facets', field: name, reason: 'not-facetable' });
262
262
  }
263
263
  }
264
- collectProjectionIssues(query.resolve, searchType, schema, issues);
264
+ collectProjectionIssues(query.resolve, [searchType], schema, issues);
265
265
  for (const sort of query.orderBy) {
266
266
  if (sort.field !== 'relevance' &&
267
267
  fieldNamed(searchType, sort.field) === undefined) {
@@ -289,35 +289,52 @@ export function validateQuery(query, searchType, schema, joins) {
289
289
  * target that does not exist, so reporting it too would bury the one mistake
290
290
  * under its consequences.
291
291
  */
292
- function collectProjectionIssues(projection, searchType, schema, issues) {
292
+ function collectProjectionIssues(projection, searchTypes, schema, issues) {
293
293
  for (const [name, level] of Object.entries(projection ?? {})) {
294
- const field = fieldNamed(searchType, name);
295
- if (field === undefined) {
294
+ // A level is read against every type it may be reached through: a lookup
295
+ // naming several targets resolves a referent of any of them, so the level
296
+ // below is valid wherever ANY of those targets serves it – a field one
297
+ // target declares and another does not is exactly what a polymorphic
298
+ // selection asks for.
299
+ const fields = searchTypes
300
+ .map((searchType) => fieldNamed(searchType, name))
301
+ .filter((field) => field !== undefined);
302
+ if (fields.length === 0) {
296
303
  issues.push({ part: 'resolve', field: name, reason: 'unknown-field' });
297
304
  continue;
298
305
  }
299
306
  // Descending into an inline reference is free, so a nested level is valid
300
307
  // wherever the entries exist – what it is *for* is the lookup below it.
301
- const nested = nestedReferenceType(schema, field);
302
- if (nested !== undefined) {
308
+ // `searchSchema` holds the targets of one lookup to one declaration per
309
+ // name, so a name is nested or a lookup, never both; each is still read
310
+ // on its own here rather than one deciding for the other.
311
+ const nested = [];
312
+ for (const field of fields) {
313
+ const referenceType = nestedReferenceType(schema, field);
314
+ if (referenceType !== undefined) {
315
+ nested.push(referenceType);
316
+ }
317
+ }
318
+ if (nested.length > 0) {
303
319
  collectProjectionIssues(level.resolve, nested, schema, issues);
304
- continue;
305
320
  }
306
- if (field.kind !== 'reference' || field.ref?.strategy !== 'lookup') {
307
- issues.push({ part: 'resolve', field: name, reason: 'not-resolvable' });
321
+ const lookups = fields.filter((field) => field.kind === 'reference' && field.ref?.strategy === 'lookup');
322
+ if (lookups.length === 0) {
323
+ if (nested.length === 0) {
324
+ issues.push({ part: 'resolve', field: name, reason: 'not-resolvable' });
325
+ }
308
326
  continue;
309
327
  }
310
- const targetName = field.ref.target;
311
- const target = [...schema.values()].find((rootType) => rootType.name === targetName);
312
- if (target === undefined) {
328
+ const targets = lookups.flatMap((field) => referencedTargetsOf(field, schema));
329
+ if (targets.length === 0) {
313
330
  // searchSchema rejects a lookup whose target it cannot resolve, so this
314
331
  // is a query built against a different schema than the engine serves.
315
332
  issues.push({ part: 'resolve', field: name, reason: 'not-resolvable' });
316
333
  continue;
317
334
  }
318
335
  for (const wanted of level.fields ?? []) {
319
- const targetField = fieldNamed(target, wanted);
320
- if (targetField === undefined || targetField.output !== true) {
336
+ const served = targets.some((target) => fieldNamed(target, wanted)?.output === true);
337
+ if (!served) {
321
338
  issues.push({
322
339
  part: 'resolve',
323
340
  field: `${name}.${wanted}`,
@@ -325,7 +342,7 @@ function collectProjectionIssues(projection, searchType, schema, issues) {
325
342
  });
326
343
  }
327
344
  }
328
- collectProjectionIssues(level.resolve, target, schema, issues);
345
+ collectProjectionIssues(level.resolve, targets, schema, issues);
329
346
  }
330
347
  }
331
348
  /** Throw on the first structurally invalid query part ({@link validateQuery}),