@lde/search 0.9.0 → 0.11.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/README.md +61 -27
- package/dist/adapter.d.ts +1 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +1 -1
- package/dist/engine.d.ts +5 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/frame-by-type.d.ts +12 -6
- package/dist/frame-by-type.d.ts.map +1 -1
- package/dist/frame-by-type.js +46 -12
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/project.d.ts +11 -8
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +105 -13
- package/dist/schema.d.ts +119 -33
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +139 -9
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,8 +58,7 @@ a typed, deterministic function – easy to test, and swappable per deployment.
|
|
|
58
58
|
Exports are stratified by audience:
|
|
59
59
|
|
|
60
60
|
- **`@lde/search`** – the authoring surface: `defineSearchType`,
|
|
61
|
-
`searchSchema`, `projectRoots
|
|
62
|
-
validation, and every model/query/result type.
|
|
61
|
+
`searchSchema`, `projectRoots`, validation, and every model/query/result type.
|
|
63
62
|
- **`@lde/search/adapter`** – plumbing for engine adapters and API surfaces:
|
|
64
63
|
`physicalFields`, the field selectors, `assertValidQuery`, the filter
|
|
65
64
|
operators and storage codecs.
|
|
@@ -72,11 +71,22 @@ Exports are stratified by audience:
|
|
|
72
71
|
The model has three levels, with analogues in SHACL ([one possible source](#why-a-declarative-model))
|
|
73
72
|
and GraphQL (one of the surfaces):
|
|
74
73
|
|
|
75
|
-
| Term | What it is
|
|
76
|
-
| -------------- |
|
|
77
|
-
| `SearchField` | One queryable field: a `kind`, the IR `path` it projects from, and the capability flags it opts into
|
|
78
|
-
| `SearchType` | One
|
|
79
|
-
| `SearchSchema` | The whole search declaration: every
|
|
74
|
+
| Term | What it is | SHACL | GraphQL |
|
|
75
|
+
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ----------- |
|
|
76
|
+
| `SearchField` | One queryable field: a `kind`, the IR `path` it projects from, and the capability flags it opts into | property shape | field |
|
|
77
|
+
| `SearchType` | One type’s complete declaration: its logical API `name` and fields (incl. derived). A **Root Type** declares a `class` (indexed, keys the schema); a **Reference Type** declares none (reached only through an inline reference) | NodeShape | object type |
|
|
78
|
+
| `SearchSchema` | The whole search declaration: every Root Type, keyed by `class` IRI, plus the Reference Types – build one with `searchSchema(...types)` | shapes graph | schema |
|
|
79
|
+
|
|
80
|
+
A `SearchType` is a **Root Type** or a **Reference Type**, told apart by one
|
|
81
|
+
absence: a Root Type declares a `class`, so roots are selected for it and a
|
|
82
|
+
writer opens a collection for it; a Reference Type declares none, so it is
|
|
83
|
+
never selected, framed by type or indexed – its identity is its name, and its
|
|
84
|
+
type comes from the edge that points at it. `searchSchema` partitions its
|
|
85
|
+
arguments accordingly: Root Types key the class map (`schema.values()` yields
|
|
86
|
+
only them, so no writer ever opens a collection for a Reference Type), Reference
|
|
87
|
+
Types go into a name index an inline `ref.typeName` resolves against. The
|
|
88
|
+
absence is load-bearing, and enforced at the type level – an indexed Reference
|
|
89
|
+
Type fails to compile ([ADR 11](../../docs/decisions/0011-decouple-rdf-depth-from-the-api-surface.md)).
|
|
80
90
|
|
|
81
91
|
`projectRoots` and the engine port each execute one `SearchType` at a time –
|
|
82
92
|
projection over the roots the pipeline selector supplied for that type; the
|
|
@@ -101,20 +111,19 @@ Two conventions hold across the whole family:
|
|
|
101
111
|
## Field model
|
|
102
112
|
|
|
103
113
|
The mapping is data, not code. Each field declares its `kind`, the IR `path` to
|
|
104
|
-
read (or a `derive` function for a **derived** field, computed from the
|
|
105
|
-
|
|
106
|
-
capabilities it opts into.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
read (or a `derive` function for a **derived** field, computed from the document
|
|
115
|
+
in declaration order – so it may read fields declared before it, never the
|
|
116
|
+
graph), and the capabilities (**roles**) it opts into. `path` is therefore the
|
|
117
|
+
complete statement of what the projection reads from the graph. A field that
|
|
118
|
+
declares **no** role is an **internal field**: projected so a later `derive` can
|
|
119
|
+
read it, then pruned before the writer and absent from the collection definition
|
|
120
|
+
– not stored, not indexed, no RAM. The physical field names a declaration fans
|
|
121
|
+
out to (per-locale search/sort keys) come from `physicalFields`, the single
|
|
122
|
+
convention projection, the collection definition and the query compiler all
|
|
123
|
+
share.
|
|
110
124
|
|
|
111
125
|
```ts
|
|
112
|
-
import {
|
|
113
|
-
defineSearchType,
|
|
114
|
-
projectRoots,
|
|
115
|
-
irisOf,
|
|
116
|
-
searchSchema,
|
|
117
|
-
} from '@lde/search';
|
|
126
|
+
import { defineSearchType, projectRoots, searchSchema } from '@lde/search';
|
|
118
127
|
|
|
119
128
|
const DATASET = defineSearchType({
|
|
120
129
|
name: 'Dataset', // logical API name: names the GraphQL type, a REST path, …
|
|
@@ -141,12 +150,17 @@ const DATASET = defineSearchType({
|
|
|
141
150
|
},
|
|
142
151
|
// → size (int)
|
|
143
152
|
{ name: 'size', path: 'urn:dr:size', kind: 'integer', sortable: true },
|
|
144
|
-
//
|
|
153
|
+
// internal field (no role): projected as a reading device for the derive
|
|
154
|
+
// below, then pruned before the writer – absent from the collection too
|
|
155
|
+
{ name: 'classes', path: 'urn:dr:class', kind: 'reference' },
|
|
156
|
+
// derived field (no path): computed from the document in declaration order,
|
|
157
|
+
// never from the graph – so `path` stays the whole statement of what is read
|
|
145
158
|
{
|
|
146
159
|
name: 'classCount',
|
|
147
160
|
kind: 'integer',
|
|
148
161
|
sortable: true,
|
|
149
|
-
derive: (
|
|
162
|
+
derive: (document) =>
|
|
163
|
+
(document.classes as string[] | undefined)?.length ?? 0,
|
|
150
164
|
},
|
|
151
165
|
],
|
|
152
166
|
});
|
|
@@ -205,10 +219,29 @@ directly.
|
|
|
205
219
|
| `date` | `range` (inclusive) | yes | yes | ISO 8601 string (surface) |
|
|
206
220
|
| `boolean` | `is` | yes | – | boolean (absent = false) |
|
|
207
221
|
|
|
208
|
-
A `reference` carries `labelOnly`
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
222
|
+
A `reference` carries one of two strategies today: `labelOnly` (id + display
|
|
223
|
+
label, resolved at query time from a label source) and `inline` (the referent’s
|
|
224
|
+
own projected fields, carried inline). `idOnly` stays a forward declaration.
|
|
225
|
+
|
|
226
|
+
An **inline reference** resolves `ref.typeName` to a declared **Reference Type**
|
|
227
|
+
and projects the referent through it – a nested `SearchDocument`, or an array
|
|
228
|
+
for an `array` reference. Roles decide whether the nesting surfaces, so the same
|
|
229
|
+
construct serves two jobs (see
|
|
230
|
+
[ADR 11](../../docs/decisions/0011-decouple-rdf-depth-from-the-api-surface.md)):
|
|
231
|
+
|
|
232
|
+
- a **reading device** declares no role, so it is an internal field: projected
|
|
233
|
+
so a later `derive` can select and flatten a value a `path` cannot address (a
|
|
234
|
+
qualified hop), then pruned before the writer – nothing nested reaches the
|
|
235
|
+
engine or the API;
|
|
236
|
+
- an **API device** declares `output`, deliberately surfacing the nested
|
|
237
|
+
Reference Type.
|
|
238
|
+
|
|
239
|
+
So RDF depth and API shape stay independent: inline as deep as the source
|
|
240
|
+
demands, expose exactly the flat fields you want. Framing follows the inline
|
|
241
|
+
reference graph to the depth the schema declares (`Dataset → Subset →
|
|
242
|
+
Measurement` is two hops), and `searchSchema` rejects inline cycles – the one
|
|
243
|
+
way that depth could be unbounded – so it stays a bounded property of the
|
|
244
|
+
declaration.
|
|
212
245
|
|
|
213
246
|
A reference resolves its label from a **label source**: `labelSource` names
|
|
214
247
|
the `SearchType` whose collection holds the referenced entities. The named
|
|
@@ -225,8 +258,9 @@ each root’s subgraph is framed one at a time and its document yielded as
|
|
|
225
258
|
produced, so beyond a subject index memory stays flat at scale (framing the
|
|
226
259
|
whole graph at once is roughly O(N²)). Duplicate triples are collapsed first,
|
|
227
260
|
because some SPARQL engines (e.g. QLever) do not deduplicate `CONSTRUCT` output.
|
|
228
|
-
|
|
229
|
-
|
|
261
|
+
Every predicate a value comes from is read through a field’s `path`; a `derive`
|
|
262
|
+
computes only from the document projected so far, so `path` is the whole
|
|
263
|
+
statement of what the projection reads. `assertTypeInSchema` guards that
|
|
230
264
|
the passed `SearchType` is a member of the schema – the port’s own membership
|
|
231
265
|
check – so no schema is ever forged to scope a projection to one type.
|
|
232
266
|
|
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, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, referenceFields, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
|
|
9
|
+
export { assertTypeInSchema, physicalFields, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, isInlineReference, referenceFields, referenceTypeNamed, inlineFramingDepth, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
|
|
10
10
|
export type { PhysicalFields } from './schema.js';
|
|
11
11
|
export { physicalNameTokens } from './physical-name.js';
|
|
12
12
|
export { filterOperatorFor, filterOperator, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,UAAU,EACV,YAAY,EACZ,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,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,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,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,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,6 @@
|
|
|
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, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, referenceFields, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
|
|
9
|
+
export { assertTypeInSchema, physicalFields, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, isInlineReference, referenceFields, referenceTypeNamed, inlineFramingDepth, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
|
|
10
10
|
export { physicalNameTokens } from './physical-name.js';
|
|
11
11
|
export { filterOperatorFor, filterOperator, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
|
package/dist/engine.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SearchQuery } from './query.js';
|
|
2
|
-
import type { SearchSchema, SearchType } from './schema.js';
|
|
2
|
+
import type { RootType, RootTypeOf, SearchSchema, SearchType } from './schema.js';
|
|
3
3
|
/**
|
|
4
4
|
* The engine port: the boundary a concrete engine adapter (e.g. the engine
|
|
5
5
|
* `@lde/search-typesense`’s `createTypesenseSearchEngine` returns) implements.
|
|
@@ -34,7 +34,7 @@ export interface SearchEngine<Types extends readonly SearchType[] = readonly Sea
|
|
|
34
34
|
/** The declaration this engine serves – exposed so a surface can route and
|
|
35
35
|
* a caller can enumerate the searchable types. */
|
|
36
36
|
readonly schema: SearchSchema<Types>;
|
|
37
|
-
search<T extends Types
|
|
37
|
+
search<T extends RootTypeOf<Types>>(searchType: T, query: SearchQuery): Promise<SearchResult<FacetFieldsOf<T>, OutputFieldsOf<T>>>;
|
|
38
38
|
/**
|
|
39
39
|
* The batch entry point: answer several facet-only queries against one type
|
|
40
40
|
* in a single engine round-trip (where the engine supports one, e.g.
|
|
@@ -52,7 +52,7 @@ export interface SearchEngine<Types extends readonly SearchType[] = readonly Sea
|
|
|
52
52
|
* transport itself). The port contract holds for every query in the batch;
|
|
53
53
|
* an empty `queries` resolves to `[]` without touching the engine.
|
|
54
54
|
*/
|
|
55
|
-
searchFacets<T extends Types
|
|
55
|
+
searchFacets<T extends RootTypeOf<Types>>(searchType: T, queries: readonly SearchQuery[]): Promise<readonly FacetsOutcome<FacetFieldsOf<T>>[]>;
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
58
|
* One query’s outcome in a {@link SearchEngine.searchFacets} batch – its
|
|
@@ -84,7 +84,7 @@ export type FacetMap<FacetField extends string = string> = Readonly<Partial<Reco
|
|
|
84
84
|
* `: SearchType` annotation degrades to `string` (its field names are not
|
|
85
85
|
* statically known).
|
|
86
86
|
*/
|
|
87
|
-
export type FacetFieldsOf<Type extends SearchType> =
|
|
87
|
+
export type FacetFieldsOf<Type extends SearchType> = RootType extends Type ? string : Extract<Type['fields'][number], {
|
|
88
88
|
readonly facetable: true;
|
|
89
89
|
}>['name'];
|
|
90
90
|
/**
|
|
@@ -92,7 +92,7 @@ export type FacetFieldsOf<Type extends SearchType> = SearchType extends Type ? s
|
|
|
92
92
|
* can hold. Like {@link FacetFieldsOf}, degrades to `string` for a widened
|
|
93
93
|
* declaration.
|
|
94
94
|
*/
|
|
95
|
-
export type OutputFieldsOf<Type extends SearchType> =
|
|
95
|
+
export type OutputFieldsOf<Type extends SearchType> = RootType extends Type ? string : Extract<Type['fields'][number], {
|
|
96
96
|
readonly output: true;
|
|
97
97
|
}>['name'];
|
|
98
98
|
/**
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
|
package/dist/frame-by-type.d.ts
CHANGED
|
@@ -27,15 +27,21 @@ export interface SubjectIndex {
|
|
|
27
27
|
export declare function buildSubjectIndex(quads: Iterable<Quad>): SubjectIndex;
|
|
28
28
|
/**
|
|
29
29
|
* Frame each of the given `roots` from a prebuilt {@link SubjectIndex} into one
|
|
30
|
-
* JSON-LD node. Each root subject’s own triples plus the
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* `@context`, so framed keys
|
|
30
|
+
* JSON-LD node. Each root subject’s own triples plus the nodes it references –
|
|
31
|
+
* to `depth` hops (e.g. nested publisher/distribution resources at one hop, an
|
|
32
|
+
* inline reference’s referents deeper) – are framed one subject at a time, so
|
|
33
|
+
* beyond the shared subject index only a single subgraph is held (whole-graph
|
|
34
|
+
* `jsonld.frame()` is ~O(N²)). The frame carries no `@context`, so framed keys
|
|
35
|
+
* are full predicate IRIs.
|
|
36
|
+
*
|
|
37
|
+
* `depth` is the number of reference hops to embed; it defaults to one (the
|
|
38
|
+
* long-standing single-hop embed) and comes from the schema for a type with
|
|
39
|
+
* inline references (`inlineFramingDepth`). Bounded per batch, so the O(N²)
|
|
40
|
+
* framing cost applies to a batch of roots, not the graph (ADR 12).
|
|
35
41
|
*
|
|
36
42
|
* The roots are supplied explicitly rather than discovered from `rdf:type`: the
|
|
37
43
|
* caller ({@link projectRoots}, from the pipeline selector) already holds them
|
|
38
44
|
* and passes them directly. A root absent from the index simply frames nothing.
|
|
39
45
|
*/
|
|
40
|
-
export declare function frameSubjects(index: SubjectIndex, roots: readonly string[]): AsyncIterable<FramedNode>;
|
|
46
|
+
export declare function frameSubjects(index: SubjectIndex, roots: readonly string[], depth?: number): AsyncIterable<FramedNode>;
|
|
41
47
|
//# sourceMappingURL=frame-by-type.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frame-by-type.d.ts","sourceRoot":"","sources":["../src/frame-by-type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAGzC,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAIjD;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC;CAC1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,YAAY,CAkBrE;AAED
|
|
1
|
+
{"version":3,"file":"frame-by-type.d.ts","sourceRoot":"","sources":["../src/frame-by-type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAGzC,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAIjD;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC;CAC1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,YAAY,CAkBrE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAuB,aAAa,CAClC,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,KAAK,SAAI,GACR,aAAa,CAAC,UAAU,CAAC,CAqB3B"}
|
package/dist/frame-by-type.js
CHANGED
|
@@ -34,25 +34,26 @@ export function buildSubjectIndex(quads) {
|
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* Frame each of the given `roots` from a prebuilt {@link SubjectIndex} into one
|
|
37
|
-
* JSON-LD node. Each root subject’s own triples plus the
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* `@context`, so framed keys
|
|
37
|
+
* JSON-LD node. Each root subject’s own triples plus the nodes it references –
|
|
38
|
+
* to `depth` hops (e.g. nested publisher/distribution resources at one hop, an
|
|
39
|
+
* inline reference’s referents deeper) – are framed one subject at a time, so
|
|
40
|
+
* beyond the shared subject index only a single subgraph is held (whole-graph
|
|
41
|
+
* `jsonld.frame()` is ~O(N²)). The frame carries no `@context`, so framed keys
|
|
42
|
+
* are full predicate IRIs.
|
|
43
|
+
*
|
|
44
|
+
* `depth` is the number of reference hops to embed; it defaults to one (the
|
|
45
|
+
* long-standing single-hop embed) and comes from the schema for a type with
|
|
46
|
+
* inline references (`inlineFramingDepth`). Bounded per batch, so the O(N²)
|
|
47
|
+
* framing cost applies to a batch of roots, not the graph (ADR 12).
|
|
42
48
|
*
|
|
43
49
|
* The roots are supplied explicitly rather than discovered from `rdf:type`: the
|
|
44
50
|
* caller ({@link projectRoots}, from the pipeline selector) already holds them
|
|
45
51
|
* and passes them directly. A root absent from the index simply frames nothing.
|
|
46
52
|
*/
|
|
47
|
-
export async function* frameSubjects(index, roots) {
|
|
53
|
+
export async function* frameSubjects(index, roots, depth = 1) {
|
|
48
54
|
const { bySubject } = index;
|
|
49
55
|
for (const rootIri of roots) {
|
|
50
|
-
const
|
|
51
|
-
const referenced = owned
|
|
52
|
-
.filter((quad) => quad.object.termType === 'NamedNode' ||
|
|
53
|
-
quad.object.termType === 'BlankNode')
|
|
54
|
-
.flatMap((quad) => bySubject.get(quad.object.value) ?? []);
|
|
55
|
-
const subgraph = [...owned, ...referenced];
|
|
56
|
+
const subgraph = collectSubgraph(bySubject, rootIri, depth);
|
|
56
57
|
const expanded = await jsonld.fromRDF(subgraph);
|
|
57
58
|
// Frame for THIS specific root subject by its `@id`. The subgraph embeds the
|
|
58
59
|
// root’s one-hop references, and such a referent can itself be one of the
|
|
@@ -67,3 +68,36 @@ export async function* frameSubjects(index, roots) {
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Collect a root subject’s triples plus those of every subject it references
|
|
73
|
+
* within `depth` hops, breadth-first. Each subject is visited once (so a
|
|
74
|
+
* reference cycle in the data terminates and no subject’s triples are
|
|
75
|
+
* duplicated); `jsonld.frame` embeds whatever the subgraph reaches. Bounded by
|
|
76
|
+
* `depth`, so only the reachable subgraph of one root is materialized at a time.
|
|
77
|
+
*/
|
|
78
|
+
function collectSubgraph(bySubject, rootIri, depth) {
|
|
79
|
+
const collected = [];
|
|
80
|
+
const visited = new Set();
|
|
81
|
+
let frontier = [rootIri];
|
|
82
|
+
for (let hop = 0; hop <= depth; hop++) {
|
|
83
|
+
const next = [];
|
|
84
|
+
for (const subject of frontier) {
|
|
85
|
+
if (visited.has(subject)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
visited.add(subject);
|
|
89
|
+
const owned = bySubject.get(subject) ?? [];
|
|
90
|
+
collected.push(...owned);
|
|
91
|
+
if (hop < depth) {
|
|
92
|
+
for (const quad of owned) {
|
|
93
|
+
if (quad.object.termType === 'NamedNode' ||
|
|
94
|
+
quad.object.termType === 'BlankNode') {
|
|
95
|
+
next.push(quad.object.value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
frontier = next;
|
|
101
|
+
}
|
|
102
|
+
return collected;
|
|
103
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { projectRoots
|
|
1
|
+
export { projectRoots } from './project.js';
|
|
2
2
|
export type { SearchDocument } from './project.js';
|
|
3
3
|
export { defineSearchType, searchSchema, validateSearchType, assertValidSearchType, } from './schema.js';
|
|
4
|
-
export type { FieldKind, SearchField, SearchFieldBase, TextField, KeywordField, ReferenceField, NumericField, BooleanField, SearchType, SearchTypeIssue, SearchSchema, FacetRange, } from './schema.js';
|
|
4
|
+
export type { FieldKind, SearchField, SearchFieldBase, TextField, KeywordField, ReferenceField, NumericField, BooleanField, SearchType, SearchTypeBase, RootType, ReferenceType, RootTypeOf, SearchTypeIssue, SearchSchema, FacetRange, } from './schema.js';
|
|
5
5
|
export type { SearchQuery, Filter, Sort } from './query.js';
|
|
6
6
|
export type { SearchEngine, SearchResult, SearchHit, ResultDocument, SearchValue, LocalizedValue, Reference, FacetBucket, FacetMap, FacetsOutcome, FacetFieldsOf, OutputFieldsOf, } from './engine.js';
|
|
7
7
|
export type { FramedNode } from './frame-by-type.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAKnD,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,qBAAqB,GACtB,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,GACX,MAAM,aAAa,CAAC;AAIrB,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAI5D,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,cAAc,EACd,WAAW,EACX,cAAc,EACd,SAAS,EACT,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
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// under `@lde/search/adapter`; the engine-port conformance suite under
|
|
5
5
|
// `@lde/search/testing`.
|
|
6
6
|
// Projection: RDF CONSTRUCT quads → flat search documents, driven by the
|
|
7
|
-
// unified SearchField/SearchType model.
|
|
8
|
-
//
|
|
9
|
-
export { projectRoots
|
|
7
|
+
// unified SearchField/SearchType model. A `derive` reads the projected document,
|
|
8
|
+
// never the graph, so the IR readers stay internal to projection.
|
|
9
|
+
export { projectRoots } from './project.js';
|
|
10
10
|
// Unified field model: one declaration drives projection, engine collection
|
|
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.
|
package/dist/project.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Quad } from '@rdfjs/types';
|
|
2
2
|
import { type FramedNode } from './frame-by-type.js';
|
|
3
|
-
import { type SearchSchema, type SearchType } from './schema.js';
|
|
3
|
+
import { type RootType, type SearchSchema, type SearchType } from './schema.js';
|
|
4
4
|
/** A flat search document. `id` is the engine document key. */
|
|
5
5
|
export type SearchDocument = {
|
|
6
6
|
id: string;
|
|
@@ -8,12 +8,18 @@ export type SearchDocument = {
|
|
|
8
8
|
/**
|
|
9
9
|
* Project one framed JSON-LD node into a flat search document: apply each field
|
|
10
10
|
* of the type in declaration order. A field with a `derive` function computes
|
|
11
|
-
* its value from the
|
|
12
|
-
*
|
|
11
|
+
* its value from the document as populated so far (so a derived field may read
|
|
12
|
+
* fields declared before it), never from the graph – `path` is the complete
|
|
13
|
+
* statement of what the projection reads. {@link isInternalField Internal
|
|
14
|
+
* fields} (those declaring no role) are populated so a later derive can read
|
|
15
|
+
* them, then pruned before the document is returned: they must reach neither a
|
|
16
|
+
* writer nor the collection definition. Pruning ({@link pruneInternalFields})
|
|
17
|
+
* recurses into the referents of surfaced inline references, so that invariant
|
|
18
|
+
* holds at every depth, not just the root. The physical field names a field
|
|
13
19
|
* fans out to come from {@link physicalFields}, the single source shared with
|
|
14
20
|
* the engine collection definition and the query compiler.
|
|
15
21
|
*/
|
|
16
|
-
export declare function projectDocument(node: FramedNode, searchType: SearchType): SearchDocument;
|
|
22
|
+
export declare function projectDocument(node: FramedNode, searchType: SearchType, schema?: SearchSchema): SearchDocument;
|
|
17
23
|
/**
|
|
18
24
|
* Project a single type over a known set of `roots` – the per-type, roots-given
|
|
19
25
|
* projection. The roots are supplied by the caller (the pipeline selector)
|
|
@@ -27,8 +33,5 @@ export declare function projectDocument(node: FramedNode, searchType: SearchType
|
|
|
27
33
|
* Consumes `quads` once, so it accepts any `Iterable` – a batch’s materialized
|
|
28
34
|
* array or a chained generator merging several readers.
|
|
29
35
|
*/
|
|
30
|
-
export declare function projectRoots(quads: Iterable<Quad>, roots: readonly string[], schema: SearchSchema, searchType:
|
|
31
|
-
export declare function literalsOf(node: FramedNode, path: string): string[];
|
|
32
|
-
export declare function firstLiteralOf(node: FramedNode, path: string): string | undefined;
|
|
33
|
-
export declare function irisOf(node: FramedNode, path: string): string[];
|
|
36
|
+
export declare function projectRoots(quads: Iterable<Quad>, roots: readonly string[], schema: SearchSchema, searchType: RootType): AsyncIterable<SearchDocument>;
|
|
34
37
|
//# sourceMappingURL=project.d.ts.map
|
package/dist/project.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,
|
|
1
|
+
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAWL,KAAK,QAAQ,EAEb,KAAK,YAAY,EACjB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AAErB,+DAA+D;AAC/D,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEtE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,UAAU,EACtB,MAAM,CAAC,EAAE,YAAY,GACpB,cAAc,CAIhB;AAiED;;;;;;;;;;;;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,GACnB,aAAa,CAAC,cAAc,CAAC,CAW/B"}
|
package/dist/project.js
CHANGED
|
@@ -1,22 +1,73 @@
|
|
|
1
1
|
import { fold } from '@lde/text-normalization';
|
|
2
2
|
import { buildSubjectIndex, frameSubjects, } from './frame-by-type.js';
|
|
3
|
-
import { assertTypeInSchema, displayFieldName, isoToUnixSeconds, physicalFields, } from './schema.js';
|
|
3
|
+
import { assertTypeInSchema, displayFieldName, inlineFramingDepth, isInternalField, isInlineReference, isoToUnixSeconds, physicalFields, referenceTypeNamed, } from './schema.js';
|
|
4
4
|
/**
|
|
5
5
|
* Project one framed JSON-LD node into a flat search document: apply each field
|
|
6
6
|
* of the type in declaration order. A field with a `derive` function computes
|
|
7
|
-
* its value from the
|
|
8
|
-
*
|
|
7
|
+
* its value from the document as populated so far (so a derived field may read
|
|
8
|
+
* fields declared before it), never from the graph – `path` is the complete
|
|
9
|
+
* statement of what the projection reads. {@link isInternalField Internal
|
|
10
|
+
* fields} (those declaring no role) are populated so a later derive can read
|
|
11
|
+
* them, then pruned before the document is returned: they must reach neither a
|
|
12
|
+
* writer nor the collection definition. Pruning ({@link pruneInternalFields})
|
|
13
|
+
* recurses into the referents of surfaced inline references, so that invariant
|
|
14
|
+
* holds at every depth, not just the root. The physical field names a field
|
|
9
15
|
* fans out to come from {@link physicalFields}, the single source shared with
|
|
10
16
|
* the engine collection definition and the query compiler.
|
|
11
17
|
*/
|
|
12
|
-
export function projectDocument(node, searchType) {
|
|
18
|
+
export function projectDocument(node, searchType, schema) {
|
|
19
|
+
const document = projectFields(node, searchType, schema);
|
|
20
|
+
pruneInternalFields(document, searchType, schema);
|
|
21
|
+
return document;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Prune every internal field from a fully projected document, in place. Runs
|
|
25
|
+
* only after all projection – so every derive that might read an internal
|
|
26
|
+
* field, at any depth, has already run – which makes this single post-order
|
|
27
|
+
* pass safe. A no-role inline reference is itself internal, so it is deleted
|
|
28
|
+
* whole here; a surfaced (`output`) inline reference survives, but its own
|
|
29
|
+
* internal helper fields are pruned from the nested document. That keeps the
|
|
30
|
+
* *a field without a role reaches neither the engine nor the API* invariant
|
|
31
|
+
* true at every depth of the reference graph, not just at the root.
|
|
32
|
+
*/
|
|
33
|
+
function pruneInternalFields(document, searchType, schema) {
|
|
34
|
+
for (const field of searchType.fields) {
|
|
35
|
+
if (isInternalField(field)) {
|
|
36
|
+
delete document[field.name];
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
// A surfaced inline reference nests its referent(s) as SearchDocument(s);
|
|
40
|
+
// prune those too, by their reference type.
|
|
41
|
+
if (schema === undefined || field.kind !== 'reference') {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const ref = field.ref;
|
|
45
|
+
if (ref?.strategy !== 'inline') {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const referenceType = referenceTypeNamed(schema, ref.typeName);
|
|
49
|
+
const nested = document[field.name];
|
|
50
|
+
if (referenceType === undefined || nested === undefined) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
for (const referent of Array.isArray(nested) ? nested : [nested]) {
|
|
54
|
+
pruneInternalFields(referent, referenceType, schema);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Apply every field of `searchType` to a fresh document, without pruning – the
|
|
59
|
+
* shared core of {@link projectDocument} and inline-referent projection.
|
|
60
|
+
* Pruning is deferred to a single recursive pass ({@link pruneInternalFields})
|
|
61
|
+
* once the whole nested structure is projected, so a derive at any depth can
|
|
62
|
+
* still read an internal field before it is removed. */
|
|
63
|
+
function projectFields(node, searchType, schema) {
|
|
13
64
|
const id = node['@id'];
|
|
14
65
|
if (typeof id !== 'string') {
|
|
15
|
-
throw new Error(`Cannot project a
|
|
66
|
+
throw new Error(`Cannot project a “${searchType.name}” node without an @id: every search document needs a stable key, and an empty one would collide with other keyless nodes.`);
|
|
16
67
|
}
|
|
17
68
|
const document = { id };
|
|
18
69
|
for (const field of searchType.fields) {
|
|
19
|
-
applyField(document, node, field);
|
|
70
|
+
applyField(document, node, field, schema);
|
|
20
71
|
}
|
|
21
72
|
return document;
|
|
22
73
|
}
|
|
@@ -40,13 +91,14 @@ export async function* projectRoots(quads, roots, schema, searchType) {
|
|
|
40
91
|
// non-`DISTINCT` `SELECT` over a one-to-many join yields the same subject per
|
|
41
92
|
// matched row – and a repeated root would otherwise frame and emit a
|
|
42
93
|
// duplicate document under the same `id`.
|
|
43
|
-
|
|
44
|
-
|
|
94
|
+
const depth = inlineFramingDepth(schema, searchType);
|
|
95
|
+
for await (const node of frameSubjects(index, [...new Set(roots)], depth)) {
|
|
96
|
+
yield projectDocument(node, searchType, schema);
|
|
45
97
|
}
|
|
46
98
|
}
|
|
47
|
-
function applyField(document, node, field) {
|
|
99
|
+
function applyField(document, node, field, schema) {
|
|
48
100
|
if (field.derive !== undefined) {
|
|
49
|
-
const value = field.derive(
|
|
101
|
+
const value = field.derive(document);
|
|
50
102
|
if (value !== undefined) {
|
|
51
103
|
document[field.name] = value;
|
|
52
104
|
}
|
|
@@ -57,6 +109,16 @@ function applyField(document, node, field) {
|
|
|
57
109
|
// Neither path nor derive: populated outside the projection, if at all.
|
|
58
110
|
return;
|
|
59
111
|
}
|
|
112
|
+
if (isInlineReference(field)) {
|
|
113
|
+
// An inline reference is a nested structure, not a bare IRI: it can only be
|
|
114
|
+
// projected with the schema that declares its reference type. Without one,
|
|
115
|
+
// project nothing rather than fall through and emit the referent IRIs under
|
|
116
|
+
// the field name (the wrong shape).
|
|
117
|
+
if (schema !== undefined) {
|
|
118
|
+
applyInlineReference(document, node, path, field, schema);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
60
122
|
switch (field.kind) {
|
|
61
123
|
case 'text':
|
|
62
124
|
return applyText(document, langValuesOf(node, path), field);
|
|
@@ -141,20 +203,50 @@ function applyFacet(document, raw, field) {
|
|
|
141
203
|
setArray(document, physicalFields(field).search[0], dedupe(values.map((value) => fold(value))));
|
|
142
204
|
}
|
|
143
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Project an inline reference: the referent node(s) embedded under `path` are
|
|
208
|
+
* each projected through the reference’s {@link ReferenceType} (its fields’
|
|
209
|
+
* paths relative to the referent node) and attached under the field’s name – a
|
|
210
|
+
* nested {@link SearchDocument} for a single reference, an array for an
|
|
211
|
+
* `array` one. The referent is projected in full – internal fields included –
|
|
212
|
+
* so the declaring type’s (or the reference type’s own) derives can read them;
|
|
213
|
+
* {@link pruneInternalFields} then removes the internal fields from a *surfaced*
|
|
214
|
+
* referent and deletes an internal inline reference whole. Recurses through
|
|
215
|
+
* `schema`, so an inline reference may itself carry further inline references to
|
|
216
|
+
* the schema’s declared depth. The referent type is guaranteed declared by
|
|
217
|
+
* {@link searchSchema}.
|
|
218
|
+
*/
|
|
219
|
+
function applyInlineReference(document, node, path, field, schema) {
|
|
220
|
+
// Resolves for a schema that declares the referent (always so for the schema a
|
|
221
|
+
// type is projected through); a type framed against a foreign schema that
|
|
222
|
+
// omits it simply contributes no nesting.
|
|
223
|
+
const referenceType = referenceTypeNamed(schema, field.ref.typeName);
|
|
224
|
+
if (referenceType === undefined) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const referents = valuesOf(node, path)
|
|
228
|
+
.filter(isObject)
|
|
229
|
+
.filter((referent) => typeof referent['@id'] === 'string')
|
|
230
|
+
.map((referent) => projectFields(referent, referenceType, schema));
|
|
231
|
+
if (referents.length === 0) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
document[field.name] = field.array === true ? referents : referents[0];
|
|
235
|
+
}
|
|
144
236
|
function langValuesOf(node, path) {
|
|
145
237
|
return valuesOf(node, path)
|
|
146
238
|
.map(toLangValue)
|
|
147
239
|
.filter((value) => value !== undefined);
|
|
148
240
|
}
|
|
149
|
-
|
|
241
|
+
function literalsOf(node, path) {
|
|
150
242
|
return valuesOf(node, path)
|
|
151
243
|
.map(literalString)
|
|
152
244
|
.filter((value) => value !== undefined);
|
|
153
245
|
}
|
|
154
|
-
|
|
246
|
+
function firstLiteralOf(node, path) {
|
|
155
247
|
return literalsOf(node, path)[0];
|
|
156
248
|
}
|
|
157
|
-
|
|
249
|
+
function irisOf(node, path) {
|
|
158
250
|
return valuesOf(node, path)
|
|
159
251
|
.map(iriString)
|
|
160
252
|
.filter((value) => value !== undefined);
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { FramedNode } from './frame-by-type.js';
|
|
2
1
|
import type { SearchDocument } from './project.js';
|
|
3
2
|
/**
|
|
4
3
|
* The engine-neutral kind of a queryable field. It drives every downstream
|
|
@@ -32,9 +31,11 @@ export declare function filterOperatorFor(kind: FieldKind): FilterOperator | und
|
|
|
32
31
|
* Capability flags (`searchable`/`filterable`/`facetable`/`sortable`/`output`)
|
|
33
32
|
* are independent opt-ins: a field exposes exactly the roles it declares. A
|
|
34
33
|
* field with a {@link SearchFieldBase.derive `derive`} function instead of a
|
|
35
|
-
* `path` is a **derived field** – computed from the
|
|
36
|
-
*
|
|
37
|
-
* behavior (e.g. `status`, the compatibility booleans).
|
|
34
|
+
* `path` is a **derived field** – computed from the document projected so far
|
|
35
|
+
* rather than read from the graph – yet it still carries full query/schema/output
|
|
36
|
+
* behavior (e.g. `status`, the compatibility booleans). A field declaring **no**
|
|
37
|
+
* role at all is an {@link isInternalField **internal field**}: projected so a
|
|
38
|
+
* later derive can read it, then pruned before the writer.
|
|
38
39
|
*
|
|
39
40
|
* The physical field names a declaration fans out to (per-locale search/sort
|
|
40
41
|
* keys) follow one convention, owned by
|
|
@@ -73,15 +74,17 @@ export interface SearchFieldBase {
|
|
|
73
74
|
readonly sortable?: boolean;
|
|
74
75
|
/**
|
|
75
76
|
* Compute this field’s value instead of projecting it from a `path` – a
|
|
76
|
-
* status token, a compatibility boolean, a count over
|
|
77
|
+
* status token, a compatibility boolean, a count over an earlier field.
|
|
77
78
|
* Mutually exclusive with `path`. Runs in declaration order during
|
|
78
|
-
* projection, receiving
|
|
79
|
-
*
|
|
80
|
-
* `statusRank` reading the derived `status
|
|
81
|
-
*
|
|
82
|
-
*
|
|
79
|
+
* projection, receiving **only** the document as populated so far – never the
|
|
80
|
+
* graph – so a derived field reads fields declared before it (e.g. a
|
|
81
|
+
* `statusRank` reading the derived `status`, or a count reading an
|
|
82
|
+
* {@link isInternalField internal} field). Return `undefined` to leave the
|
|
83
|
+
* field absent. The field still carries full query/schema/output behaviour
|
|
84
|
+
* like any other. Reading only the document is what keeps `path` the complete
|
|
85
|
+
* statement of what the projection reads from the graph.
|
|
83
86
|
*/
|
|
84
|
-
readonly derive?: (
|
|
87
|
+
readonly derive?: (document: SearchDocument) => unknown;
|
|
85
88
|
}
|
|
86
89
|
/** Full-text inclusion with a `query_by` weight (folded; per-locale for
|
|
87
90
|
* localized text). Presence is what makes a field searchable. */
|
|
@@ -191,26 +194,60 @@ export interface FacetRange {
|
|
|
191
194
|
readonly min?: number;
|
|
192
195
|
readonly max?: number;
|
|
193
196
|
}
|
|
194
|
-
/**
|
|
195
|
-
|
|
196
|
-
* `class` IRI its documents are instances of (the RDF class), and the queryable
|
|
197
|
-
* `fields` (including {@link SearchField.derive derived} ones). A SHACL
|
|
198
|
-
* generator can emit one per NodeShape (`name`←`sh:name`/local name,
|
|
199
|
-
* `class`←`sh:targetClass`, `fields`←its property shapes), but that is a source,
|
|
200
|
-
* not a requirement.
|
|
201
|
-
*/
|
|
202
|
-
export interface SearchType {
|
|
197
|
+
/** The declaration members every {@link SearchType} shares. */
|
|
198
|
+
export interface SearchTypeBase {
|
|
203
199
|
/** Logical API name (PascalCase, e.g. `Dataset`) – names the type in the API
|
|
204
200
|
* surfaces (GraphQL type names, a REST path), the way each field’s
|
|
205
201
|
* {@link SearchField.name} names that field. Deliberately declared rather
|
|
206
202
|
* than derived from the `class` IRI, so re-modelling the vocabulary cannot
|
|
207
203
|
* silently rename the public contract. */
|
|
208
204
|
readonly name: string;
|
|
205
|
+
readonly fields: readonly SearchField[];
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* A **Root Type**: a {@link SearchType} that is indexed. It declares a `class`,
|
|
209
|
+
* roots are selected for it, a Writer owns a collection for it, and the
|
|
210
|
+
* {@link SearchSchema} is keyed by it. A SHACL generator can emit one per
|
|
211
|
+
* NodeShape (`name`←`sh:name`/local name, `class`←`sh:targetClass`,
|
|
212
|
+
* `fields`←its property shapes), but that is a source, not a requirement.
|
|
213
|
+
*/
|
|
214
|
+
export interface RootType extends SearchTypeBase {
|
|
209
215
|
/** The RDF class IRI its documents are instances of (`sh:targetClass`); the
|
|
210
|
-
* key a {@link SearchSchema} maps this type under.
|
|
216
|
+
* key a {@link SearchSchema} maps this type under. Its presence is what makes
|
|
217
|
+
* a type a Root Type – and so what gives it a collection. */
|
|
211
218
|
readonly class: string;
|
|
212
|
-
readonly fields: readonly SearchField[];
|
|
213
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* A **Reference Type**: a {@link SearchType} reached only through an
|
|
222
|
+
* {@link ReferenceField.ref inline reference}. It declares **no `class`** –
|
|
223
|
+
* never selected, never framed by type, never indexed; its identity is its
|
|
224
|
+
* `name`, and its type comes from the edge that points at it, not from the node.
|
|
225
|
+
* The absence is load-bearing, not stylistic: a `class` would put it in the
|
|
226
|
+
* {@link SearchSchema} map and silently earn it a collection nobody asked for.
|
|
227
|
+
* The shape an inline reference carries – see
|
|
228
|
+
* [ADR 11](../../docs/decisions/0011-decouple-rdf-depth-from-the-api-surface.md).
|
|
229
|
+
*/
|
|
230
|
+
export interface ReferenceType extends SearchTypeBase {
|
|
231
|
+
/** A Reference Type declares no class; declaring one makes it a
|
|
232
|
+
* {@link RootType}. Typed as `never` so the two shapes discriminate the way
|
|
233
|
+
* {@link SearchField} discriminates by `kind`: an indexed Reference Type
|
|
234
|
+
* fails to compile, not at run time. */
|
|
235
|
+
readonly class?: never;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* One type’s complete search declaration: its logical API `name`, the queryable
|
|
239
|
+
* `fields` (including {@link SearchField.derive derived} ones), and – for a
|
|
240
|
+
* {@link RootType} – the RDF `class` its documents are instances of. Either a
|
|
241
|
+
* Root Type or a {@link ReferenceType}; the absence of a `class` tells them
|
|
242
|
+
* apart.
|
|
243
|
+
*/
|
|
244
|
+
export type SearchType = RootType | ReferenceType;
|
|
245
|
+
/** The Root Types among a declared tuple – the ones a {@link SearchSchema}
|
|
246
|
+
* keys and a Writer opens a collection for. Reference Types are excluded, so
|
|
247
|
+
* no consumer that iterates `schema.values()` ever meets one. */
|
|
248
|
+
export type RootTypeOf<Types extends readonly SearchType[]> = Extract<Types[number], {
|
|
249
|
+
readonly class: string;
|
|
250
|
+
}>;
|
|
214
251
|
/**
|
|
215
252
|
* Declare a {@link SearchType}, capturing it as a literal: the `const` type
|
|
216
253
|
* parameter preserves the field names and capability flags that the type-level
|
|
@@ -221,31 +258,67 @@ export interface SearchType {
|
|
|
221
258
|
*/
|
|
222
259
|
export declare function defineSearchType<const Type extends SearchType>(searchType: Type): Type;
|
|
223
260
|
/**
|
|
224
|
-
* The complete search declaration of a deployment: every
|
|
225
|
-
* keyed by its `class` IRI
|
|
226
|
-
*
|
|
227
|
-
*
|
|
261
|
+
* The complete search declaration of a deployment: every {@link RootType},
|
|
262
|
+
* keyed by its `class` IRI, plus the {@link ReferenceType}s an inline reference
|
|
263
|
+
* resolves against. Build one with {@link searchSchema}, which captures the
|
|
264
|
+
* declared types as a literal tuple (`Types`), so schema-bound consumers (the
|
|
265
|
+
* engine port) can type their per-type behaviour off it. A plain
|
|
228
266
|
* `: SearchSchema` annotation widens gracefully to `SearchType`.
|
|
267
|
+
*
|
|
268
|
+
* `values()` yields only Root Types – Reference Types are held apart in a name
|
|
269
|
+
* index ({@link referenceTypeNamed}), so a Writer that opens one collection per
|
|
270
|
+
* `values()` entry can never open one for a Reference Type.
|
|
229
271
|
*/
|
|
230
272
|
/** Brand for {@link SearchSchema}: type-only, no runtime existence. Makes the
|
|
231
273
|
* schema NOMINAL – a hand-built `Map` is not assignable, so `searchSchema()`
|
|
232
274
|
* (which validates) is the only way to obtain one and downstream consumers
|
|
233
275
|
* need no defensive re-validation. */
|
|
234
276
|
export declare const validSearchSchema: unique symbol;
|
|
235
|
-
export interface SearchSchema<Types extends readonly SearchType[] = readonly SearchType[]> extends ReadonlyMap<string, Types
|
|
277
|
+
export interface SearchSchema<Types extends readonly SearchType[] = readonly SearchType[]> extends ReadonlyMap<string, RootTypeOf<Types>> {
|
|
236
278
|
readonly [validSearchSchema]: true;
|
|
237
279
|
}
|
|
238
280
|
/**
|
|
239
|
-
* Build a {@link SearchSchema} from
|
|
281
|
+
* Build a {@link SearchSchema} from type declarations. Its arguments are
|
|
282
|
+
* **partitioned**: {@link RootType}s (those declaring a `class`) key the map –
|
|
283
|
+
* so a Writer opens exactly one collection per Root Type – and
|
|
284
|
+
* {@link ReferenceType}s (no `class`) go into a name index that an inline
|
|
285
|
+
* `ref.typeName` resolves against ({@link referenceTypeNamed}).
|
|
240
286
|
*
|
|
241
287
|
* Every declaration is validated ({@link assertValidSearchType}) – the
|
|
242
288
|
* declaration-time counterpart of the port’s `assertValidQuery` – and the
|
|
243
|
-
* schema-wide invariants are enforced: no two
|
|
244
|
-
* (they would silently overwrite each other in the map)
|
|
245
|
-
* key the API surfaces
|
|
246
|
-
*
|
|
289
|
+
* schema-wide invariants are enforced: no two Root Types may share a `class` IRI
|
|
290
|
+
* (they would silently overwrite each other in the map) and no two types may
|
|
291
|
+
* share a `name` (names key the API surfaces, across Root and Reference Types
|
|
292
|
+
* alike). Every inline reference must resolve to a declared Reference Type, and
|
|
293
|
+
* the inline reference graph must be acyclic – the only way its depth could be
|
|
294
|
+
* unbounded. Throws on the first invalid declaration, so a bad schema fails at
|
|
295
|
+
* startup, not per document at index time or per query.
|
|
247
296
|
*/
|
|
248
297
|
export declare function searchSchema<const Types extends readonly SearchType[]>(...types: Types): SearchSchema<Types>;
|
|
298
|
+
/**
|
|
299
|
+
* The {@link ReferenceType} an inline `ref.typeName` names, or `undefined` when
|
|
300
|
+
* the schema declares none by that name. The read side of the name index
|
|
301
|
+
* {@link searchSchema} partitions the Reference Types into – the projection
|
|
302
|
+
* resolves an inline reference’s referent shape through it.
|
|
303
|
+
*/
|
|
304
|
+
export declare function referenceTypeNamed(schema: SearchSchema, name: string): ReferenceType | undefined;
|
|
305
|
+
/** Whether a field is an inline reference – a {@link ReferenceField} whose
|
|
306
|
+
* `ref` carries its referent’s projected fields ({@link ReferenceType}). */
|
|
307
|
+
export declare function isInlineReference(field: SearchField): field is ReferenceField & {
|
|
308
|
+
readonly ref: {
|
|
309
|
+
readonly typeName: string;
|
|
310
|
+
readonly strategy: 'inline';
|
|
311
|
+
};
|
|
312
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* The framing depth a Root Type needs: how many hops the inline reference graph
|
|
315
|
+
* reaches from it (`Dataset → Subset → Measurement` is two), floored at one so
|
|
316
|
+
* the existing single-hop embed for non-inline references is preserved. Depth is
|
|
317
|
+
* a property of the declaration, bounded because {@link searchSchema} rejects
|
|
318
|
+
* inline cycles – never a knob or a constant. Framing bounded per batch keeps
|
|
319
|
+
* memory bounded by the unit of work (ADR 12), not the graph.
|
|
320
|
+
*/
|
|
321
|
+
export declare function inlineFramingDepth(schema: SearchSchema, searchType: SearchType): number;
|
|
249
322
|
/**
|
|
250
323
|
* The text field a label source serves labels from – the ‘label’ convention
|
|
251
324
|
* in one place: an `output` (something to reconstruct a label from),
|
|
@@ -301,7 +374,7 @@ export declare function validateSearchType(searchType: SearchType): readonly Sea
|
|
|
301
374
|
* meet an index the deployment did not declare. Identity-based: the exact
|
|
302
375
|
* declaration object must be in the schema, not a lookalike.
|
|
303
376
|
*/
|
|
304
|
-
export declare function assertTypeInSchema(schema: SearchSchema, searchType:
|
|
377
|
+
export declare function assertTypeInSchema(schema: SearchSchema, searchType: RootType): void;
|
|
305
378
|
/** Throw on a structurally invalid {@link SearchType} ({@link validateSearchType}),
|
|
306
379
|
* naming every issue. Called by {@link searchSchema} for each declaration. */
|
|
307
380
|
export declare function assertValidSearchType(searchType: SearchType): void;
|
|
@@ -374,6 +447,19 @@ export declare function filterableFields(searchType: SearchType): readonly Searc
|
|
|
374
447
|
export declare function sortableFields(searchType: SearchType): readonly SearchField[];
|
|
375
448
|
/** Fields that appear in the API output type, in declaration order. */
|
|
376
449
|
export declare function outputFields(searchType: SearchType): readonly SearchField[];
|
|
450
|
+
/**
|
|
451
|
+
* Whether a field declares **no** role – none of `output`, `searchable`,
|
|
452
|
+
* `filterable`, `facetable`, `sortable`. Such a field is an **internal field**:
|
|
453
|
+
* the projection populates it (so a later {@link SearchFieldBase.derive} can
|
|
454
|
+
* read it), then prunes it before the document reaches a writer, and the engine
|
|
455
|
+
* collection definition omits it entirely – not stored, not indexed, no RAM.
|
|
456
|
+
* Absence of a role declares that intent; there is no separate marker flag.
|
|
457
|
+
*
|
|
458
|
+
* The single predicate the projection and the collection definition share, so
|
|
459
|
+
* they cannot disagree on what is internal. See the Search context’s load-bearing
|
|
460
|
+
* line: *a field without a Role is an Internal Field.*
|
|
461
|
+
*/
|
|
462
|
+
export declare function isInternalField(field: SearchField): boolean;
|
|
377
463
|
/** Fields of kind `reference` (IRI-valued, label-resolved), in declaration order. */
|
|
378
464
|
export declare function referenceFields(searchType: SearchType): readonly ReferenceField[];
|
|
379
465
|
/** Look up a field by its logical name. */
|
package/dist/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/schema.js
CHANGED
|
@@ -28,32 +28,143 @@ export function defineSearchType(searchType) {
|
|
|
28
28
|
return searchType;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* The Reference Type name index each {@link SearchSchema} carries alongside its
|
|
32
|
+
* class-keyed root map, kept out of the map itself so no consumer iterating
|
|
33
|
+
* `values()` ever meets a Reference Type. Held in a `WeakMap` rather than a
|
|
34
|
+
* property, so the schema stays a plain branded `Map` and the index is read only
|
|
35
|
+
* through {@link referenceTypeNamed}.
|
|
36
|
+
*/
|
|
37
|
+
const referenceTypesBySchema = new WeakMap();
|
|
38
|
+
/** Whether a declared type is a {@link RootType} (declares a `class`) rather
|
|
39
|
+
* than a {@link ReferenceType}. */
|
|
40
|
+
function isRootType(searchType) {
|
|
41
|
+
return searchType.class !== undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build a {@link SearchSchema} from type declarations. Its arguments are
|
|
45
|
+
* **partitioned**: {@link RootType}s (those declaring a `class`) key the map –
|
|
46
|
+
* so a Writer opens exactly one collection per Root Type – and
|
|
47
|
+
* {@link ReferenceType}s (no `class`) go into a name index that an inline
|
|
48
|
+
* `ref.typeName` resolves against ({@link referenceTypeNamed}).
|
|
32
49
|
*
|
|
33
50
|
* Every declaration is validated ({@link assertValidSearchType}) – the
|
|
34
51
|
* declaration-time counterpart of the port’s `assertValidQuery` – and the
|
|
35
|
-
* schema-wide invariants are enforced: no two
|
|
36
|
-
* (they would silently overwrite each other in the map)
|
|
37
|
-
* key the API surfaces
|
|
38
|
-
*
|
|
52
|
+
* schema-wide invariants are enforced: no two Root Types may share a `class` IRI
|
|
53
|
+
* (they would silently overwrite each other in the map) and no two types may
|
|
54
|
+
* share a `name` (names key the API surfaces, across Root and Reference Types
|
|
55
|
+
* alike). Every inline reference must resolve to a declared Reference Type, and
|
|
56
|
+
* the inline reference graph must be acyclic – the only way its depth could be
|
|
57
|
+
* unbounded. Throws on the first invalid declaration, so a bad schema fails at
|
|
58
|
+
* startup, not per document at index time or per query.
|
|
39
59
|
*/
|
|
40
60
|
export function searchSchema(...types) {
|
|
41
61
|
const typeIris = new Set();
|
|
42
62
|
const names = new Set();
|
|
43
63
|
for (const searchType of types) {
|
|
44
64
|
assertValidSearchType(searchType);
|
|
45
|
-
if (
|
|
46
|
-
|
|
65
|
+
if (isRootType(searchType)) {
|
|
66
|
+
if (typeIris.has(searchType.class)) {
|
|
67
|
+
throw new Error(`Duplicate search type IRI “${searchType.class}”; each Root Type must declare a distinct class.`);
|
|
68
|
+
}
|
|
69
|
+
typeIris.add(searchType.class);
|
|
47
70
|
}
|
|
48
71
|
if (names.has(searchType.name)) {
|
|
49
72
|
throw new Error(`Duplicate search type name “${searchType.name}”; each SearchType must declare a distinct name.`);
|
|
50
73
|
}
|
|
51
|
-
typeIris.add(searchType.class);
|
|
52
74
|
names.add(searchType.name);
|
|
53
75
|
}
|
|
76
|
+
const referenceTypes = new Map(types
|
|
77
|
+
.filter((searchType) => !isRootType(searchType))
|
|
78
|
+
.map((searchType) => [searchType.name, searchType]));
|
|
79
|
+
assertResolvableInlineReferences(types, referenceTypes);
|
|
54
80
|
assertResolvableLabelSources(types);
|
|
55
81
|
// The one blessed cast: only this validated constructor mints the brand.
|
|
56
|
-
|
|
82
|
+
const schema = new Map(types
|
|
83
|
+
.filter(isRootType)
|
|
84
|
+
.map((searchType) => [searchType.class, searchType]));
|
|
85
|
+
referenceTypesBySchema.set(schema, referenceTypes);
|
|
86
|
+
return schema;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The {@link ReferenceType} an inline `ref.typeName` names, or `undefined` when
|
|
90
|
+
* the schema declares none by that name. The read side of the name index
|
|
91
|
+
* {@link searchSchema} partitions the Reference Types into – the projection
|
|
92
|
+
* resolves an inline reference’s referent shape through it.
|
|
93
|
+
*/
|
|
94
|
+
export function referenceTypeNamed(schema, name) {
|
|
95
|
+
return referenceTypesBySchema.get(schema)?.get(name);
|
|
96
|
+
}
|
|
97
|
+
/** Whether a field is an inline reference – a {@link ReferenceField} whose
|
|
98
|
+
* `ref` carries its referent’s projected fields ({@link ReferenceType}). */
|
|
99
|
+
export function isInlineReference(field) {
|
|
100
|
+
return field.kind === 'reference' && field.ref?.strategy === 'inline';
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The framing depth a Root Type needs: how many hops the inline reference graph
|
|
104
|
+
* reaches from it (`Dataset → Subset → Measurement` is two), floored at one so
|
|
105
|
+
* the existing single-hop embed for non-inline references is preserved. Depth is
|
|
106
|
+
* a property of the declaration, bounded because {@link searchSchema} rejects
|
|
107
|
+
* inline cycles – never a knob or a constant. Framing bounded per batch keeps
|
|
108
|
+
* memory bounded by the unit of work (ADR 12), not the graph.
|
|
109
|
+
*/
|
|
110
|
+
export function inlineFramingDepth(schema, searchType) {
|
|
111
|
+
return Math.max(1, inlineChainLength(schema, searchType));
|
|
112
|
+
}
|
|
113
|
+
/** The longest inline reference chain reachable from `searchType`. The recursion
|
|
114
|
+
* terminates because {@link searchSchema} rejects inline cycles; a reference the
|
|
115
|
+
* given schema does not declare (e.g. a type framed against another schema)
|
|
116
|
+
* contributes no depth. */
|
|
117
|
+
function inlineChainLength(schema, searchType) {
|
|
118
|
+
let longest = 0;
|
|
119
|
+
for (const field of searchType.fields) {
|
|
120
|
+
if (!isInlineReference(field)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const referent = referenceTypeNamed(schema, field.ref.typeName);
|
|
124
|
+
if (referent === undefined) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
longest = Math.max(longest, 1 + inlineChainLength(schema, referent));
|
|
128
|
+
}
|
|
129
|
+
return longest;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Every inline `ref.typeName` must resolve to a **declared Reference Type**, and
|
|
133
|
+
* the inline reference graph must be acyclic. Unlike a `labelOnly` reference –
|
|
134
|
+
* whose `typeName` is just an API name – an inline reference carries its
|
|
135
|
+
* referent’s fields, so it must know their shape, and an inline cycle is the one
|
|
136
|
+
* way framing depth could be unbounded. Checked schema-wide, because a single
|
|
137
|
+
* declaration cannot see its siblings.
|
|
138
|
+
*/
|
|
139
|
+
function assertResolvableInlineReferences(types, referenceTypes) {
|
|
140
|
+
for (const searchType of types) {
|
|
141
|
+
for (const field of searchType.fields) {
|
|
142
|
+
if (!isInlineReference(field)) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!referenceTypes.has(field.ref.typeName)) {
|
|
146
|
+
throw new Error(`Inline reference “${searchType.name}.${field.name}” names “${field.ref.typeName}”, which is not a declared reference type; declare a reference type (a SearchType with no class) with that name.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const referenceType of referenceTypes.values()) {
|
|
151
|
+
assertNoInlineCycle(referenceType, referenceTypes, new Set());
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function assertNoInlineCycle(referenceType, referenceTypes, onPath) {
|
|
155
|
+
if (onPath.has(referenceType.name)) {
|
|
156
|
+
throw new Error(`Inline reference cycle through reference type “${referenceType.name}”; an inline reference graph must be acyclic, so its framing depth stays bounded.`);
|
|
157
|
+
}
|
|
158
|
+
const extended = new Set([...onPath, referenceType.name]);
|
|
159
|
+
for (const field of referenceType.fields) {
|
|
160
|
+
if (!isInlineReference(field)) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
// Resolvability is validated before any cycle check, so every inline
|
|
164
|
+
// `typeName` here names a declared reference type.
|
|
165
|
+
const referent = referenceTypes.get(field.ref.typeName);
|
|
166
|
+
assertNoInlineCycle(referent, referenceTypes, extended);
|
|
167
|
+
}
|
|
57
168
|
}
|
|
58
169
|
/**
|
|
59
170
|
* The text field a label source serves labels from – the ‘label’ convention
|
|
@@ -317,6 +428,25 @@ export function sortableFields(searchType) {
|
|
|
317
428
|
export function outputFields(searchType) {
|
|
318
429
|
return searchType.fields.filter((field) => field.output === true);
|
|
319
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* Whether a field declares **no** role – none of `output`, `searchable`,
|
|
433
|
+
* `filterable`, `facetable`, `sortable`. Such a field is an **internal field**:
|
|
434
|
+
* the projection populates it (so a later {@link SearchFieldBase.derive} can
|
|
435
|
+
* read it), then prunes it before the document reaches a writer, and the engine
|
|
436
|
+
* collection definition omits it entirely – not stored, not indexed, no RAM.
|
|
437
|
+
* Absence of a role declares that intent; there is no separate marker flag.
|
|
438
|
+
*
|
|
439
|
+
* The single predicate the projection and the collection definition share, so
|
|
440
|
+
* they cannot disagree on what is internal. See the Search context’s load-bearing
|
|
441
|
+
* line: *a field without a Role is an Internal Field.*
|
|
442
|
+
*/
|
|
443
|
+
export function isInternalField(field) {
|
|
444
|
+
return (field.output !== true &&
|
|
445
|
+
field.searchable === undefined &&
|
|
446
|
+
field.filterable !== true &&
|
|
447
|
+
field.facetable !== true &&
|
|
448
|
+
field.sortable !== true);
|
|
449
|
+
}
|
|
320
450
|
/** Fields of kind `reference` (IRI-valued, label-resolved), in declaration order. */
|
|
321
451
|
export function referenceFields(searchType) {
|
|
322
452
|
return searchType.fields.filter((field) => field.kind === 'reference');
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAShD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,YAAY,GACzB,IAAI,CAsJN"}
|
package/dist/testing.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { filterOperatorFor, } from './query.js';
|
|
3
|
-
import { facetableFields, filterableFields
|
|
3
|
+
import { facetableFields, filterableFields } from './schema.js';
|
|
4
4
|
/**
|
|
5
5
|
* The executable {@link SearchEngine} port contract (import from
|
|
6
6
|
* `@lde/search/testing`): a vitest suite every engine adapter runs against a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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",
|