@lde/search 0.37.0 → 0.39.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/period.d.ts CHANGED
@@ -53,11 +53,11 @@ export declare const OPEN_END: number;
53
53
  */
54
54
  export declare function periodOf(value: string): StoredPeriod | undefined;
55
55
  /**
56
- * The period several values cover between them: the earliest start and the
57
- * latest end, or `undefined` when none states a period. One stored range per
58
- * document is what an engine can sort on and compare in a single clause, so a
59
- * work dated `1668`, `1676` and `1686` is stored as 1668 through 1686 – and a
60
- * range falling between its dates still matches it.
56
+ * The period several periods cover between them: the earliest start and the
57
+ * latest end. One stored range per document is what an engine can sort on and
58
+ * compare in a single clause, so a work dated `1668`, `1676` and `1686` is
59
+ * stored as 1668 through 1686 – and a range falling between its dates still
60
+ * matches it.
61
61
  */
62
- export declare function periodOfAll(values: readonly string[]): StoredPeriod | undefined;
62
+ export declare function periodCovering(first: StoredPeriod, ...rest: readonly StoredPeriod[]): StoredPeriod;
63
63
  //# sourceMappingURL=period.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"period.d.ts","sourceRoot":"","sources":["../src/period.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,QAAa,CAAC;AAErC;oCACoC;AACpC,eAAO,MAAM,QAAQ,QAAU,CAAC;AAEhC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAehE;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,SAAS,MAAM,EAAE,GACxB,YAAY,GAAG,SAAS,CAgB1B"}
1
+ {"version":3,"file":"period.d.ts","sourceRoot":"","sources":["../src/period.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,QAAa,CAAC;AAErC;oCACoC;AACpC,eAAO,MAAM,QAAQ,QAAU,CAAC;AAEhC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAehE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,YAAY,EACnB,GAAG,IAAI,EAAE,SAAS,YAAY,EAAE,GAC/B,YAAY,CAQd"}
package/dist/period.js CHANGED
@@ -59,28 +59,17 @@ export function periodOf(value) {
59
59
  return start <= end && !openAtBothEnds ? { start, end } : undefined;
60
60
  }
61
61
  /**
62
- * The period several values cover between them: the earliest start and the
63
- * latest end, or `undefined` when none states a period. One stored range per
64
- * document is what an engine can sort on and compare in a single clause, so a
65
- * work dated `1668`, `1676` and `1686` is stored as 1668 through 1686 – and a
66
- * range falling between its dates still matches it.
62
+ * The period several periods cover between them: the earliest start and the
63
+ * latest end. One stored range per document is what an engine can sort on and
64
+ * compare in a single clause, so a work dated `1668`, `1676` and `1686` is
65
+ * stored as 1668 through 1686 – and a range falling between its dates still
66
+ * matches it.
67
67
  */
68
- export function periodOfAll(values) {
69
- let covered;
70
- for (const value of values) {
71
- const period = periodOf(value);
72
- if (period === undefined) {
73
- continue;
74
- }
75
- covered =
76
- covered === undefined
77
- ? period
78
- : {
79
- start: Math.min(covered.start, period.start),
80
- end: Math.max(covered.end, period.end),
81
- };
82
- }
83
- return covered;
68
+ export function periodCovering(first, ...rest) {
69
+ return rest.reduce((covered, period) => ({
70
+ start: Math.min(covered.start, period.start),
71
+ end: Math.max(covered.end, period.end),
72
+ }), first);
84
73
  }
85
74
  /** One side of an interval, or a whole plain value. */
86
75
  function sideOf(text) {
package/dist/project.d.ts CHANGED
@@ -27,6 +27,24 @@ export type SearchDocument = {
27
27
  export interface ProjectionContext extends Partial<Readonly<Record<ProjectionValue, string>>> {
28
28
  /** The IRI of the dataset being indexed. */
29
29
  readonly dataset?: string;
30
+ /**
31
+ * Called once for each value a typed field could not read and so left out:
32
+ * a `date` over `Z.j`, an `integer` over `onbekend`, a `period` over
33
+ * `1987/1897`, a `reference` over a literal, a `boolean` outside
34
+ * `true`/`false`/`1`/`0`. Values from a `derive` count too; one that is not
35
+ * a string is reported as `String(value)`.
36
+ */
37
+ readonly onUnreadable?: (unreadable: UnreadableValue) => void;
38
+ }
39
+ /** A value a typed field could not read ({@link ProjectionContext.onUnreadable}). */
40
+ export interface UnreadableValue {
41
+ /** The field that could not read it; dotted for a field of a nested
42
+ * referent (`creator.birthDate`). */
43
+ readonly field: string;
44
+ /** The value as the source – or the derive – stated it. */
45
+ readonly value: string;
46
+ /** The IRI of the node the document was projected from. */
47
+ readonly id: string;
30
48
  }
31
49
  /**
32
50
  * Project one framed JSON-LD node into a flat search document: apply each field
@@ -1 +1 @@
1
- {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAGzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAwBL,KAAK,eAAe,EAIpB,KAAK,QAAQ,EAEb,KAAK,YAAY,EACjB,KAAK,UAAU,EAGhB,MAAM,aAAa,CAAC;AAGrB;;;;;;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;AAsLD;;;;;;;;;;;;GAYG;AACH,wBAAuB,YAAY,CACjC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,QAAQ,EACpB,OAAO,GAAE,iBAAyC,GACjD,aAAa,CAAC,cAAc,CAAC,CAc/B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,aAAa,EACvB,UAAU,EAAE,UAAU,EACtB,MAAM,CAAC,EAAE,YAAY,GACpB,SAAS,MAAM,EAAE,CAmCnB"}
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAGzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAsBL,KAAK,eAAe,EAIpB,KAAK,QAAQ,EAEb,KAAK,YAAY,EACjB,KAAK,UAAU,EAGhB,MAAM,aAAa,CAAC;AAUrB;;;;;;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;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,eAAe,KAAK,IAAI,CAAC;CAC/D;AAED,qFAAqF;AACrF,MAAM,WAAW,eAAe;IAC9B;0CACsC;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,UAAU,EACtB,MAAM,CAAC,EAAE,YAAY,EACrB,OAAO,GAAE,iBAAyC,GACjD,cAAc,CAkBhB;AAgMD;;;;;;;;;;;;GAYG;AACH,wBAAuB,YAAY,CACjC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,QAAQ,EACpB,OAAO,GAAE,iBAAyC,GACjD,aAAa,CAAC,cAAc,CAAC,CAc/B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,aAAa,EACvB,UAAU,EAAE,UAAU,EACtB,MAAM,CAAC,EAAE,YAAY,GACpB,SAAS,MAAM,EAAE,CAmCnB"}
package/dist/project.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { fold } from '@lde/text-normalization';
2
2
  import { matchLanguage } from './language.js';
3
3
  import { buildSubjectIndex, frameSubjects, } from './frame-by-type.js';
4
- import { companionNames, assertTypeInSchema, displayFieldName, displayLangOf, documentKeyOf, endpointFieldOf, fieldNamed, identityFieldOf, inheritedFacetPolicies, inlineFramingDepth, irAlias, isAbsoluteIri, isInternalField, isInlineReference, isoToUnixSeconds, localLookupTargetsOf, physicalFields, referencedTargetsOf, referenceTypeNamed, storableUnixSeconds, storedTargetOf, TARGET_FIELD, } from './schema.js';
5
- import { periodOfAll } from './period.js';
4
+ import { companionNames, assertTypeInSchema, displayFieldName, displayLangOf, documentKeyOf, endpointFieldOf, fieldNamed, identityFieldOf, inheritedFacetPolicies, inlineFramingDepth, irAlias, isAbsoluteIri, isInternalField, isInlineReference, localLookupTargetsOf, physicalFields, referencedTargetsOf, referenceTypeNamed, storedTargetOf, TARGET_FIELD, } from './schema.js';
5
+ import { periodCovering, periodOf } from './period.js';
6
+ import { describeValue, isReadKind, readValue, UNREADABLE, } from './read-value.js';
6
7
  /** The context a projection runs with when its caller supplied none. */
7
8
  const NO_PROJECTION_CONTEXT = {};
8
9
  /**
@@ -20,12 +21,13 @@ const NO_PROJECTION_CONTEXT = {};
20
21
  * the engine collection definition and the query compiler.
21
22
  */
22
23
  export function projectDocument(node, searchType, schema, context = NO_PROJECTION_CONTEXT) {
23
- if (documentKey(node) === undefined) {
24
+ const key = documentKey(node);
25
+ if (key === undefined) {
24
26
  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.`);
25
27
  }
26
28
  // The guard above is what makes this a SearchDocument: projectFields sets `id`
27
29
  // for exactly the nodes documentKey resolves.
28
- const document = projectFields(node, searchType, schema, context);
30
+ const document = projectFields(node, searchType, schema, context, unreadableSink(context, key));
29
31
  pruneInternalFields(document, searchType, schema);
30
32
  return document;
31
33
  }
@@ -93,10 +95,10 @@ function pruneInternalFields(document, searchType, schema) {
93
95
  * Pruning is deferred to a single recursive pass ({@link pruneInternalFields})
94
96
  * once the whole nested structure is projected, so a derive at any depth can
95
97
  * still read an internal field before it is removed. */
96
- function projectFields(node, searchType, schema, context, nested = false) {
98
+ function projectFields(node, searchType, schema, context, sink, nested = false) {
97
99
  const document = emptyDocument(documentIdOf(node, searchType), searchType);
98
100
  for (const field of searchType.fields) {
99
- applyField(document, node, field, searchType, schema, context, nested);
101
+ applyField(document, node, field, searchType, schema, context, sink, nested);
100
102
  }
101
103
  return document;
102
104
  }
@@ -255,7 +257,8 @@ function hasValue(document, field, searchType) {
255
257
  (displayLangOf(field, key) !== undefined &&
256
258
  fieldNamed(searchType, key) === undefined)));
257
259
  }
258
- function applyField(document, node, field, searchType, schema, context, nested) {
260
+ function applyField(document, node, field, searchType, schema, context, sink, nested) {
261
+ const unread = (value) => sink(field.name, value);
259
262
  // The three value sources, mutually exclusive by declaration
260
263
  // (`validateSearchType`): a projection value, a computed value, a graph path.
261
264
  if ((field.kind === 'keyword' || field.kind === 'reference') &&
@@ -264,27 +267,24 @@ function applyField(document, node, field, searchType, schema, context, nested)
264
267
  }
265
268
  if (field.derive !== undefined) {
266
269
  const derived = field.derive(document, context);
267
- // A derived `reference` is held to the same rule as a read one: what it
268
- // stores is a selection key, and the surface types it `IRI`. ADR 21 sends
269
- // an IRI-valued `keyword` derive here by converting it to `reference`, so
270
- // this is the busiest way a non-IRI could otherwise reach the index. A
271
- // value that survives nothing leaves the field absent, exactly as a derive
272
- // returning `undefined` does.
273
- if (field.kind === 'date') {
274
- return applyDerivedDate(document, field, derived);
275
- }
276
270
  if (field.kind === 'period') {
277
- return setPeriod(document, field, derivedStrings(derived));
271
+ return setPeriod(document, field, derivedStrings(derived, unread), unread);
272
+ }
273
+ // A derived value is held to the same rule as a read one, through the same
274
+ // reader: a derived `reference` stores a selection key the surface types
275
+ // `IRI` – ADR 21 sends an IRI-valued `keyword` derive here by converting it
276
+ // to `reference`, so this is the busiest way a non-IRI could otherwise
277
+ // reach the index – and a derived `integer` an int64.
278
+ if (isReadKind(field.kind)) {
279
+ return applyRead(document, field, field.kind, Array.isArray(derived) ? derived : [derived], unread);
280
+ }
281
+ // A `keyword` or `text` field reads any value but NaN, which serializes as
282
+ // `null` – stored by an engine as no value, or refused with the document.
283
+ if (Number.isNaN(derived)) {
284
+ unread('NaN');
278
285
  }
279
- const value = field.kind === 'reference' && derived !== undefined
280
- ? derivedIris(derived)
281
- : derived;
282
- // NaN is not a value any kind stores, and a derive is the only route that
283
- // can produce one (`setNumber` guards the read route). It serializes as
284
- // `null`, which an engine rejects for a numeric field – so drop it, leaving
285
- // the field absent as an unparseable string already does.
286
- if (value !== undefined && !Number.isNaN(value)) {
287
- document[field.name] = value;
286
+ else if (derived !== undefined) {
287
+ document[field.name] = derived;
288
288
  }
289
289
  return;
290
290
  }
@@ -307,7 +307,7 @@ function applyField(document, node, field, searchType, schema, context, nested)
307
307
  // project nothing rather than fall through and emit the referent IRIs under
308
308
  // the field name (the wrong shape).
309
309
  if (schema !== undefined) {
310
- applyInlineReference(document, node, alias, field, schema, context, nested);
310
+ applyInlineReference(document, node, alias, field, schema, context, sink, nested);
311
311
  }
312
312
  return;
313
313
  }
@@ -318,7 +318,7 @@ function applyField(document, node, field, searchType, schema, context, nested)
318
318
  if (field.kind === 'reference' && schema !== undefined) {
319
319
  const localTargets = localLookupTargetsOf(field, schema);
320
320
  if (localTargets.length > 0) {
321
- const endpoints = applyNestedReferents(document, valuesOf(node, alias), field, localTargets, schema, context);
321
+ const endpoints = applyNestedReferents(document, valuesOf(node, alias), field, localTargets, schema, context, sink);
322
322
  // A `local` lookup stores an object, so its own id is a level deeper than
323
323
  // a filter can reach – an engine welds conditions on an entry's LEAF
324
324
  // fields only, and facets flat values. `filterable` and `facetable`
@@ -335,28 +335,42 @@ function applyField(document, node, field, searchType, schema, context, nested)
335
335
  return applyFacet(document, literalsOf(node, alias), field, schema);
336
336
  case 'reference': {
337
337
  const targetsById = new Map();
338
- const values = referenceValues(node, alias, field, schema, targetsById);
338
+ const values = referenceValues(node, alias, field, schema, targetsById, unread);
339
339
  rememberTargets(document, targetsById);
340
340
  return applyFacet(document, values, field, schema, targetsById);
341
341
  }
342
342
  case 'integer':
343
- return setNumber(document, field.name, toInteger(firstLiteralOf(node, alias)));
344
343
  case 'number':
345
- return setNumber(document, field.name, toNumber(firstLiteralOf(node, alias)));
346
- case 'date': {
347
- const literal = firstLiteralOf(node, alias);
348
- return setDate(document, field, literal === undefined ? undefined : isoToUnixSeconds(literal));
349
- }
344
+ case 'date':
345
+ case 'boolean':
346
+ return applyRead(document, field, field.kind, literalsOf(node, alias), unread);
350
347
  case 'period':
351
- return setPeriod(document, field, literalsOf(node, alias));
352
- case 'boolean': {
353
- // The xsd:boolean lexical space: true/false/1/0.
354
- const literal = firstLiteralOf(node, alias);
355
- if (literal !== undefined) {
356
- document[field.name] = literal === 'true' || literal === '1';
357
- }
358
- return;
348
+ return setPeriod(document, field, literalsOf(node, alias), unread);
349
+ }
350
+ }
351
+ /**
352
+ * Store what {@link readValue} reads of `values` as `kind`, reporting every
353
+ * value it cannot. An `array` reference stores the list; every other kind is a
354
+ * scalar in the engine, so it stores the first value read – and an unreadable
355
+ * or blank value cannot cost the field a readable one behind it.
356
+ */
357
+ function applyRead(document, field, kind, values, unread) {
358
+ const read = values.flatMap((value) => {
359
+ const reading = readValue(kind, value);
360
+ if (reading === UNREADABLE) {
361
+ unread(describeValue(value));
362
+ return [];
359
363
  }
364
+ return reading === undefined ? [] : [reading];
365
+ });
366
+ if (kind === 'date') {
367
+ return setDate(document, field, read[0]);
368
+ }
369
+ if (kind === 'reference' && field.array === true) {
370
+ document[field.name] = read;
371
+ }
372
+ else if (read.length > 0) {
373
+ document[field.name] = read[0];
360
374
  }
361
375
  }
362
376
  /**
@@ -385,7 +399,16 @@ function applyField(document, node, field, searchType, schema, context, nested)
385
399
  * referent’s node IRIs therefore sees a key instead, which is why the two are
386
400
  * worth declaring together only deliberately.
387
401
  */
388
- function referenceValues(node, alias, field, schema, targetsById) {
402
+ function referenceValues(node, alias, field, schema, targetsById, unread) {
403
+ // A literal where a referent belongs states no IRI to store. A node without
404
+ // one – a blank node – carries no value a publisher could look up, so only
405
+ // literals are reported, and a blank one states nothing to correct.
406
+ for (const value of valuesOf(node, alias)) {
407
+ const literal = iriString(value) === undefined ? literalString(value) : undefined;
408
+ if (literal !== undefined && literal.trim() !== '') {
409
+ unread(literal);
410
+ }
411
+ }
389
412
  // Guaranteed declared: `searchSchema` validates that every named target
390
413
  // resolves, and the projection only ever runs against a type of its schema.
391
414
  const targets = referencedTargetsOf(field, schema);
@@ -685,7 +708,7 @@ function applyProjectionValue(document, field, from, context, schema) {
685
708
  * the schema’s declared depth. The referent type is guaranteed declared by
686
709
  * {@link searchSchema}.
687
710
  */
688
- function applyInlineReference(document, node, alias, field, schema, context, nested) {
711
+ function applyInlineReference(document, node, alias, field, schema, context, sink, nested) {
689
712
  // Resolves for a schema that declares the referent (always so for the schema a
690
713
  // type is projected through); a type framed against a foreign schema that
691
714
  // omits it simply contributes no nesting.
@@ -695,7 +718,7 @@ function applyInlineReference(document, node, alias, field, schema, context, nes
695
718
  }
696
719
  const asEdge = edgeReader(referenceType, field.ref.identity);
697
720
  const asLiteralEntry = literalReader(referenceType);
698
- const referents = applyNestedReferents(document, valuesOf(node, alias).map((value) => asLiteralEntry(value) ?? asEdge(value)), field, [referenceType], schema, context);
721
+ const referents = applyNestedReferents(document, valuesOf(node, alias).map((value) => asLiteralEntry(value) ?? asEdge(value)), field, [referenceType], schema, context, sink);
699
722
  applyIdentityCompanion(document, referents, field, schema, nested);
700
723
  }
701
724
  /**
@@ -897,7 +920,7 @@ function identityValue(value) {
897
920
  * top-level document of that type would be – so the stored id is the one the
898
921
  * target’s collection files it under, and the lookup can find it.
899
922
  */
900
- function applyNestedReferents(document, values, field, nestedTypes, schema, context) {
923
+ function applyNestedReferents(document, values, field, nestedTypes, schema, context, sink) {
901
924
  // One node may stand for several entries: a weldable leaf is single-valued,
902
925
  // so an edge the graph gave several roles or several endpoints fans out into
903
926
  // one entry per combination BEFORE it is projected (ADR 26).
@@ -910,8 +933,18 @@ function applyNestedReferents(document, values, field, nestedTypes, schema, cont
910
933
  const nestedType = nestedTypes.length === 1
911
934
  ? nestedTypes[0]
912
935
  : targetOfReferent(value, nestedTypes);
936
+ // Every tuple of a node carries the node’s other values, so each value
937
+ // is reported once per node, not once per tuple it was copied into.
938
+ let reported;
939
+ const nestedSink = (name, value) => {
940
+ const key = JSON.stringify([name, value]);
941
+ if (!(reported ??= new Set()).has(key)) {
942
+ reported.add(key);
943
+ sink(`${field.name}.${name}`, value);
944
+ }
945
+ };
913
946
  return tuplesOf(value, nestedType, field).map((tuple) => {
914
- const projected = projectFields(tuple, nestedType, schema, context, true);
947
+ const projected = projectFields(tuple, nestedType, schema, context, nestedSink, true);
915
948
  if (nestedTypes.length > 1 && Object.keys(projected).length > 0) {
916
949
  projected[TARGET_FIELD] = nestedType.name;
917
950
  }
@@ -1010,9 +1043,6 @@ function literalsOf(node, key) {
1010
1043
  .map(literalString)
1011
1044
  .filter((value) => value !== undefined);
1012
1045
  }
1013
- function firstLiteralOf(node, key) {
1014
- return literalsOf(node, key)[0];
1015
- }
1016
1046
  function irisOf(node, key) {
1017
1047
  return valuesOf(node, key)
1018
1048
  .map(iriString)
@@ -1075,44 +1105,6 @@ function iriString(value) {
1075
1105
  const iri = isObject(value) ? value['@id'] : value;
1076
1106
  return typeof iri === 'string' && isAbsoluteIri(iri) ? iri : undefined;
1077
1107
  }
1078
- /** What a `derive` on a `reference` field is allowed to have produced: the
1079
- * absolute IRIs among its return value, keeping the shape the declaration
1080
- * promises (a list stays a list, a single value stays single) so `array` still
1081
- * decides the stored shape. A lone non-IRI becomes `undefined`, i.e. absent –
1082
- * the same outcome the graph path gives it. */
1083
- function derivedIris(value) {
1084
- if (Array.isArray(value)) {
1085
- return value.filter((entry) => typeof entry === 'string' && isAbsoluteIri(entry));
1086
- }
1087
- return typeof value === 'string' && isAbsoluteIri(value) ? value : undefined;
1088
- }
1089
- function toInteger(literal) {
1090
- return literal === undefined ? undefined : Math.trunc(Number(literal));
1091
- }
1092
- function toNumber(literal) {
1093
- return literal === undefined ? undefined : Number(literal);
1094
- }
1095
- /**
1096
- * A derived `date` goes through the same storage codec as a read one: the field
1097
- * is stored as Unix seconds, so an ISO string returned here would otherwise
1098
- * land as a string in an int64 field – and it is parsed like any literal, its
1099
- * precision included. A derive that computed seconds itself returns a number,
1100
- * which states no precision and so is stored as a date-time; the codec never
1101
- * saw it and so never range-checked it, and seconds outside the int64 a `date`
1102
- * field is stored in would be refused by the engine where an unparseable
1103
- * string merely leaves the field absent – so drop those too.
1104
- */
1105
- function applyDerivedDate(document, field, derived) {
1106
- if (typeof derived === 'string') {
1107
- return setDate(document, field, isoToUnixSeconds(derived));
1108
- }
1109
- if (typeof derived === 'number') {
1110
- // Truncated, as the codec truncates a string’s fractional seconds: the
1111
- // field is an int64. `storableUnixSeconds` drops NaN.
1112
- const seconds = storableUnixSeconds(Math.trunc(derived));
1113
- return setDate(document, field, seconds === undefined ? undefined : { seconds, precision: 'dateTime' });
1114
- }
1115
- }
1116
1108
  /** Store a `date` field’s seconds, and its precision in the companion beside
1117
1109
  * them ({@link physicalFields}), so the surface can serve the value in the
1118
1110
  * form its source gave. */
@@ -1124,40 +1116,55 @@ function setDate(document, field, stored) {
1124
1116
  document[physicalFields(field).precision] = stored.precision;
1125
1117
  }
1126
1118
  /**
1127
- * Store a `period` field: its values verbatim under the field’s own name, for
1128
- * output, and the stretch they cover between them in the bound companions
1129
- * ({@link physicalFields}). A value that states no period is still stored – it
1130
- * is what the source said – but bounds nothing.
1119
+ * Store a `period` field: the values that state a period, verbatim under the
1120
+ * field’s own name, for output, and the stretch they cover between them in the
1121
+ * bound companions ({@link physicalFields}). A value that states no period –
1122
+ * `onbekend`, `1987/1897` – is reported and left out, so every value served is
1123
+ * the EDTF its scalar promises; a deployment that wants to show such text
1124
+ * declares a `keyword` field over the same path.
1131
1125
  *
1132
- * Blank values are dropped before a single-valued field keeps its first, so
1133
- * an empty literal cannot cost the field the value behind it; and the bounds
1134
- * are taken from exactly the values stored, so what is served and what is
1135
- * filtered cannot disagree.
1126
+ * Blank and unreadable values are dropped before a single-valued field keeps
1127
+ * its first, so neither can cost the field the period behind it; and the
1128
+ * bounds are taken from exactly the values stored, so what is served and what
1129
+ * is filtered cannot disagree.
1136
1130
  */
1137
- function setPeriod(document, field, values) {
1138
- const nonBlank = dedupe(values.map((value) => value.trim())).filter((value) => value !== '');
1139
- const stated = field.array === true ? nonBlank : nonBlank.slice(0, 1);
1131
+ function setPeriod(document, field, values, unread) {
1132
+ const readable = dedupe(values.map((value) => value.trim())).flatMap((value) => {
1133
+ if (value === '') {
1134
+ return [];
1135
+ }
1136
+ const period = periodOf(value);
1137
+ if (period === undefined) {
1138
+ unread(value);
1139
+ return [];
1140
+ }
1141
+ return [{ value, period }];
1142
+ });
1143
+ const stated = field.array === true ? readable : readable.slice(0, 1);
1140
1144
  if (stated.length === 0) {
1141
1145
  return;
1142
1146
  }
1143
- document[field.name] = field.array === true ? stated : stated[0];
1147
+ document[field.name] =
1148
+ field.array === true ? stated.map(({ value }) => value) : stated[0].value;
1144
1149
  const companions = physicalFields(field).period;
1145
- const covered = periodOfAll(stated);
1146
- if (companions !== undefined && covered !== undefined) {
1150
+ if (companions !== undefined) {
1151
+ const [first, ...rest] = stated.map(({ period }) => period);
1152
+ const covered = periodCovering(first, ...rest);
1147
1153
  document[companions.start] = covered.start;
1148
1154
  document[companions.end] = covered.end;
1149
1155
  }
1150
1156
  }
1151
1157
  /** A derive’s value as the strings a `period` stores: one string, or the
1152
- * strings in a list; anything else states no value. */
1153
- function derivedStrings(derived) {
1158
+ * strings in a list. `null` and `undefined` state no value; anything else
1159
+ * is reported. */
1160
+ function derivedStrings(derived, unread) {
1154
1161
  const values = Array.isArray(derived) ? derived : [derived];
1155
- return values.filter((value) => typeof value === 'string');
1156
- }
1157
- function setNumber(document, field, value) {
1158
- if (value !== undefined && !Number.isNaN(value)) {
1159
- document[field] = value;
1160
- }
1162
+ return values.filter((value) => {
1163
+ if (typeof value !== 'string' && value !== undefined && value !== null) {
1164
+ unread(describeValue(value));
1165
+ }
1166
+ return typeof value === 'string';
1167
+ });
1161
1168
  }
1162
1169
  function dedupe(values) {
1163
1170
  return [...new Set(values)];
@@ -1187,3 +1194,15 @@ function setArray(document, field, values) {
1187
1194
  function isObject(value) {
1188
1195
  return typeof value === 'object' && value !== null && !Array.isArray(value);
1189
1196
  }
1197
+ /** The sink a root document’s fields report to: the context’s
1198
+ * {@link ProjectionContext.onUnreadable}, told which node the value was
1199
+ * dropped from. */
1200
+ function unreadableSink(context, id) {
1201
+ const { onUnreadable } = context;
1202
+ return onUnreadable === undefined
1203
+ ? ignoreUnreadable
1204
+ : (field, value) => onUnreadable({ field, value, id });
1205
+ }
1206
+ function ignoreUnreadable() {
1207
+ // Nobody asked for the report.
1208
+ }
@@ -0,0 +1,44 @@
1
+ import { type StoredDate } from './schema.js';
2
+ /** What reading a value that is there, but not one its kind can store, gives:
3
+ * `onbekend` for an integer, `ja` for a boolean. */
4
+ export declare const UNREADABLE: unique symbol;
5
+ /**
6
+ * One value read as a field’s kind: what the field stores, {@link UNREADABLE}
7
+ * for a value it cannot store, or `undefined` for no value at all – a missing
8
+ * or blank one, which states nothing to correct at the source.
9
+ */
10
+ type Reading<T> = T | typeof UNREADABLE | undefined;
11
+ /** The kinds that can refuse a value: the rest store any literal. */
12
+ declare const READ_KINDS: readonly ["integer", "number", "boolean", "date", "reference"];
13
+ /** One of {@link READ_KINDS}. */
14
+ export type ReadKind = (typeof READ_KINDS)[number];
15
+ /** Whether `kind` is one {@link readValue} reads. */
16
+ export declare function isReadKind(kind: string): kind is ReadKind;
17
+ /**
18
+ * Read one value – a literal from the graph, or whatever a `derive` returned –
19
+ * as `kind` stores it. Both value routes read through here, so a kind accepts
20
+ * and refuses the same values whichever route they came by.
21
+ *
22
+ * - `integer`: a finite number, or an `xsd:decimal` or `xsd:double` lexical
23
+ * form, truncated, within ±2⁵³ – beyond that a double no longer holds every
24
+ * integer, so the stored value would not be the stated one. `INF` and `NaN`
25
+ * are refused: an engine cannot store them.
26
+ * - `number`: any finite one, untruncated.
27
+ * - `boolean`: `true` or `false`, or the `xsd:boolean` lexical space
28
+ * (`true`/`false`/`1`/`0`).
29
+ * - `date`: anything the `date` codec reads ({@link isoToUnixSeconds}), or
30
+ * Unix seconds as a number.
31
+ * - `reference`: an absolute IRI.
32
+ *
33
+ * A string is read with surrounding whitespace collapsed, as XSD does.
34
+ */
35
+ export declare function readValue(kind: ReadKind, value: unknown): Reading<number | boolean | string | StoredDate>;
36
+ /**
37
+ * A value as the report shows it: `String(value)`, or its `typeof` when it has
38
+ * no string form, as an object built without a prototype has not. Never
39
+ * throws, so a derive’s odd return is reported rather than crashing the
40
+ * projection.
41
+ */
42
+ export declare function describeValue(value: unknown): string;
43
+ export {};
44
+ //# sourceMappingURL=read-value.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-value.d.ts","sourceRoot":"","sources":["../src/read-value.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AAErB;qDACqD;AACrD,eAAO,MAAM,UAAU,eAAuB,CAAC;AAE/C;;;;GAIG;AACH,KAAK,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,UAAU,GAAG,SAAS,CAAC;AAEpD,qEAAqE;AACrE,QAAA,MAAM,UAAU,gEAMN,CAAC;AAEX,iCAAiC;AACjC,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,qDAAqD;AACrD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,QAAQ,CAEzD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,OAAO,GACb,OAAO,CAAC,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,CAAC,CA8CjD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAMpD"}
@@ -0,0 +1,96 @@
1
+ import { isAbsoluteIri, isoToUnixSeconds, storableUnixSeconds, } from './schema.js';
2
+ /** What reading a value that is there, but not one its kind can store, gives:
3
+ * `onbekend` for an integer, `ja` for a boolean. */
4
+ export const UNREADABLE = Symbol('unreadable');
5
+ /** The kinds that can refuse a value: the rest store any literal. */
6
+ const READ_KINDS = [
7
+ 'integer',
8
+ 'number',
9
+ 'boolean',
10
+ 'date',
11
+ 'reference',
12
+ ];
13
+ /** Whether `kind` is one {@link readValue} reads. */
14
+ export function isReadKind(kind) {
15
+ return READ_KINDS.includes(kind);
16
+ }
17
+ /**
18
+ * Read one value – a literal from the graph, or whatever a `derive` returned –
19
+ * as `kind` stores it. Both value routes read through here, so a kind accepts
20
+ * and refuses the same values whichever route they came by.
21
+ *
22
+ * - `integer`: a finite number, or an `xsd:decimal` or `xsd:double` lexical
23
+ * form, truncated, within ±2⁵³ – beyond that a double no longer holds every
24
+ * integer, so the stored value would not be the stated one. `INF` and `NaN`
25
+ * are refused: an engine cannot store them.
26
+ * - `number`: any finite one, untruncated.
27
+ * - `boolean`: `true` or `false`, or the `xsd:boolean` lexical space
28
+ * (`true`/`false`/`1`/`0`).
29
+ * - `date`: anything the `date` codec reads ({@link isoToUnixSeconds}), or
30
+ * Unix seconds as a number.
31
+ * - `reference`: an absolute IRI.
32
+ *
33
+ * A string is read with surrounding whitespace collapsed, as XSD does.
34
+ */
35
+ export function readValue(kind, value) {
36
+ if (value === undefined || value === null) {
37
+ return undefined;
38
+ }
39
+ const text = typeof value === 'string' ? value.trim() : undefined;
40
+ if (text === '') {
41
+ return undefined;
42
+ }
43
+ switch (kind) {
44
+ case 'integer':
45
+ case 'number': {
46
+ const number = typeof value === 'number'
47
+ ? value
48
+ : text !== undefined && NUMERIC.test(text)
49
+ ? Number(text)
50
+ : Number.NaN;
51
+ if (kind === 'number') {
52
+ return Number.isFinite(number) ? number : UNREADABLE;
53
+ }
54
+ const integer = Math.trunc(number);
55
+ return Number.isSafeInteger(integer) ? integer : UNREADABLE;
56
+ }
57
+ case 'boolean':
58
+ if (typeof value === 'boolean') {
59
+ return value;
60
+ }
61
+ return text !== undefined && BOOLEAN.has(text)
62
+ ? text === 'true' || text === '1'
63
+ : UNREADABLE;
64
+ case 'date': {
65
+ if (typeof value === 'number') {
66
+ // Truncated, as the codec truncates a string’s fractional seconds: the
67
+ // field is an int64. `storableUnixSeconds` drops NaN.
68
+ const seconds = storableUnixSeconds(Math.trunc(value));
69
+ return seconds === undefined
70
+ ? UNREADABLE
71
+ : { seconds, precision: 'dateTime' };
72
+ }
73
+ return ((text === undefined ? undefined : isoToUnixSeconds(text)) ?? UNREADABLE);
74
+ }
75
+ case 'reference':
76
+ return text !== undefined && isAbsoluteIri(text) ? text : UNREADABLE;
77
+ }
78
+ }
79
+ /**
80
+ * A value as the report shows it: `String(value)`, or its `typeof` when it has
81
+ * no string form, as an object built without a prototype has not. Never
82
+ * throws, so a derive’s odd return is reported rather than crashing the
83
+ * projection.
84
+ */
85
+ export function describeValue(value) {
86
+ try {
87
+ return String(value);
88
+ }
89
+ catch {
90
+ return typeof value;
91
+ }
92
+ }
93
+ /** The `xsd:decimal` and `xsd:double` lexical forms, `INF` and `NaN` aside. */
94
+ const NUMERIC = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
95
+ /** The `xsd:boolean` lexical space. */
96
+ const BOOLEAN = new Set(['true', 'false', '1', '0']);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/search",
3
- "version": "0.37.0",
3
+ "version": "0.39.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",