@lde/search 0.18.3 → 0.20.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,9 +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, inlineFramingDepth, fieldNamed, datasetField, ID_FIELD, AND_KEY, OR_KEY, labelFieldOf, labelFieldNameOf, DEFAULT_LABEL_FIELD, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
9
+ export { assertTypeInSchema, physicalFields, irAlias, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, isInlineReference, nestedReferenceType, nestedFieldName, referenceFields, referenceTypeNamed, inlineFramingDepth, fieldNamed, datasetField, ID_FIELD, AND_KEY, OR_KEY, labelFieldOf, labelFieldNameOf, 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
+ export { joinGraph, MAX_JOIN_DEPTH } from './join-graph.js';
13
+ export type { JoinGraph } from './join-graph.js';
12
14
  export { filterOperatorFor, filterOperator, filterOn, isUnsatisfiable, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
13
15
  export type { FilterOperator, QueryIssue } from './query.js';
14
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,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,OAAO,EACP,MAAM,EACN,YAAY,EACZ,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,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,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,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,OAAO,EACP,MAAM,EACN,YAAY,EACZ,gBAAgB,EAChB,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"}
package/dist/adapter.js CHANGED
@@ -6,6 +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, inlineFramingDepth, fieldNamed, datasetField, ID_FIELD, AND_KEY, OR_KEY, labelFieldOf, labelFieldNameOf, DEFAULT_LABEL_FIELD, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
9
+ export { assertTypeInSchema, physicalFields, irAlias, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, isInlineReference, nestedReferenceType, nestedFieldName, referenceFields, referenceTypeNamed, inlineFramingDepth, fieldNamed, datasetField, ID_FIELD, AND_KEY, OR_KEY, labelFieldOf, labelFieldNameOf, DEFAULT_LABEL_FIELD, isRangeFacet, isAbsoluteIri, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
10
10
  export { physicalNameTokens } from './physical-name.js';
11
+ export { joinGraph, MAX_JOIN_DEPTH } from './join-graph.js';
11
12
  export { filterOperatorFor, filterOperator, filterOn, isUnsatisfiable, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
package/dist/index.d.ts CHANGED
@@ -2,8 +2,10 @@ export { projectRoots } from './project.js';
2
2
  export type { ProjectedNode, ProjectionContext, SearchDocument, } from './project.js';
3
3
  export { defineSearchType, searchSchema, validateSearchType, assertValidSearchType, ID_FIELD, AND_KEY, OR_KEY, } from './schema.js';
4
4
  export type { FieldKind, SearchField, SearchFieldBase, TextField, KeywordField, ReferenceField, NumericField, BooleanField, SearchType, SearchTypeBase, RootType, ReferenceType, RootTypeOf, SearchTypeIssue, SearchSchema, FacetRange, ProjectionValue, } from './schema.js';
5
+ export { joinGraph, MAX_JOIN_DEPTH } from './join-graph.js';
6
+ export type { JoinGraph } from './join-graph.js';
5
7
  export { filterOn } from './query.js';
6
- export type { SearchQuery, Filter, Criterion, Sort } from './query.js';
8
+ export type { SearchQuery, Filter, Criterion, CriterionBase, Sort, } from './query.js';
7
9
  export type { SearchEngine, SearchResult, SearchHit, ResultDocument, SearchValue, LocalizedValue, Reference, NestedDocument, FacetBucket, FacetMap, FacetsOutcome, FacetFieldsOf, OutputFieldsOf, } from './engine.js';
8
10
  export type { FramedNode } from './frame-by-type.js';
9
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,GACP,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,SAAS,EACT,WAAW,EACX,eAAe,EACf,SAAS,EACT,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,cAAc,EACd,QAAQ,EACR,aAAa,EACb,UAAU,EACV,eAAe,EACf,YAAY,EACZ,UAAU,EACV,eAAe,GAChB,MAAM,aAAa,CAAC;AAOrB,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAIvE,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,GACP,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,SAAS,EACT,WAAW,EACX,eAAe,EACf,SAAS,EACT,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,cAAc,EACd,QAAQ,EACR,aAAa,EACb,UAAU,EACV,eAAe,EACf,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,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"}
package/dist/index.js CHANGED
@@ -11,6 +11,10 @@ export { projectRoots } from './project.js';
11
11
  // schema, query semantics and the GraphQL surface – a discriminated union by
12
12
  // `kind`, validated again at runtime when the schema is built.
13
13
  export { defineSearchType, searchSchema, validateSearchType, assertValidSearchType, ID_FIELD, AND_KEY, OR_KEY, } from './schema.js';
14
+ // The declared join edges between root types: which types a query can filter
15
+ // across, and which collections must be rebuilt together. Built eagerly by
16
+ // `searchSchema`, so declaring a schema is what validates the joins.
17
+ export { joinGraph, MAX_JOIN_DEPTH } from './join-graph.js';
14
18
  // Engine- and protocol-neutral query IR (what a `queryDefaults` policy or an
15
19
  // in-process caller writes).
16
20
  // `filterOn` builds the ordinary one-criterion clause – the shape a consumer
@@ -0,0 +1,63 @@
1
+ import { type RootType, type SearchSchema, type SearchType } from './schema.js';
2
+ /**
3
+ * The deepest `on` path a query may state: `dataset → publisher → country` is
4
+ * three hops. Fixed rather than configurable, and enforced in the query IR
5
+ * ({@link validateQuery}) rather than in an adapter, so a later REST surface
6
+ * inherits the same ceiling. Typesense imposes no limit of its own, and the
7
+ * GraphQL max-depth guard counts selection sets rather than input nesting, so
8
+ * without this a `where` could nest a join arbitrarily deep and each hop is an
9
+ * extra collection scan.
10
+ */
11
+ export declare const MAX_JOIN_DEPTH = 3;
12
+ /**
13
+ * The declared join edges of a {@link SearchSchema}: which Root Types are
14
+ * reachable from which through {@link ReferenceField.joinable} references, and
15
+ * which of them must be rebuilt together.
16
+ *
17
+ * Two members, because a consumer needs exactly two answers. A **query**
18
+ * compiler asks *what does this path of field names reach* ({@link
19
+ * JoinGraph.resolve}); a **writer** asks *which collections come into existence
20
+ * together* ({@link JoinGraph.components}). Everything else – deriving the
21
+ * edges from `joinable` + `labelSource`, the one-edge-per-target rule, cycle
22
+ * rejection, the depth cap, and the asymmetry between undirected membership and
23
+ * directed creation order – is hidden here, so no consumer restates it.
24
+ *
25
+ * `resolve` returns a {@link RootType}, never a collection name: how a type is
26
+ * named in an engine is engine- and deployment-specific and stays in the
27
+ * adapter.
28
+ */
29
+ export interface JoinGraph {
30
+ /**
31
+ * The **join components**: groups of Root Types that reference one another,
32
+ * directly or transitively, and therefore share a rebuild
33
+ * ([ADR 19](../../docs/decisions/0019-filter-across-collections-through-declared-joins.md)).
34
+ * Every Root Type appears in exactly one; a type with no joinable edge is a
35
+ * singleton and keeps per-collection isolation exactly as before.
36
+ *
37
+ * Membership is the **undirected** connected component – a referrer and its
38
+ * referent must rebuild together whichever way the edge points – while the
39
+ * order **within** a component is the **directed** topological sort,
40
+ * referenced first. That asymmetry is load-bearing: an engine cannot create a
41
+ * collection whose reference names a collection that does not exist yet, so
42
+ * the referent’s collection must be opened first.
43
+ */
44
+ readonly components: readonly (readonly RootType[])[];
45
+ /**
46
+ * The Root Type a path of joinable field names reaches from `from`, or
47
+ * `undefined` when the path does not resolve – an unknown field name, a
48
+ * reference that is not `joinable`, or a path deeper than
49
+ * {@link MAX_JOIN_DEPTH}. An empty path resolves to `from` itself only when
50
+ * `from` is a Root Type; a Reference Type has no collection to join to.
51
+ */
52
+ resolve(from: SearchType, path: readonly string[]): RootType | undefined;
53
+ }
54
+ /**
55
+ * The join graph of a {@link SearchSchema}, built once per schema and cached:
56
+ * {@link searchSchema} builds it eagerly, so a schema whose joins do not hold
57
+ * up – two joinable fields at one target, a cycle – fails at startup rather
58
+ * than on the first query or the first rebuild.
59
+ *
60
+ * Calling it again for the same schema returns the same graph.
61
+ */
62
+ export declare function joinGraph(schema: SearchSchema): JoinGraph;
63
+ //# sourceMappingURL=join-graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"join-graph.d.ts","sourceRoot":"","sources":["../src/join-graph.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AAErB;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,SAAS;IACxB;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC,SAAS,QAAQ,EAAE,CAAC,EAAE,CAAC;IACtD;;;;;;OAMG;IACH,OAAO,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,QAAQ,GAAG,SAAS,CAAC;CAC1E;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,SAAS,CAQzD"}
@@ -0,0 +1,192 @@
1
+ import { referenceFields, } from './schema.js';
2
+ /**
3
+ * The deepest `on` path a query may state: `dataset → publisher → country` is
4
+ * three hops. Fixed rather than configurable, and enforced in the query IR
5
+ * ({@link validateQuery}) rather than in an adapter, so a later REST surface
6
+ * inherits the same ceiling. Typesense imposes no limit of its own, and the
7
+ * GraphQL max-depth guard counts selection sets rather than input nesting, so
8
+ * without this a `where` could nest a join arbitrarily deep and each hop is an
9
+ * extra collection scan.
10
+ */
11
+ export const MAX_JOIN_DEPTH = 3;
12
+ /**
13
+ * The join graph of a {@link SearchSchema}, built once per schema and cached:
14
+ * {@link searchSchema} builds it eagerly, so a schema whose joins do not hold
15
+ * up – two joinable fields at one target, a cycle – fails at startup rather
16
+ * than on the first query or the first rebuild.
17
+ *
18
+ * Calling it again for the same schema returns the same graph.
19
+ */
20
+ export function joinGraph(schema) {
21
+ const cached = graphsBySchema.get(schema);
22
+ if (cached !== undefined) {
23
+ return cached;
24
+ }
25
+ const built = buildJoinGraph(schema);
26
+ graphsBySchema.set(schema, built);
27
+ return built;
28
+ }
29
+ /** One graph per schema, held beside it rather than on it, so a
30
+ * {@link SearchSchema} stays a plain branded `Map`. */
31
+ const graphsBySchema = new WeakMap();
32
+ /** The edges leaving one Root Type, keyed by field name. Total by
33
+ * construction: the index is built with an entry for EVERY Root Type, an empty
34
+ * map where a type declares none, so no caller needs a fallback it can never
35
+ * take. */
36
+ function edgesOf(edges, searchType) {
37
+ return edges.get(searchType.class);
38
+ }
39
+ /** The edges leaving one Root Type, in declaration order. */
40
+ function edgesFrom(edges, searchType) {
41
+ return edgesOf(edges, searchType).values();
42
+ }
43
+ /**
44
+ * Derive, validate and index the edges. Throws – rather than reporting issues –
45
+ * because it is called from {@link searchSchema}, whose contract is that an
46
+ * invalid declaration fails at startup.
47
+ */
48
+ function buildJoinGraph(schema) {
49
+ const rootTypes = [...schema.values()];
50
+ const byName = new Map(rootTypes.map((type) => [type.name, type]));
51
+ const edges = new Map();
52
+ for (const searchType of rootTypes) {
53
+ edges.set(searchType.class, declaredEdges(searchType, byName));
54
+ }
55
+ assertAcyclic(rootTypes, edges);
56
+ return {
57
+ components: componentsOf(rootTypes, edges),
58
+ resolve(from, path) {
59
+ if (path.length > MAX_JOIN_DEPTH) {
60
+ return undefined;
61
+ }
62
+ // A Reference Type is never indexed, so it has no collection to join
63
+ // from; only a member of this schema’s class map can start a path.
64
+ const start = from.class === undefined ? undefined : schema.get(from.class);
65
+ if (start === undefined || start !== from) {
66
+ return undefined;
67
+ }
68
+ let current = start;
69
+ for (const name of path) {
70
+ const edge = edgesOf(edges, current).get(name);
71
+ if (edge === undefined) {
72
+ return undefined;
73
+ }
74
+ current = edge.target;
75
+ }
76
+ return current;
77
+ },
78
+ };
79
+ }
80
+ /**
81
+ * The joinable edges one Root Type declares, keyed by field name.
82
+ *
83
+ * Enforces the **one edge per (type, target)** rule here rather than in a
84
+ * consumer: a Typesense join addresses the referent by *collection*, not by
85
+ * field, so a second reference to the same collection is accepted, indexed and
86
+ * then unreachable – the engine resolves every join through the first field
87
+ * that matched and silently ignores the rest
88
+ * ({@link https://github.com/typesense/typesense/issues/3021}). `publisher` and
89
+ * `creator` both resolving to `Organization` is the ordinary case, so it must
90
+ * be a declaration error naming both fields rather than a surprise at query
91
+ * time.
92
+ */
93
+ function declaredEdges(searchType, byName) {
94
+ const edges = new Map();
95
+ const claimedBy = new Map();
96
+ for (const field of referenceFields(searchType)) {
97
+ if (field.joinable !== true) {
98
+ continue;
99
+ }
100
+ // Both lookups always hit: `validateSearchType` rejects `joinable` without
101
+ // a `labelSource`, and a label source is always an indexed Root Type – it
102
+ // must declare a `searchable` `label` field, which a Reference Type cannot
103
+ // carry (a nested field is `output` only). So a joinable edge always has a
104
+ // collection at the far end without a rule of its own.
105
+ const target = byName.get(field.labelSource);
106
+ const claimed = claimedBy.get(target.name);
107
+ if (claimed !== undefined) {
108
+ 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.`);
109
+ }
110
+ claimedBy.set(target.name, field.name);
111
+ edges.set(field.name, { field, target });
112
+ }
113
+ return edges;
114
+ }
115
+ /**
116
+ * Reject a cycle in the **directed** join graph. A cycle would make the
117
+ * topological creation order below undefinable – there is no collection to
118
+ * create first – and, with the component as the unit of rebuild, no order in
119
+ * which a component’s collections could come into existence at all.
120
+ */
121
+ function assertAcyclic(rootTypes, edges) {
122
+ const done = new Set();
123
+ const visit = (searchType, onPath) => {
124
+ if (onPath.some((ancestor) => ancestor.class === searchType.class)) {
125
+ const cycle = [...onPath, searchType]
126
+ .map((type) => `“${type.name}”`)
127
+ .join(' → ');
128
+ throw new Error(`Join cycle ${cycle}; the joinable references between search types must be acyclic, so a component’s collections have an order to be created in.`);
129
+ }
130
+ if (done.has(searchType.class)) {
131
+ return;
132
+ }
133
+ const extended = [...onPath, searchType];
134
+ for (const edge of edgesFrom(edges, searchType)) {
135
+ visit(edge.target, extended);
136
+ }
137
+ done.add(searchType.class);
138
+ };
139
+ for (const searchType of rootTypes) {
140
+ visit(searchType, []);
141
+ }
142
+ }
143
+ /**
144
+ * Partition the Root Types into join components, each topologically ordered.
145
+ *
146
+ * Membership is the undirected connected component (union-find over the
147
+ * edges); the order within one is a depth-first topological sort over the
148
+ * directed edges, so a type is emitted only after everything it references.
149
+ * Components come out in declaration order of their first member, so a schema’s
150
+ * rebuild order is stable across runs.
151
+ */
152
+ function componentsOf(rootTypes, edges) {
153
+ const parent = new Map(rootTypes.map((type) => [type.class, type.class]));
154
+ const find = (iri) => {
155
+ let root = iri;
156
+ while (parent.get(root) !== root) {
157
+ root = parent.get(root);
158
+ }
159
+ return root;
160
+ };
161
+ for (const searchType of rootTypes) {
162
+ for (const edge of edgesFrom(edges, searchType)) {
163
+ parent.set(find(searchType.class), find(edge.target.class));
164
+ }
165
+ }
166
+ const members = new Map();
167
+ for (const searchType of rootTypes) {
168
+ const root = find(searchType.class);
169
+ members.set(root, [...(members.get(root) ?? []), searchType]);
170
+ }
171
+ return [...members.values()].map((component) => topologicallyOrdered(component, edges));
172
+ }
173
+ /** One component’s members, everything a type references before the type
174
+ * itself. Terminates because {@link assertAcyclic} ran first. */
175
+ function topologicallyOrdered(component, edges) {
176
+ const ordered = [];
177
+ const emitted = new Set();
178
+ const visit = (searchType) => {
179
+ if (emitted.has(searchType.class)) {
180
+ return;
181
+ }
182
+ emitted.add(searchType.class);
183
+ for (const edge of edgesFrom(edges, searchType)) {
184
+ visit(edge.target);
185
+ }
186
+ ordered.push(searchType);
187
+ };
188
+ for (const searchType of component) {
189
+ visit(searchType);
190
+ }
191
+ return ordered;
192
+ }
@@ -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,EAWL,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;AAyED;;;;;;;;;;;;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,CAW/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,EAYL,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;AA2ED;;;;;;;;;;;;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,CAW/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, inlineFramingDepth, irAlias, isInternalField, isInlineReference, isoToUnixSeconds, physicalFields, referenceTypeNamed, } from './schema.js';
3
+ import { assertTypeInSchema, displayFieldName, inlineFramingDepth, irAlias, isAbsoluteIri, isInternalField, isInlineReference, isoToUnixSeconds, physicalFields, referenceTypeNamed, } from './schema.js';
4
4
  /** The context a projection runs with when its caller supplied none. */
5
5
  const NO_PROJECTION_CONTEXT = {};
6
6
  /**
@@ -19,7 +19,7 @@ const NO_PROJECTION_CONTEXT = {};
19
19
  */
20
20
  export function projectDocument(node, searchType, schema, context = NO_PROJECTION_CONTEXT) {
21
21
  if (documentKey(node) === undefined) {
22
- throw new Error(`Cannot project a “${searchType.name}” node without an @id (a blank node label is not one): every search document needs a stable key, and an empty one would collide with other keyless nodes.`);
22
+ throw new Error(`Cannot project a “${searchType.name}” node whose @id is not an absolute IRI (got ${JSON.stringify(node['@id']) ?? 'none'}; a blank node label or a relative reference is not one): every search document needs a stable key, and an empty one would collide with other keyless nodes.`);
23
23
  }
24
24
  // The guard above is what makes this a SearchDocument: projectFields sets `id`
25
25
  // for exactly the nodes documentKey resolves.
@@ -76,15 +76,17 @@ function projectFields(node, searchType, schema, context) {
76
76
  return document;
77
77
  }
78
78
  /**
79
- * The document key a framed node carries, if any. A blank node label (`_:b0`) is
80
- * not one: framing mints it per call, so it recurs across documents and can
81
- * change when unrelated triples do. A node bearing one is therefore projected as
82
- * if framing had pruned its `@id` – which it does whenever the label occurs only
83
- * once in the framing results.
79
+ * The document key a framed node carries, if any – an {@link isAbsoluteIri
80
+ * absolute IRI}, the same rule {@link iriString} applies to a referent, so a
81
+ * root and a reference cannot disagree about what counts as identity. A blank
82
+ * node label (`_:b0`) is not one: framing mints it per call, so it recurs across
83
+ * documents and can change when unrelated triples do. A node bearing one is
84
+ * therefore projected as if framing had pruned its `@id` – which it does
85
+ * whenever the label occurs only once in the framing results.
84
86
  */
85
87
  function documentKey(node) {
86
88
  const id = node['@id'];
87
- return typeof id === 'string' && !id.startsWith('_:') ? id : undefined;
89
+ return typeof id === 'string' && isAbsoluteIri(id) ? id : undefined;
88
90
  }
89
91
  /**
90
92
  * Project a single type over a known set of `roots` – the per-type, roots-given
@@ -362,14 +364,25 @@ function literalString(value) {
362
364
  }
363
365
  return undefined;
364
366
  }
367
+ /**
368
+ * The IRI a reference value carries, or `undefined` when it carries none. A
369
+ * value may arrive as a bare string – `schema:sameAs`, `contentUrl`,
370
+ * `thumbnailUrl` and `landingPage` all range on `schema:URL`, which a source may
371
+ * emit as a literal rather than a node – or as a node object.
372
+ *
373
+ * **A referent with no IRI yields nothing**, the same rule {@link documentKey}
374
+ * applies to a root and for the same reason: what a `labelOnly`/`idOnly`
375
+ * reference stores is a selection key, and a blank node label is not one.
376
+ * Framing mints it per call, so it recurs across documents and changes when
377
+ * unrelated triples do – indexing it would key a facet bucket on a value that
378
+ * neither groups what is equal nor separates what is not. An **inline**
379
+ * reference is untouched by this: it carries the referent’s fields rather than
380
+ * its identity ({@link applyInlineReference}), so a blank-node referent nests
381
+ * exactly as before.
382
+ */
365
383
  function iriString(value) {
366
- if (typeof value === 'string') {
367
- return value;
368
- }
369
- if (isObject(value) && typeof value['@id'] === 'string') {
370
- return value['@id'];
371
- }
372
- return undefined;
384
+ const iri = isObject(value) ? value['@id'] : value;
385
+ return typeof iri === 'string' && isAbsoluteIri(iri) ? iri : undefined;
373
386
  }
374
387
  function toInteger(literal) {
375
388
  return literal === undefined ? undefined : Math.trunc(Number(literal));
package/dist/query.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type SearchType } from './schema.js';
2
+ import { type JoinGraph } from './join-graph.js';
2
3
  /**
3
4
  * The engine- and protocol-neutral query IR. Every API surface compiles its
4
5
  * input into this; every engine adapter compiles it into an engine query. One
@@ -20,6 +21,35 @@ export interface SearchQuery {
20
21
  /** Selects the per-locale fields to query/sort on (from `Accept-Language`). */
21
22
  readonly locale: string;
22
23
  }
24
+ /**
25
+ * What every {@link Criterion} carries, whatever its operator: the field it
26
+ * constrains, and – for a criterion that constrains a *joined* document rather
27
+ * than this one – the path of joinable references to walk to reach it.
28
+ */
29
+ export interface CriterionBase {
30
+ /** The logical field name, on the type the {@link CriterionBase.on} path
31
+ * reaches (or on the searched type when there is none). */
32
+ readonly field: string;
33
+ /**
34
+ * A **join path**: the names of the {@link ReferenceField.joinable}
35
+ * references to follow, in order, to the type this criterion constrains.
36
+ * `['dataset', 'publisher']` on a `CreativeWork` criterion means *the
37
+ * publisher of the dataset this work belongs to*. Omitted (or empty) for the
38
+ * ordinary case: the criterion constrains the searched type itself.
39
+ *
40
+ * A path, deliberately, and not boolean structure – so the criterion stays an
41
+ * **atom** and `where` stays the flat conjunction of disjunctions
42
+ * [ADR 18](../../docs/decisions/0018-filter-across-several-fields-with-one-clause.md)
43
+ * made it. `on` sits on the criterion rather than on the {@link Filter} for
44
+ * the same reason a criterion carries its own operator: a `Filter`-level
45
+ * `on` would scope the whole disjunction and make
46
+ * `“published by X, or titled Y”` inexpressible.
47
+ *
48
+ * Capped at {@link MAX_JOIN_DEPTH} hops and resolved by
49
+ * {@link validateQuery} against the schema’s {@link JoinGraph}.
50
+ */
51
+ readonly on?: readonly string[];
52
+ }
23
53
  /**
24
54
  * One criterion on one field. The operator is fixed by that field’s
25
55
  * {@link FieldKind} ({@link filterOperatorFor}): keyword/reference use `in` (OR
@@ -29,21 +59,20 @@ export interface SearchQuery {
29
59
  * Criteria are the atoms a {@link Filter} is built from, so each carries its own
30
60
  * field AND its own operator: one clause may range over a `reference` and a
31
61
  * `keyword` field at once, and each field is constrained the way its kind
32
- * allows.
62
+ * allows. Each equally carries its own {@link CriterionBase.on} join path, so
63
+ * one clause may mix a condition on this document with one on a joined
64
+ * document.
33
65
  */
34
- export type Criterion = {
35
- readonly field: string;
66
+ export type Criterion = (CriterionBase & {
36
67
  readonly in: readonly string[];
37
- } | {
38
- readonly field: string;
68
+ }) | (CriterionBase & {
39
69
  readonly range: {
40
70
  readonly min?: number | string;
41
71
  readonly max?: number | string;
42
72
  };
43
- } | {
44
- readonly field: string;
73
+ }) | (CriterionBase & {
45
74
  readonly is: boolean;
46
- };
75
+ });
47
76
  /**
48
77
  * One `where` clause: a disjunction, matching a document that satisfies **any**
49
78
  * of its criteria. An ordinary single-field filter is the one-criterion case,
@@ -92,6 +121,10 @@ export declare function filterOn(criterion: Criterion): Filter;
92
121
  * is a disjunction that happens to include identity, not an enumeration of
93
122
  * wanted documents, so it stays the vacuous no-op every other empty `in` is.
94
123
  *
124
+ * A joined identity clause (`{ on: […], field: 'id', in: [] }`) reads the same
125
+ * way one hop out: it enumerates the *referents* wanted, and no referent means
126
+ * no document can match through that edge.
127
+ *
95
128
  * An adapter MUST answer an unsatisfiable query with an empty result rather
96
129
  * than dispatching it.
97
130
  */
@@ -108,7 +141,12 @@ export declare function isUnsatisfiable(query: SearchQuery): boolean;
108
141
  export interface QueryIssue {
109
142
  readonly part: 'where' | 'facets' | 'orderBy';
110
143
  readonly field: string;
111
- readonly reason: 'unknown-field' | 'not-filterable' | 'operator-mismatch' | 'not-facetable';
144
+ readonly reason: 'unknown-field' | 'not-filterable' | 'operator-mismatch' | 'not-facetable'
145
+ /** The criterion’s `on` path is longer than {@link MAX_JOIN_DEPTH}. */
146
+ | 'join-too-deep'
147
+ /** The `on` path does not resolve: a name that is not a field, a reference
148
+ * that is not `joinable`, or no join graph to resolve it against. */
149
+ | 'unknown-join';
112
150
  }
113
151
  /**
114
152
  * Structurally validate a query against its search type: **every criterion of
@@ -121,15 +159,24 @@ export interface QueryIssue {
121
159
  * checks declaration only, not the `sortable` flag: that flag means *publicly
122
160
  * selectable*, and a deployment policy may sort on a private tie-break field.
123
161
  *
162
+ * A criterion carrying an {@link CriterionBase.on} join path is checked against
163
+ * the type that path reaches instead: the path must be at most
164
+ * {@link MAX_JOIN_DEPTH} hops (`join-too-deep`) and must resolve through
165
+ * `joins` (`unknown-join`), and the leaf field must then be filterable on the
166
+ * *target* type. The cap lives here, in the IR, rather than in an adapter or a
167
+ * surface, so every surface inherits it. `joins` is optional because a query
168
+ * with no join path needs none; a joined criterion validated without one is an
169
+ * `unknown-join`, never a silently unchecked filter.
170
+ *
124
171
  * This is the port’s always-on guard: every {@link SearchEngine} adapter MUST
125
172
  * reject a query with issues ({@link assertValidQuery}) instead of passing
126
173
  * garbage to its engine, so validation holds for every caller – including
127
174
  * `queryDefaults` policies and surfaces weaker than GraphQL.
128
175
  */
129
- export declare function validateQuery(query: SearchQuery, searchType: SearchType): readonly QueryIssue[];
176
+ export declare function validateQuery(query: SearchQuery, searchType: SearchType, joins?: JoinGraph): readonly QueryIssue[];
130
177
  /** Throw on the first structurally invalid query part ({@link validateQuery}),
131
178
  * naming every issue. The always-on entry point for engine adapters. */
132
- export declare function assertValidQuery(query: SearchQuery, searchType: SearchType): void;
179
+ export declare function assertValidQuery(query: SearchQuery, searchType: SearchType, joins?: JoinGraph): void;
133
180
  /**
134
181
  * The 1-based page an `offset` falls on – the numbered-pagination presentation
135
182
  * of the IR, shared by the surfaces and the adapters. `limit: 0` (a facet-only
@@ -1 +1 @@
1
- {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AAErB;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,SAAS,IAAI,EAAE,CAAC;IAClC,2BAA2B;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,SAAS,GACjB;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC1D;IACE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAC/B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;KAChC,CAAC;CACH,GACD;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;CAAE,CAAC;AAErD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,EAAE,EAAE,SAAS,SAAS,EAAE,CAAC;CACnC;AAED,+BAA+B;AAC/B,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;CACpC;AAKD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,cAAc,CAEnE;AAED;wDACwD;AACxD,wBAAgB,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAErD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAW3D;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EACb,eAAe,GAAG,gBAAgB,GAAG,mBAAmB,GAAG,eAAe,CAAC;CAC9E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,UAAU,GACrB,SAAS,UAAU,EAAE,CA0DvB;AAED;yEACyE;AACzE,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,UAAU,GACrB,IAAI,CAUN;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnE"}
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,5 @@
1
1
  import { fieldNamed, filterOperatorFor, ID_FIELD, } from './schema.js';
2
+ import { MAX_JOIN_DEPTH } from './join-graph.js';
2
3
  // The kind→operator table lives with the field model in schema.ts (one source
3
4
  // for the query IR, the surfaces, the adapters AND declaration validation);
4
5
  // re-exported here so the query-IR module stays the natural import site.
@@ -32,6 +33,10 @@ export function filterOn(criterion) {
32
33
  * is a disjunction that happens to include identity, not an enumeration of
33
34
  * wanted documents, so it stays the vacuous no-op every other empty `in` is.
34
35
  *
36
+ * A joined identity clause (`{ on: […], field: 'id', in: [] }`) reads the same
37
+ * way one hop out: it enumerates the *referents* wanted, and no referent means
38
+ * no document can match through that edge.
39
+ *
35
40
  * An adapter MUST answer an unsatisfiable query with an empty result rather
36
41
  * than dispatching it.
37
42
  */
@@ -45,6 +50,13 @@ export function isUnsatisfiable(query) {
45
50
  criterion.in.length === 0);
46
51
  });
47
52
  }
53
+ /** The `field` a joined issue is reported under: the path and the leaf name
54
+ * together, so a message says which hop and which field, not just which
55
+ * field. `dataset.publisher.id` reads the way the criterion was written. */
56
+ function issueField(criterion) {
57
+ const on = criterion.on ?? [];
58
+ return on.length === 0 ? criterion.field : [...on, criterion.field].join('.');
59
+ }
48
60
  /**
49
61
  * Structurally validate a query against its search type: **every criterion of
50
62
  * every `where` clause** names a declared, `filterable` field whose kind accepts
@@ -56,12 +68,21 @@ export function isUnsatisfiable(query) {
56
68
  * checks declaration only, not the `sortable` flag: that flag means *publicly
57
69
  * selectable*, and a deployment policy may sort on a private tie-break field.
58
70
  *
71
+ * A criterion carrying an {@link CriterionBase.on} join path is checked against
72
+ * the type that path reaches instead: the path must be at most
73
+ * {@link MAX_JOIN_DEPTH} hops (`join-too-deep`) and must resolve through
74
+ * `joins` (`unknown-join`), and the leaf field must then be filterable on the
75
+ * *target* type. The cap lives here, in the IR, rather than in an adapter or a
76
+ * surface, so every surface inherits it. `joins` is optional because a query
77
+ * with no join path needs none; a joined criterion validated without one is an
78
+ * `unknown-join`, never a silently unchecked filter.
79
+ *
59
80
  * This is the port’s always-on guard: every {@link SearchEngine} adapter MUST
60
81
  * reject a query with issues ({@link assertValidQuery}) instead of passing
61
82
  * garbage to its engine, so validation holds for every caller – including
62
83
  * `queryDefaults` policies and surfaces weaker than GraphQL.
63
84
  */
64
- export function validateQuery(query, searchType) {
85
+ export function validateQuery(query, searchType, joins) {
65
86
  const issues = [];
66
87
  for (const filter of query.where) {
67
88
  // Each criterion is checked on its own, so a multi-criterion clause needs
@@ -71,6 +92,25 @@ export function validateQuery(query, searchType) {
71
92
  // a `reference` membership with a `date` range without either being wrong.
72
93
  for (const criterion of filter.or) {
73
94
  const name = criterion.field;
95
+ const field = issueField(criterion);
96
+ // A criterion may constrain a JOINED document, and then every rule below
97
+ // is about the type its `on` path reaches, not about the searched one.
98
+ // Resolving the path needs the schema’s join graph, so a caller that
99
+ // supplied none cannot serve a joined criterion at all.
100
+ const on = criterion.on ?? [];
101
+ let constrained = searchType;
102
+ if (on.length > 0) {
103
+ if (on.length > MAX_JOIN_DEPTH) {
104
+ issues.push({ part: 'where', field, reason: 'join-too-deep' });
105
+ continue;
106
+ }
107
+ const target = joins?.resolve(searchType, on);
108
+ if (target === undefined) {
109
+ issues.push({ part: 'where', field, reason: 'unknown-join' });
110
+ continue;
111
+ }
112
+ constrained = target;
113
+ }
74
114
  // `id` is filterable on every type without being declared by any: it is
75
115
  // the document’s IRI, so the lookup exists wherever documents do.
76
116
  // Membership only – an IRI has no range and no truth value.
@@ -78,23 +118,23 @@ export function validateQuery(query, searchType) {
78
118
  if (filterOperator(criterion) !== 'in') {
79
119
  issues.push({
80
120
  part: 'where',
81
- field: name,
121
+ field,
82
122
  reason: 'operator-mismatch',
83
123
  });
84
124
  }
85
125
  continue;
86
126
  }
87
- const field = fieldNamed(searchType, name);
88
- if (field === undefined) {
89
- issues.push({ part: 'where', field: name, reason: 'unknown-field' });
127
+ const declared = fieldNamed(constrained, name);
128
+ if (declared === undefined) {
129
+ issues.push({ part: 'where', field, reason: 'unknown-field' });
90
130
  }
91
- else if (field.filterable !== true) {
92
- issues.push({ part: 'where', field: name, reason: 'not-filterable' });
131
+ else if (declared.filterable !== true) {
132
+ issues.push({ part: 'where', field, reason: 'not-filterable' });
93
133
  }
94
- else if (filterOperatorFor(field.kind) !== filterOperator(criterion)) {
134
+ else if (filterOperatorFor(declared.kind) !== filterOperator(criterion)) {
95
135
  issues.push({
96
136
  part: 'where',
97
- field: name,
137
+ field,
98
138
  reason: 'operator-mismatch',
99
139
  });
100
140
  }
@@ -123,8 +163,8 @@ export function validateQuery(query, searchType) {
123
163
  }
124
164
  /** Throw on the first structurally invalid query part ({@link validateQuery}),
125
165
  * naming every issue. The always-on entry point for engine adapters. */
126
- export function assertValidQuery(query, searchType) {
127
- const issues = validateQuery(query, searchType);
166
+ export function assertValidQuery(query, searchType, joins) {
167
+ const issues = validateQuery(query, searchType, joins);
128
168
  if (issues.length > 0) {
129
169
  const detail = issues
130
170
  .map((issue) => `${issue.part}: “${issue.field}” (${issue.reason})`)
package/dist/schema.d.ts CHANGED
@@ -189,28 +189,76 @@ export interface ReferenceField extends SearchFieldBase, Searchable {
189
189
  * resolution.
190
190
  */
191
191
  readonly labelSource?: string;
192
- /** The referenced entity’s shape and how much of it to carry. Required when
193
- * the field is `output` (the API surfaces need the reference type name);
194
- * optional for a facet- or filter-only reference. */
192
+ /**
193
+ * Turn this reference into an **engine-level join**, so a query can filter
194
+ * this type by a condition on the referent – `“every object published by
195
+ * institution X”` in one round-trip instead of two. Valid only alongside
196
+ * {@link ReferenceField.labelSource}, which already asserts that this
197
+ * field’s values are ids of documents in that type’s collection – exactly
198
+ * the fact a join needs.
199
+ *
200
+ * A capability flag in the vocabulary of `filterable`/`facetable`/`sortable`,
201
+ * and deliberately not derived from `labelSource`: an engine may refuse to
202
+ * index a mutual reference, so auto-deriving would make an existing schema
203
+ * silently lose a field. A `labelSource` added for display therefore costs
204
+ * nothing it does not cost today; only a `joinable` edge pays the
205
+ * component-scoped rebuild coupling.
206
+ *
207
+ * Never on an `inline` reference: that carries its referent as a nested
208
+ * object rather than as an id an engine can point a reference field at.
209
+ *
210
+ * At most ONE joinable field per (type, label source) – see
211
+ * {@link joinGraph}.
212
+ */
213
+ readonly joinable?: boolean;
214
+ /**
215
+ * The referenced entity’s shape and how much of it to carry. Required when
216
+ * the field is `output` – the strategy is what decides the output shape, so
217
+ * a surfaced reference must state it; optional for a facet- or filter-only
218
+ * reference.
219
+ *
220
+ * A **discriminated union by `strategy`**, because `typeName` is load-bearing
221
+ * for two of the three and meaningless for the third: a `labelOnly` reference
222
+ * needs a name to emit its id-plus-label type under, an `inline` one needs a
223
+ * name that resolves to a declared {@link ReferenceType}, and an `idOnly` one
224
+ * carries a bare IRI that may belong to no declared type at all (`sameAs`
225
+ * points at a vocabulary nobody indexes). Declaring `typeName` on an `idOnly`
226
+ * reference is still useful where a target *is* nameable – an API surface can
227
+ * then tell IRIs of that target apart from IRIs at large.
228
+ */
195
229
  readonly ref?: {
196
230
  /** Logical API type name of the referenced entity (PascalCase, e.g.
197
- * `Organization`) – names the reference’s type in the API surfaces, the
198
- * way {@link SearchType.name} names a root type; fields sharing it share
199
- * one emitted type. For a `labelOnly`/`idOnly` reference it is a name,
200
- * not a key: it need not correspond to any indexed root type, and it may
201
- * name one (`creator` → `Person`, with the labels resolved from that
202
- * root’s collection via `labelSource`) – an API surface then serves the
203
- * reference under a derived name (GraphQL: `‹Name›Reference`) to keep
204
- * type names unique. An `inline` reference’s typeName instead resolves to
205
- * a declared {@link ReferenceType}, so it can never name a root type. */
231
+ * `Organization`) – names the reference’s type in the API surfaces,
232
+ * the way {@link SearchType.name} names a root type; fields sharing it
233
+ * share one emitted type. For a `labelOnly` reference it is a name,
234
+ * not a key: it need not correspond to any indexed root type, and it
235
+ * may name one (`creator` → `Person`, with the labels resolved from
236
+ * that root’s collection via `labelSource`) – an API surface then
237
+ * serves the reference under a derived name (GraphQL:
238
+ * `‹Name›Reference`) to keep type names unique. An `inline`
239
+ * reference’s typeName instead resolves to a declared
240
+ * {@link ReferenceType}, so it can never name a root type. */
206
241
  readonly typeName: string;
207
- /** How much of the referenced entity the reference carries. `labelOnly`
208
- * (id + display label, resolved from a label source) and `inline` (the
209
- * referent’s own projected fields, carried inline through a declared
210
- * {@link ReferenceType} – see {@link isInlineReference}) are implemented;
211
- * `idOnly` is a forward declaration, so that declarations (and the SHACL
212
- * `search:nestedStrategy` mapping) keep their shape when it lands. */
213
- readonly strategy: 'labelOnly' | 'idOnly' | 'inline';
242
+ /** `labelOnly` carries the id plus a display label resolved from a
243
+ * label source; `inline` carries the referent’s own projected fields
244
+ * through a declared {@link ReferenceType} (see
245
+ * {@link isInlineReference}). */
246
+ readonly strategy: 'labelOnly' | 'inline';
247
+ } | {
248
+ /** Optional here, unlike the other strategies: an `idOnly` reference
249
+ * emits no type of its own, so a name is not needed to emit one under.
250
+ * Declare it when the referent’s IRIs form a nameable set an API
251
+ * surface should distinguish; omit it for IRIs that belong to no such
252
+ * set. */
253
+ readonly typeName?: string;
254
+ /** The IRI, and nothing else – no label resolution, no nested fields.
255
+ * Surfaces as a bare IRI rather than an object, which is what makes it
256
+ * the right strategy for a reference whose referent is not an entity
257
+ * this deployment describes: a canonical vocabulary URI, a licence, a
258
+ * content URL. A {@link ReferenceField.labelSource} is still honoured
259
+ * for facet bucket labels – the strategy governs the output shape, not
260
+ * whether a bucket can be labelled. */
261
+ readonly strategy: 'idOnly';
214
262
  };
215
263
  }
216
264
  /**
@@ -351,7 +399,8 @@ export interface SearchSchema<Types extends readonly SearchType[] = readonly Sea
351
399
  *
352
400
  * Every declaration is validated ({@link assertValidSearchType}) – the
353
401
  * declaration-time counterpart of the port’s `assertValidQuery` – and the
354
- * schema-wide invariants are enforced: no two Root Types may share a `class` IRI
402
+ * schema-wide invariants are enforced – including the join rules, by building
403
+ * the {@link joinGraph} eagerly: no two Root Types may share a `class` IRI
355
404
  * (they would silently overwrite each other in the map) and no two types may
356
405
  * share a `name` (names key the API surfaces, across Root and Reference Types
357
406
  * alike). Every inline reference must resolve to a declared Reference Type, and
@@ -435,7 +484,7 @@ export declare function labelFieldOf(searchType: SearchType): TextField | undefi
435
484
  */
436
485
  export interface SearchTypeIssue {
437
486
  readonly field: string;
438
- readonly reason: 'duplicate-field-name' | 'invalid-field-name' | 'unknown-kind' | 'invalid-locale' | 'missing-ref' | 'ref-not-allowed' | 'text-requires-locales' | 'locales-not-allowed' | 'facet-ranges-not-allowed' | 'searchable-not-allowed' | 'transform-not-allowed' | 'derive-with-path' | 'from-not-allowed' | 'from-with-path' | 'from-with-derive' | 'from-with-inline-ref' | 'unknown-projection-value' | 'duplicate-projection-value' | 'text-not-filterable' | 'text-not-facetable' | 'reserved-field-name';
487
+ readonly reason: 'duplicate-field-name' | 'invalid-field-name' | 'unknown-kind' | 'invalid-locale' | 'missing-ref' | 'ref-not-allowed' | 'text-requires-locales' | 'locales-not-allowed' | 'facet-ranges-not-allowed' | 'searchable-not-allowed' | 'transform-not-allowed' | 'derive-with-path' | 'from-not-allowed' | 'from-with-path' | 'from-with-derive' | 'from-with-inline-ref' | 'unknown-projection-value' | 'duplicate-projection-value' | 'text-not-filterable' | 'text-not-facetable' | 'joinable-not-allowed' | 'joinable-without-label-source' | 'joinable-with-inline-ref' | 'reserved-field-name';
439
488
  }
440
489
  /**
441
490
  * The reserved logical name of a document’s IRI – the one field every indexed
@@ -478,6 +527,13 @@ export declare const OR_KEY = "or";
478
527
  * reserved name↔locale separator);
479
528
  * - a `reference` field that is `output` declares `ref` (the API surfaces
480
529
  * need the reference type name); `ref` on any other kind is meaningless;
530
+ * - `joinable` only on a `reference` field, and only alongside a `labelSource`
531
+ * – the join addresses the referent’s collection, which is the one the label
532
+ * source names, so without it the flag states an edge to nowhere – and never
533
+ * on an `inline` reference, which is stored as a nested object rather than as
534
+ * an id a reference field can point at. The schema-wide half of the rule (at
535
+ * most one joinable field per (type, label source), no cycles) is
536
+ * {@link joinGraph}’s;
481
537
  * - a `text` field declares at least one locale (`und` = untagged; projection and
482
538
  * result reconstruction have no representation for unlocalized text – use
483
539
  * `keyword` for untagged strings); `locales` on any other kind is
@@ -583,11 +639,20 @@ export declare function sortableFields(searchType: SearchType): readonly SearchF
583
639
  export declare function outputFields(searchType: SearchType): readonly SearchField[];
584
640
  /**
585
641
  * Whether a field declares **no** role – none of `output`, `searchable`,
586
- * `filterable`, `facetable`, `sortable`. Such a field is an **internal field**:
587
- * the projection populates it (so a later {@link SearchFieldBase.derive} can
588
- * read it), then prunes it before the document reaches a writer, and the engine
589
- * collection definition omits it entirely – not stored, not indexed, no RAM.
590
- * Absence of a role declares that intent; there is no separate marker flag.
642
+ * `filterable`, `facetable`, `sortable`, `joinable`. Such a field is an
643
+ * **internal field**: the projection populates it (so a later
644
+ * {@link SearchFieldBase.derive} can read it), then prunes it before the
645
+ * document reaches a writer, and the engine collection definition omits it
646
+ * entirely – not stored, not indexed, no RAM. Absence of a role declares that
647
+ * intent; there is no separate marker flag.
648
+ *
649
+ * {@link ReferenceField.joinable} counts as a role for exactly the reason the
650
+ * others do: it is a promise about what the *engine* can do with the field, and
651
+ * an engine can only join through a value it actually stores. Leaving it out
652
+ * would let a joinable-but-otherwise-role-less reference declare an edge the
653
+ * schema resolves, the query validates and the compiler emits – against a
654
+ * collection that never stored the column, so every such query fails at the
655
+ * engine.
591
656
  *
592
657
  * The single predicate the projection and the collection definition share, so
593
658
  * they cannot disagree on what is internal. See the Search context’s load-bearing
@@ -638,6 +703,24 @@ export declare function irAlias(searchType: SearchType, field: SearchField): str
638
703
  export declare function isRangeFacet(field: SearchField): field is NumericField & {
639
704
  readonly facetRanges: readonly FacetRange[];
640
705
  };
706
+ /**
707
+ * Whether a value is an **absolute IRI**: a scheme, then anything with no
708
+ * whitespace. The single definition the projection (which
709
+ * drops a reference value failing it) and an API surface (which types such a
710
+ * value as an IRI, and rejects one that is not) both read, so the two cannot
711
+ * disagree about what a reference holds – the argument {@link physicalFields}
712
+ * makes for the physical fanout.
713
+ *
714
+ * Deliberately a **scheme check, not an `http(s)` one**. `urn:`, `doi:`, `ark:`,
715
+ * `tag:`, `mailto:` and a deployment’s own minted scheme (`urn:lde:…`) are
716
+ * ordinary Linked Data, and rejecting them would make this package refuse
717
+ * conformant data. What the check does exclude is what a reference must never
718
+ * hold: a blank node label (`_:b0` – a scheme must start with a letter), a bare
719
+ * token (`boerenbont`), and a relative reference. Full RFC 3987 parsing would
720
+ * buy nothing over that while starting to reject real data with a character
721
+ * someone forgot to percent-encode.
722
+ */
723
+ export declare function isAbsoluteIri(value: string): boolean;
641
724
  /**
642
725
  * The engine storage codec for `date` fields: stored as Unix seconds (a
643
726
  * sortable, range-filterable int64), ISO 8601 at the API edges. One pair for
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAErE;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,GACnB,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,CAAC;AAE/E;oEACoE;AACpE,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,OAAO,GAAG,IAAI,CAAC;AAcnD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,GAAG,cAAc,GAAG,SAAS,CAE7E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,MAAM,WAAW,GACrB,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,YAAY,GAAG,YAAY,CAAC;AAE1E,qEAAqE;AACrE,MAAM,WAAW,eAAe;IAC9B;wCACoC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;+EAC2E;IAC3E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;2DAGuD;IACvD,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB;;yCAEqC;IACrC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,gEAAgE;IAChE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,yBAAyB;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,iCAAiC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,qFAAqF;IACrF,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAChB,QAAQ,EAAE,aAAa,EACvB,OAAO,EAAE,iBAAiB,KACvB,OAAO,CAAC;CACd;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,CAAC;AAExC;kEACkE;AAClE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,SAAU,SAAQ,eAAe,EAAE,UAAU;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,UAAU,CAAC,EAAE,KAAK,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC;IAC3B,QAAQ,CAAC,WAAW,CAAC,EAAE,KAAK,CAAC;CAC9B;AAED;gCACgC;AAChC,MAAM,WAAW,YAAa,SAAQ,eAAe,EAAE,UAAU;IAC/D,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,wEAAwE;IACxE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/C,QAAQ,CAAC,WAAW,CAAC,EAAE,KAAK,CAAC;IAC7B;8CAC0C;IAC1C,QAAQ,CAAC,IAAI,CAAC,EAAE,eAAe,CAAC;CACjC;AAED,gFAAgF;AAChF,MAAM,WAAW,cAAe,SAAQ,eAAe,EAAE,UAAU;IACjE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,uCAAuC;IACvC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/C,QAAQ,CAAC,WAAW,CAAC,EAAE,KAAK,CAAC;IAC7B;;;oCAGgC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,eAAe,CAAC;IAChC;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;0DAEsD;IACtD,QAAQ,CAAC,GAAG,CAAC,EAAE;QACb;;;;;;;;;kFAS0E;QAC1E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B;;;;;+EAKuE;QACvE,QAAQ,CAAC,QAAQ,EAAE,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAC;KACtD,CAAC;CACH;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;CAC9C;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAa,SAAQ,eAAe,EAAE,cAAc;IACnE,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC7C,QAAQ,CAAC,UAAU,CAAC,EAAE,KAAK,CAAC;CAC7B;AAED,0DAA0D;AAC1D,MAAM,WAAW,YAAa,SAAQ,eAAe;IACnD,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,KAAK,CAAC;IAC5B,QAAQ,CAAC,WAAW,CAAC,EAAE,KAAK,CAAC;CAC9B;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,+DAA+D;AAC/D,MAAM,WAAW,cAAc;IAC7B;;;;+CAI2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;2DAMuD;IACvD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;CACzC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,QAAS,SAAQ,cAAc;IAC9C;;kEAE8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAc,SAAQ,cAAc;IACnD;;;6CAGyC;IACzC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,aAAa,CAAC;AAElD;;kEAEkE;AAClE,MAAM,MAAM,UAAU,CAAC,KAAK,SAAS,SAAS,UAAU,EAAE,IAAI,OAAO,CACnE,KAAK,CAAC,MAAM,CAAC,EACb;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAC3B,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,IAAI,SAAS,UAAU,EAC5D,UAAU,EAAE,IAAI,GACf,IAAI,CAEN;AAED;;;;;;;;;;;GAWG;AACH;;;uCAGuC;AACvC,MAAM,CAAC,OAAO,CAAC,MAAM,iBAAiB,EAAE,OAAO,MAAM,CAAC;AAEtD,MAAM,WAAW,YAAY,CAC3B,KAAK,SAAS,SAAS,UAAU,EAAE,GAAG,SAAS,UAAU,EAAE,CAC3D,SAAQ,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;IAC9C,QAAQ,CAAC,CAAC,iBAAiB,CAAC,EAAE,IAAI,CAAC;CACpC;AAoBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,KAAK,CAAC,KAAK,SAAS,SAAS,UAAU,EAAE,EACpE,GAAG,KAAK,EAAE,KAAK,GACd,YAAY,CAAC,KAAK,CAAC,CAoCrB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,GACX,aAAa,GAAG,SAAS,CAE3B;AAED;6EAC6E;AAC7E,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,GACjB,KAAK,IAAI,cAAc,GAAG;IAC3B,QAAQ,CAAC,GAAG,EAAE;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;KAAE,CAAC;CAC1E,CAEA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,WAAW,GACjB,aAAa,GAAG,SAAS,CAI3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,UAAU,GACrB,MAAM,CAER;AAsJD;yCACyC;AACzC,eAAO,MAAM,mBAAmB,UAAU,CAAC;AAE3C;uDACuD;AACvD,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM,CAE/D;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,CAQ1E;AA2CD;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EACX,sBAAsB,GACtB,oBAAoB,GACpB,cAAc,GACd,gBAAgB,GAChB,aAAa,GACb,iBAAiB,GACjB,uBAAuB,GACvB,qBAAqB,GACrB,0BAA0B,GAC1B,wBAAwB,GACxB,uBAAuB,GACvB,kBAAkB,GAClB,kBAAkB,GAClB,gBAAgB,GAChB,kBAAkB,GAClB,sBAAsB,GACtB,0BAA0B,GAC1B,4BAA4B,GAC5B,qBAAqB,GACrB,oBAAoB,GACpB,qBAAqB,CAAC;CAC3B;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,QAAQ,OAAO,CAAC;AAE7B;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,OAAO,QAAQ,CAAC;AAC7B,eAAO,MAAM,MAAM,OAAO,CAAC;AAyC3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,UAAU,GACrB,SAAS,eAAe,EAAE,CAyH5B;AAYD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,QAAQ,GACnB,IAAI,CAUN;AAED;+EAC+E;AAC/E,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CAQlE;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B;sEACkE;IAClE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;4EACwE;IACxE,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAExE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,SAAS,CAOpB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,UAAU,GACrB,SAAS,CAAC,WAAW,GAAG;IACzB,QAAQ,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAClD,CAAC,EAAE,CAOH;AAED,8DAA8D;AAC9D,wBAAgB,eAAe,CAC7B,UAAU,EAAE,UAAU,GACrB,SAAS,WAAW,EAAE,CAExB;AAED,sDAAsD;AACtD,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,UAAU,GACrB,SAAS,WAAW,EAAE,CAExB;AAED,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,WAAW,EAAE,CAE7E;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,WAAW,EAAE,CAE3E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAQ3D;AAED,qFAAqF;AACrF,wBAAgB,eAAe,CAC7B,UAAU,EAAE,UAAU,GACrB,SAAS,cAAc,EAAE,CAE3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC1B,UAAU,EAAE,UAAU,GACrB,CAAC,YAAY,GAAG,cAAc,CAAC,GAAG,SAAS,CAM7C;AAED,2CAA2C;AAC3C,wBAAgB,UAAU,CACxB,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,MAAM,GACX,WAAW,GAAG,SAAS,CAEzB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,GAAG,MAAM,CAE1E;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,WAAW,GACjB,KAAK,IAAI,YAAY,GAAG;IAAE,QAAQ,CAAC,WAAW,EAAE,SAAS,UAAU,EAAE,CAAA;CAAE,CAEzE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGhE;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,cAAc,CAgBjE"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
package/dist/schema.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { joinGraph } from './join-graph.js';
1
2
  const OPERATOR_BY_KIND = {
2
3
  text: undefined,
3
4
  keyword: 'in',
@@ -49,7 +50,8 @@ function isRootType(searchType) {
49
50
  *
50
51
  * Every declaration is validated ({@link assertValidSearchType}) – the
51
52
  * declaration-time counterpart of the port’s `assertValidQuery` – and the
52
- * schema-wide invariants are enforced: no two Root Types may share a `class` IRI
53
+ * schema-wide invariants are enforced – including the join rules, by building
54
+ * the {@link joinGraph} eagerly: no two Root Types may share a `class` IRI
53
55
  * (they would silently overwrite each other in the map) and no two types may
54
56
  * share a `name` (names key the API surfaces, across Root and Reference Types
55
57
  * alike). Every inline reference must resolve to a declared Reference Type, and
@@ -84,6 +86,12 @@ export function searchSchema(...types) {
84
86
  .filter(isRootType)
85
87
  .map((searchType) => [searchType.class, searchType]));
86
88
  referenceTypesBySchema.set(schema, referenceTypes);
89
+ // Build the join graph eagerly and discard it: it is cached per schema, and
90
+ // building it is what enforces the schema-wide join rules (one joinable
91
+ // reference per target, every target an indexed Root Type, no cycles). A
92
+ // schema whose joins do not hold up therefore fails HERE, not on the first
93
+ // query or – worse – halfway through the first rebuild.
94
+ joinGraph(schema);
87
95
  return schema;
88
96
  }
89
97
  /**
@@ -385,6 +393,13 @@ const LOCALE_PATTERN = /^[A-Za-z0-9]+(-[A-Za-z0-9]+)*$/;
385
393
  * reserved name↔locale separator);
386
394
  * - a `reference` field that is `output` declares `ref` (the API surfaces
387
395
  * need the reference type name); `ref` on any other kind is meaningless;
396
+ * - `joinable` only on a `reference` field, and only alongside a `labelSource`
397
+ * – the join addresses the referent’s collection, which is the one the label
398
+ * source names, so without it the flag states an edge to nowhere – and never
399
+ * on an `inline` reference, which is stored as a nested object rather than as
400
+ * an id a reference field can point at. The schema-wide half of the rule (at
401
+ * most one joinable field per (type, label source), no cycles) is
402
+ * {@link joinGraph}’s;
388
403
  * - a `text` field declares at least one locale (`und` = untagged; projection and
389
404
  * result reconstruction have no representation for unlocalized text – use
390
405
  * `keyword` for untagged strings); `locales` on any other kind is
@@ -444,9 +459,27 @@ export function validateSearchType(searchType) {
444
459
  if (field.output === true && field.ref === undefined) {
445
460
  issue('missing-ref');
446
461
  }
462
+ // A join addresses the referent’s collection, which is the collection the
463
+ // label source names: without one the flag states an edge to nowhere.
464
+ if (field.joinable === true && field.labelSource === undefined) {
465
+ issue('joinable-without-label-source');
466
+ }
467
+ // An inline reference is stored as a NESTED OBJECT, not as an id an
468
+ // engine can point a reference field at, so the two cannot both hold:
469
+ // the collection definition would emit the nesting and silently drop the
470
+ // reference, leaving a schema whose joins validate, compile and then fail
471
+ // at the engine. Carry the referent inline or join to it, not both.
472
+ if (field.joinable === true && field.ref?.strategy === 'inline') {
473
+ issue('joinable-with-inline-ref');
474
+ }
447
475
  }
448
- else if (field.ref !== undefined) {
449
- issue('ref-not-allowed');
476
+ else {
477
+ if (field.ref !== undefined) {
478
+ issue('ref-not-allowed');
479
+ }
480
+ if (field.joinable === true) {
481
+ issue('joinable-not-allowed');
482
+ }
450
483
  }
451
484
  if (field.kind === 'text') {
452
485
  if ((field.locales ?? []).length === 0) {
@@ -622,11 +655,20 @@ export function outputFields(searchType) {
622
655
  }
623
656
  /**
624
657
  * Whether a field declares **no** role – none of `output`, `searchable`,
625
- * `filterable`, `facetable`, `sortable`. Such a field is an **internal field**:
626
- * the projection populates it (so a later {@link SearchFieldBase.derive} can
627
- * read it), then prunes it before the document reaches a writer, and the engine
628
- * collection definition omits it entirely – not stored, not indexed, no RAM.
629
- * Absence of a role declares that intent; there is no separate marker flag.
658
+ * `filterable`, `facetable`, `sortable`, `joinable`. Such a field is an
659
+ * **internal field**: the projection populates it (so a later
660
+ * {@link SearchFieldBase.derive} can read it), then prunes it before the
661
+ * document reaches a writer, and the engine collection definition omits it
662
+ * entirely – not stored, not indexed, no RAM. Absence of a role declares that
663
+ * intent; there is no separate marker flag.
664
+ *
665
+ * {@link ReferenceField.joinable} counts as a role for exactly the reason the
666
+ * others do: it is a promise about what the *engine* can do with the field, and
667
+ * an engine can only join through a value it actually stores. Leaving it out
668
+ * would let a joinable-but-otherwise-role-less reference declare an edge the
669
+ * schema resolves, the query validates and the compiler emits – against a
670
+ * collection that never stored the column, so every such query fails at the
671
+ * engine.
630
672
  *
631
673
  * The single predicate the projection and the collection definition share, so
632
674
  * they cannot disagree on what is internal. See the Search context’s load-bearing
@@ -637,7 +679,8 @@ export function isInternalField(field) {
637
679
  field.searchable === undefined &&
638
680
  field.filterable !== true &&
639
681
  field.facetable !== true &&
640
- field.sortable !== true);
682
+ field.sortable !== true &&
683
+ (field.kind !== 'reference' || field.joinable !== true));
641
684
  }
642
685
  /** Fields of kind `reference` (IRI-valued, label-resolved), in declaration order. */
643
686
  export function referenceFields(searchType) {
@@ -692,6 +735,30 @@ export function irAlias(searchType, field) {
692
735
  export function isRangeFacet(field) {
693
736
  return field.facetRanges !== undefined && field.facetRanges.length > 0;
694
737
  }
738
+ /**
739
+ * Whether a value is an **absolute IRI**: a scheme, then anything with no
740
+ * whitespace. The single definition the projection (which
741
+ * drops a reference value failing it) and an API surface (which types such a
742
+ * value as an IRI, and rejects one that is not) both read, so the two cannot
743
+ * disagree about what a reference holds – the argument {@link physicalFields}
744
+ * makes for the physical fanout.
745
+ *
746
+ * Deliberately a **scheme check, not an `http(s)` one**. `urn:`, `doi:`, `ark:`,
747
+ * `tag:`, `mailto:` and a deployment’s own minted scheme (`urn:lde:…`) are
748
+ * ordinary Linked Data, and rejecting them would make this package refuse
749
+ * conformant data. What the check does exclude is what a reference must never
750
+ * hold: a blank node label (`_:b0` – a scheme must start with a letter), a bare
751
+ * token (`boerenbont`), and a relative reference. Full RFC 3987 parsing would
752
+ * buy nothing over that while starting to reject real data with a character
753
+ * someone forgot to percent-encode.
754
+ */
755
+ export function isAbsoluteIri(value) {
756
+ return ABSOLUTE_IRI.test(value);
757
+ }
758
+ /** Scheme (RFC 3986 `ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )`), then any
759
+ * run of non-whitespace. Non-ASCII is deliberately allowed: an IRI is not
760
+ * restricted to ASCII. */
761
+ const ABSOLUTE_IRI = /^[A-Za-z][A-Za-z0-9+.-]*:\S*$/;
695
762
  /**
696
763
  * The engine storage codec for `date` fields: stored as Unix seconds (a
697
764
  * sortable, range-filterable int64), ISO 8601 at the API edges. One pair for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/search",
3
- "version": "0.18.3",
3
+ "version": "0.20.0",
4
4
  "description": "Engine- and domain-agnostic core for RDF-backed search: a unified declarative field model (SearchField/SearchType/SearchSchema), a neutral query IR, the SearchEngine port with logical result types, and a streaming CONSTRUCT-to-document projection. Bakes in no engine, protocol, or domain.",
5
5
  "repository": {
6
6
  "url": "git+https://github.com/ldelements/lde.git",