@lde/search 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter.d.ts +2 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +2 -1
- package/dist/engine.d.ts +15 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +3 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/join-graph.js +4 -2
- package/dist/language.d.ts +22 -0
- package/dist/language.d.ts.map +1 -0
- package/dist/language.js +54 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +170 -45
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +33 -16
- package/dist/schema.d.ts +92 -36
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +225 -78
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +13 -5
- package/package.json +1 -1
package/dist/schema.js
CHANGED
|
@@ -120,7 +120,7 @@ export function referenceTypeNamed(schema, name) {
|
|
|
120
120
|
}
|
|
121
121
|
/**
|
|
122
122
|
* The {@link RootType} a declaration names – a `lookup`’s `target`, an
|
|
123
|
-
* `idOnly`’s {@link ReferenceField.labelSource} ({@link
|
|
123
|
+
* `idOnly`’s {@link ReferenceField.labelSource} ({@link labelSourceNamesOf}), a
|
|
124
124
|
* join edge – or `undefined` when the schema declares no Root Type by that
|
|
125
125
|
* name. The one reading of *which type does this point at*, so the projection
|
|
126
126
|
* (which re-keys a reference through its target’s {@link KeyField}), the
|
|
@@ -250,27 +250,34 @@ function framingReach(schema, searchType, visiting = new Set()) {
|
|
|
250
250
|
// the cut as “reach 0, and nothing else to count” left the innermost
|
|
251
251
|
// referent’s key one hop outside the frame, so it stored a node IRI that
|
|
252
252
|
// matches nothing in the target’s collection.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
253
|
+
//
|
|
254
|
+
// Several targets each contribute their own reach: a referent is framed
|
|
255
|
+
// once, and the frame has to hold whichever declaration it turns out to
|
|
256
|
+
// match, so the furthest of them decides.
|
|
257
|
+
// The field’s own traversal counts whatever it names.
|
|
258
|
+
furthest = Math.max(furthest, hops);
|
|
259
|
+
const local = localLookupTargetsOf(field, schema).filter((target) => !onPath.has(target.name));
|
|
260
|
+
for (const target of local) {
|
|
261
|
+
furthest = Math.max(furthest, hops + framingReach(schema, target, onPath));
|
|
257
262
|
}
|
|
258
263
|
// A keyed target’s key field is itself path-bearing, so its own traversal
|
|
259
264
|
// counts too – the extraction reads it off the referent with that path.
|
|
260
|
-
|
|
261
|
-
|
|
265
|
+
// Counted for every target that is not locally expanded above, cut ones
|
|
266
|
+
// included: there the extraction falls back to exactly this hop.
|
|
267
|
+
for (const target of referencedTargetsOf(field, schema)) {
|
|
268
|
+
if (local.includes(target)) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const keyPath = keyPathOf(target);
|
|
272
|
+
furthest = Math.max(furthest, hops + (keyPath === undefined ? 0 : pathHopCount(keyPath)));
|
|
273
|
+
}
|
|
262
274
|
}
|
|
263
275
|
return furthest;
|
|
264
276
|
}
|
|
265
|
-
/** The `path` of
|
|
266
|
-
*
|
|
267
|
-
function
|
|
268
|
-
if (
|
|
269
|
-
return undefined;
|
|
270
|
-
}
|
|
271
|
-
const targetName = labelSourceNameOf(field);
|
|
272
|
-
const target = targetName === undefined ? undefined : rootTypeNamed(schema, targetName);
|
|
273
|
-
if (target?.key === undefined) {
|
|
277
|
+
/** The `path` of a Root Type’s key field, when it declares a
|
|
278
|
+
* {@link RootType.key}. */
|
|
279
|
+
function keyPathOf(target) {
|
|
280
|
+
if (target.key === undefined) {
|
|
274
281
|
return undefined;
|
|
275
282
|
}
|
|
276
283
|
// `searchSchema` guarantees a key field that is declared and path-bearing –
|
|
@@ -395,7 +402,7 @@ function assertIdentityCompanion(searchType, field, referenceTypes) {
|
|
|
395
402
|
throw new Error(`${where} names identity “${identity}”, which “${field.ref.typeName}” does not declare.`);
|
|
396
403
|
}
|
|
397
404
|
if (identityField.kind !== 'reference' ||
|
|
398
|
-
|
|
405
|
+
labelSourceNamesOf(identityField).length === 0) {
|
|
399
406
|
throw new Error(`${where} names identity “${identity}”, which names no target: an identity companion holds ids of documents in a collection, so the field it harvests must be a reference declaring a “lookup” target or a label source.`);
|
|
400
407
|
}
|
|
401
408
|
}
|
|
@@ -612,15 +619,36 @@ export function labelFieldOf(searchType) {
|
|
|
612
619
|
: undefined;
|
|
613
620
|
}
|
|
614
621
|
/**
|
|
615
|
-
* The Root
|
|
616
|
-
* `target`, an `idOnly`’s
|
|
617
|
-
* when it resolves none. One
|
|
618
|
-
*
|
|
622
|
+
* The Root Types a reference resolves labels from, by name and in order of
|
|
623
|
+
* precedence: a `lookup`’s `target`, an `idOnly`’s
|
|
624
|
+
* {@link ReferenceField.labelSource}, or none when it resolves none. One
|
|
625
|
+
* reading for the two declarations, so a consumer never branches on the
|
|
626
|
+
* strategy to find the collection – and one **shape** for the one-target and
|
|
627
|
+
* the several-target declaration, so a consumer never branches on that either:
|
|
628
|
+
* a single name is the list of one.
|
|
619
629
|
*/
|
|
620
|
-
export function
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
630
|
+
export function labelSourceNamesOf(field) {
|
|
631
|
+
const declared = field.ref?.strategy === 'lookup' ? field.ref.target : field.labelSource;
|
|
632
|
+
return declared === undefined
|
|
633
|
+
? []
|
|
634
|
+
: typeof declared === 'string'
|
|
635
|
+
? [declared]
|
|
636
|
+
: declared;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* The Root Types a reference resolves against ({@link labelSourceNamesOf}),
|
|
640
|
+
* resolved in the given schema and in order of precedence. A name the schema
|
|
641
|
+
* does not declare as a Root Type is skipped – `searchSchema` rejects such a
|
|
642
|
+
* declaration, so this only happens for a field read against a foreign schema,
|
|
643
|
+
* where it resolves what it can.
|
|
644
|
+
*/
|
|
645
|
+
export function referencedTargetsOf(field, schema) {
|
|
646
|
+
if (schema === undefined || field.kind !== 'reference') {
|
|
647
|
+
return [];
|
|
648
|
+
}
|
|
649
|
+
return labelSourceNamesOf(field)
|
|
650
|
+
.map((name) => rootTypeNamed(schema, name))
|
|
651
|
+
.filter((target) => target !== undefined);
|
|
624
652
|
}
|
|
625
653
|
/**
|
|
626
654
|
* Every name a reference resolves labels from – a `lookup`’s `target` or an
|
|
@@ -655,28 +683,97 @@ function assertResolvableLabelSources(types, schema) {
|
|
|
655
683
|
throw new Error(`Reference “${searchType.name}.${field.name}” declares a label source on a ${field.ref.strategy} reference; only an idOnly reference does, for its facet buckets.`);
|
|
656
684
|
}
|
|
657
685
|
}
|
|
658
|
-
|
|
659
|
-
? field.ref.target
|
|
660
|
-
: labelSource;
|
|
661
|
-
if (sourceName === undefined) {
|
|
686
|
+
if (field.kind !== 'reference') {
|
|
662
687
|
continue;
|
|
663
688
|
}
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
689
|
+
const sources = [];
|
|
690
|
+
for (const sourceName of labelSourceNamesOf(field)) {
|
|
691
|
+
const source = rootTypeNamed(schema, sourceName);
|
|
692
|
+
if (source === undefined) {
|
|
693
|
+
// A name that IS declared, just not as a Root Type, is the confusing
|
|
694
|
+
// case: telling the author to declare a type they already declared
|
|
695
|
+
// would send them looking in the wrong place. Only a Root Type has a
|
|
696
|
+
// collection to resolve against, so name that instead.
|
|
697
|
+
throw new Error(referenceTypeNamed(schema, sourceName) === undefined
|
|
698
|
+
? `Reference “${searchType.name}.${field.name}” names unknown label source “${sourceName}”; declare a SearchType with that name.`
|
|
699
|
+
: `Reference “${searchType.name}.${field.name}” names label source “${sourceName}”, which is a Reference Type; a label source must be a Root Type, since a resolved label is read from that type’s own collection.`);
|
|
700
|
+
}
|
|
701
|
+
if (labelFieldOf(source) === undefined) {
|
|
702
|
+
throw new Error(`Reference “${searchType.name}.${field.name}” uses label source “${sourceName}”, which must declare an output, searchable text field “${labelFieldNameOf(source)}”.`);
|
|
703
|
+
}
|
|
704
|
+
sources.push(source);
|
|
705
|
+
}
|
|
706
|
+
assertCompatibleTargets(searchType, field, sources);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* The targets of a reference naming **several** may not disagree about a
|
|
712
|
+
* field they both declare: a stored referent of either kind lands in one
|
|
713
|
+
* nested object, which an engine declares once, so `birthDate` cannot be a
|
|
714
|
+
* `date` on one target and a `keyword` on the other, nor a list on one and a
|
|
715
|
+
* single value on the other – nor an id on one and a nested document on the
|
|
716
|
+
* other, nor indexed on one and stored only on the other. Everything that
|
|
717
|
+
* decides a nested field’s physical shape has to agree: kind, arity, the
|
|
718
|
+
* Roles it opts into, and for a reference the strategy and what it points at.
|
|
719
|
+
* A field only one target declares is fine – the other simply never fills it
|
|
720
|
+
* – and so is a `text` field whose locales differ, since display stores every
|
|
721
|
+
* present language regardless.
|
|
722
|
+
*/
|
|
723
|
+
function assertCompatibleTargets(searchType, field, targets) {
|
|
724
|
+
const seen = new Map();
|
|
725
|
+
for (const target of targets) {
|
|
726
|
+
for (const declared of target.fields) {
|
|
727
|
+
const earlier = seen.get(declared.name);
|
|
728
|
+
if (earlier === undefined) {
|
|
729
|
+
seen.set(declared.name, { target, field: declared });
|
|
730
|
+
continue;
|
|
673
731
|
}
|
|
674
|
-
if (
|
|
675
|
-
throw new Error(`Reference “${searchType.name}.${field.name}”
|
|
732
|
+
if (describeShape(earlier.field) !== describeShape(declared)) {
|
|
733
|
+
throw new Error(`Reference “${searchType.name}.${field.name}” names targets “${earlier.target.name}” and “${target.name}”, which both declare “${declared.name}” but not alike (${describeShape(earlier.field)} vs ${describeShape(declared)}); a referent of either kind is stored in one shape, so a field the targets share must be declared the same on both.`);
|
|
676
734
|
}
|
|
677
735
|
}
|
|
678
736
|
}
|
|
679
737
|
}
|
|
738
|
+
/**
|
|
739
|
+
* Everything that decides a field’s physical shape, spelled out for a message
|
|
740
|
+
* comparing two declarations: kind and arity, the Roles it carries, and for a
|
|
741
|
+
* reference its strategy, what it points at, and whether it stores a copy.
|
|
742
|
+
*/
|
|
743
|
+
function describeShape(field) {
|
|
744
|
+
const roles = ['filterable', 'facetable', 'sortable'].filter((role) => field[role] === true);
|
|
745
|
+
if (field.searchable !== undefined) {
|
|
746
|
+
roles.push('searchable');
|
|
747
|
+
}
|
|
748
|
+
const ref = field.kind === 'reference' && field.ref !== undefined
|
|
749
|
+
? [
|
|
750
|
+
field.ref.strategy,
|
|
751
|
+
...labelSourceNamesOf(field),
|
|
752
|
+
...(field.ref.strategy === 'inline' ? [field.ref.typeName] : []),
|
|
753
|
+
...(field.ref.strategy === 'lookup' && field.ref.local === true
|
|
754
|
+
? ['local']
|
|
755
|
+
: []),
|
|
756
|
+
]
|
|
757
|
+
: [];
|
|
758
|
+
return [
|
|
759
|
+
field.array === true ? `${field.kind} list` : field.kind,
|
|
760
|
+
...(ref.length > 0 ? [`→ ${ref.join(' ')}`] : []),
|
|
761
|
+
...(roles.length > 0 ? [`(${roles.join(', ')})`] : []),
|
|
762
|
+
].join(' ');
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* The Root Type a stored referent was projected through, among the targets
|
|
766
|
+
* its lookup names: the one its discriminator names ({@link TARGET_FIELD}),
|
|
767
|
+
* or the first declared where it carries none – a single-target lookup stores
|
|
768
|
+
* none, and the first target is also what an unmatched referent was projected
|
|
769
|
+
* through. The one reading of a stored discriminator, so the projection
|
|
770
|
+
* (which prunes through it) and an engine adapter (which reconstructs through
|
|
771
|
+
* it) cannot read it differently.
|
|
772
|
+
*/
|
|
773
|
+
export function storedTargetOf(entry, targets) {
|
|
774
|
+
const name = entry[TARGET_FIELD];
|
|
775
|
+
return targets.find((target) => target.name === name) ?? targets[0];
|
|
776
|
+
}
|
|
680
777
|
/**
|
|
681
778
|
* The reserved logical name of a document’s IRI – the one field every indexed
|
|
682
779
|
* thing carries, and the only one no {@link SearchType} declares. It is the
|
|
@@ -706,9 +803,28 @@ export const ID_FIELD = 'id';
|
|
|
706
803
|
*/
|
|
707
804
|
export const AND_KEY = 'and';
|
|
708
805
|
export const OR_KEY = 'or';
|
|
806
|
+
/**
|
|
807
|
+
* The physical name under which a stored referent of a lookup naming
|
|
808
|
+
* **several** targets records which of them it belongs to: the `name` of the
|
|
809
|
+
* Root Type whose `class` its `rdf:type` matched first, or the first target
|
|
810
|
+
* declared where none did. Written by the projection, read by an engine
|
|
811
|
+
* adapter to reconstruct the referent through the right declaration when no
|
|
812
|
+
* collection answers for it – an unidentified referent, or an identified one
|
|
813
|
+
* whose document is not indexed. A single-target lookup stores none: there is
|
|
814
|
+
* nothing to tell apart.
|
|
815
|
+
*
|
|
816
|
+
* Reserved schema-wide ({@link validateSearchType} rejects a field of this
|
|
817
|
+
* name), so it can never shadow a declared field of a stored entry.
|
|
818
|
+
*/
|
|
819
|
+
export const TARGET_FIELD = '_target';
|
|
709
820
|
/** The logical field names no {@link SearchType} may declare, each because a
|
|
710
821
|
* surface already gives that name a meaning of its own. */
|
|
711
|
-
const RESERVED_FIELD_NAMES = [
|
|
822
|
+
const RESERVED_FIELD_NAMES = [
|
|
823
|
+
ID_FIELD,
|
|
824
|
+
AND_KEY,
|
|
825
|
+
OR_KEY,
|
|
826
|
+
TARGET_FIELD,
|
|
827
|
+
];
|
|
712
828
|
/** Kinds that can feed full-text search (project a folded search field). */
|
|
713
829
|
const SEARCHABLE_KINDS = ['text', 'keyword', 'reference'];
|
|
714
830
|
/** Kinds whose projection applies the {@link KeywordField.transform}. */
|
|
@@ -833,16 +949,38 @@ export function validateSearchType(searchType) {
|
|
|
833
949
|
if (field.output === true &&
|
|
834
950
|
((field.ref?.strategy === 'inline' &&
|
|
835
951
|
field.ref.typeName === undefined) ||
|
|
836
|
-
(field.ref?.strategy === 'lookup' &&
|
|
952
|
+
(field.ref?.strategy === 'lookup' &&
|
|
953
|
+
labelSourceNamesOf(field).length === 0))) {
|
|
837
954
|
issue('missing-ref-type-name');
|
|
838
955
|
}
|
|
839
956
|
// A join addresses the referent's collection – the one a lookup's
|
|
840
957
|
// `target` or an idOnly's `labelSource` names. With neither, the flag
|
|
841
958
|
// states an edge to nowhere.
|
|
842
|
-
|
|
843
|
-
|
|
959
|
+
const targets = labelSourceNamesOf(field);
|
|
960
|
+
if (field.joinable === true && targets.length === 0) {
|
|
844
961
|
issue('joinable-without-label-source');
|
|
845
962
|
}
|
|
963
|
+
// An engine reference names ONE collection, and ids that live in
|
|
964
|
+
// several have no single collection to reference – so a reference whose
|
|
965
|
+
// referent may be any of several types can never be an engine edge. Its
|
|
966
|
+
// labels, facets and id filters all work from this document; a
|
|
967
|
+
// condition on the referent’s own fields does not (#717).
|
|
968
|
+
if (field.joinable === true && targets.length > 1) {
|
|
969
|
+
issue('joinable-with-several-targets');
|
|
970
|
+
}
|
|
971
|
+
// Precedence between targets is declaration order, so a name declared
|
|
972
|
+
// twice would have to mean two different ranks.
|
|
973
|
+
if (new Set(targets).size !== targets.length) {
|
|
974
|
+
issue('duplicate-target');
|
|
975
|
+
}
|
|
976
|
+
// A list of targets names at least one: an empty one would silently
|
|
977
|
+
// read as no target at all – no re-keying, no labels, no facet policy.
|
|
978
|
+
const declaredTargets = field.ref?.strategy === 'lookup'
|
|
979
|
+
? field.ref.target
|
|
980
|
+
: field.labelSource;
|
|
981
|
+
if (Array.isArray(declaredTargets) && declaredTargets.length === 0) {
|
|
982
|
+
issue('empty-target');
|
|
983
|
+
}
|
|
846
984
|
// An inline reference is stored as a NESTED OBJECT, not as an id an
|
|
847
985
|
// engine can point a reference field at, so the two cannot both hold:
|
|
848
986
|
// the collection definition would emit the nesting and silently drop the
|
|
@@ -1040,44 +1178,46 @@ export function identityFieldName(name) {
|
|
|
1040
1178
|
return `${name}_id`;
|
|
1041
1179
|
}
|
|
1042
1180
|
/**
|
|
1043
|
-
* The Root
|
|
1044
|
-
* label
|
|
1045
|
-
* {@link ReferenceStrategy.identity identity companion} points at.
|
|
1181
|
+
* The Root Types whose labels a reference’s **facet buckets** read, by name and
|
|
1182
|
+
* in order of precedence – its own label sources, or, for an inline reference,
|
|
1183
|
+
* those its {@link ReferenceStrategy.identity identity companion} points at.
|
|
1184
|
+
* Empty for a field that labels no bucket.
|
|
1046
1185
|
*
|
|
1047
|
-
* The same one-level-in reading {@link
|
|
1048
|
-
* same reason: the companion holds that field’s ids, so the
|
|
1049
|
-
* those ids
|
|
1050
|
-
* cannot inherit a policy from one type and its labels from another –
|
|
1051
|
-
* happened first, inherit the policy and no labels at all.
|
|
1186
|
+
* The same one-level-in reading {@link inheritedFacetPolicies} makes, and for
|
|
1187
|
+
* the same reason: the companion holds that field’s ids, so the types that
|
|
1188
|
+
* name those ids are the types that can label them. Kept together with it so a
|
|
1189
|
+
* facet cannot inherit a policy from one type and its labels from another –
|
|
1190
|
+
* or, as happened first, inherit the policy and no labels at all.
|
|
1052
1191
|
*/
|
|
1053
|
-
export function
|
|
1192
|
+
export function labelTargetNamesOf(field, schema) {
|
|
1054
1193
|
if (field.kind !== 'reference') {
|
|
1055
|
-
return
|
|
1194
|
+
return [];
|
|
1056
1195
|
}
|
|
1057
|
-
return
|
|
1196
|
+
return labelSourceNamesOf(identityFieldOf(field, schema) ?? field);
|
|
1058
1197
|
}
|
|
1059
1198
|
/**
|
|
1060
|
-
* The Root
|
|
1061
|
-
* referents through, or
|
|
1062
|
-
* stores nested documents shaped by the **target’s own
|
|
1063
|
-
* needs no reference type of its own, and reconstructs
|
|
1064
|
-
* resolved referent does.
|
|
1199
|
+
* The Root Types a {@link ReferenceStrategy.local local} lookup projects its
|
|
1200
|
+
* referents through, in order of precedence – or none for every other field.
|
|
1201
|
+
* Such a reference stores nested documents shaped by the **target’s own
|
|
1202
|
+
* declaration** – so it needs no reference type of its own, and reconstructs
|
|
1203
|
+
* through the same path a resolved referent does. With several targets, each
|
|
1204
|
+
* referent is projected through the one its `rdf:type` matches
|
|
1205
|
+
* ({@link TARGET_FIELD}).
|
|
1065
1206
|
*/
|
|
1066
|
-
export function
|
|
1067
|
-
if (
|
|
1068
|
-
field.kind !== 'reference' ||
|
|
1207
|
+
export function localLookupTargetsOf(field, schema) {
|
|
1208
|
+
if (field.kind !== 'reference' ||
|
|
1069
1209
|
field.ref?.strategy !== 'lookup' ||
|
|
1070
1210
|
field.ref.local !== true) {
|
|
1071
|
-
return
|
|
1211
|
+
return [];
|
|
1072
1212
|
}
|
|
1073
|
-
return
|
|
1213
|
+
return referencedTargetsOf(field, schema);
|
|
1074
1214
|
}
|
|
1075
1215
|
/**
|
|
1076
1216
|
* The nested reference an inline reference’s
|
|
1077
1217
|
* {@link ReferenceStrategy.identity identity companion} harvests, or
|
|
1078
1218
|
* `undefined` where the field declares none (or the schema cannot resolve its
|
|
1079
1219
|
* reference type). The one reading of *which nested field identifies the edge*,
|
|
1080
|
-
* so the projection (which harvests it), {@link
|
|
1220
|
+
* so the projection (which harvests it), {@link inheritedFacetPolicies} (which
|
|
1081
1221
|
* reads its target’s facet policy) and an adapter’s filter compiler cannot
|
|
1082
1222
|
* resolve it differently.
|
|
1083
1223
|
*/
|
|
@@ -1388,7 +1528,7 @@ export function unixSecondsToIso(seconds) {
|
|
|
1388
1528
|
* The facet is the one member that depends on more than the declaration: a
|
|
1389
1529
|
* facetable reference whose target declares a {@link FacetKeys facet policy}
|
|
1390
1530
|
* facets a `${name}_facet` companion rather than itself, and only the `schema`
|
|
1391
|
-
* can resolve that target ({@link
|
|
1531
|
+
* can resolve that target ({@link inheritedFacetPolicies}). Without one the field
|
|
1392
1532
|
* facets on its own name – the same reading the projection makes without a
|
|
1393
1533
|
* schema, where it cannot re-key a reference either.
|
|
1394
1534
|
*/
|
|
@@ -1414,7 +1554,7 @@ export function physicalFields(field, schema) {
|
|
|
1414
1554
|
// reference that names an `identity`, and a `local` lookup, whose own id is
|
|
1415
1555
|
// otherwise a level further in than an engine can weld a condition to.
|
|
1416
1556
|
const nestsAnObject = identityFieldOf(field, schema) !== undefined ||
|
|
1417
|
-
(
|
|
1557
|
+
(localLookupTargetsOf(field, schema).length > 0 &&
|
|
1418
1558
|
field.filterable === true);
|
|
1419
1559
|
const identity = nestsAnObject ? identityFieldName(field.name) : undefined;
|
|
1420
1560
|
const filtered = identity ?? field.name;
|
|
@@ -1423,16 +1563,20 @@ export function physicalFields(field, schema) {
|
|
|
1423
1563
|
sort: [],
|
|
1424
1564
|
facet: field.facetable !== true
|
|
1425
1565
|
? undefined
|
|
1426
|
-
:
|
|
1566
|
+
: inheritedFacetPolicies(field, schema).size === 0
|
|
1427
1567
|
? filtered
|
|
1428
1568
|
: `${filtered}_facet`,
|
|
1429
1569
|
identity,
|
|
1430
1570
|
};
|
|
1431
1571
|
}
|
|
1432
1572
|
/**
|
|
1433
|
-
* The {@link FacetKeys facet
|
|
1434
|
-
* Root Type
|
|
1435
|
-
*
|
|
1573
|
+
* The {@link FacetKeys facet policies} a field inherits, keyed by the `name` of
|
|
1574
|
+
* the Root Type declaring each: the `facetKeys` of every type the field names
|
|
1575
|
+
* ({@link labelSourceNamesOf}) that declares one, when the field is a facetable
|
|
1576
|
+
* reference and the schema resolves those types. Empty where nothing is
|
|
1577
|
+
* inherited. With several targets, an id is admitted to a bucket by the policy
|
|
1578
|
+
* of the target it belongs to, and unconditionally where that target declares
|
|
1579
|
+
* none. The boundary is *naming the
|
|
1436
1580
|
* target* – the same line along which a reference is re-keyed and a join is
|
|
1437
1581
|
* drawn – so a reference that names no type inherits nothing, whatever type
|
|
1438
1582
|
* its values happen to point at; and a `derive`d reference, which produces its
|
|
@@ -1448,16 +1592,19 @@ export function physicalFields(field, schema) {
|
|
|
1448
1592
|
* rather than declaring it twice is what keeps *which of a type’s ids deserve
|
|
1449
1593
|
* a bucket* a fact about the type.
|
|
1450
1594
|
*/
|
|
1451
|
-
export function
|
|
1595
|
+
export function inheritedFacetPolicies(field, schema) {
|
|
1596
|
+
const policies = new Map();
|
|
1452
1597
|
if (schema === undefined ||
|
|
1453
1598
|
field.kind !== 'reference' ||
|
|
1454
1599
|
field.facetable !== true ||
|
|
1455
1600
|
field.derive !== undefined) {
|
|
1456
|
-
return
|
|
1601
|
+
return policies;
|
|
1457
1602
|
}
|
|
1458
1603
|
const identity = identityFieldOf(field, schema);
|
|
1459
|
-
const
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1604
|
+
for (const target of referencedTargetsOf(identity ?? field, schema)) {
|
|
1605
|
+
if (target.facetKeys !== undefined) {
|
|
1606
|
+
policies.set(target.name, target.facetKeys);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
return policies;
|
|
1463
1610
|
}
|
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;AAsBhD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,YAAY,GACzB,IAAI,CAoSN"}
|
package/dist/testing.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { filterOn, filterOperatorFor, } from './query.js';
|
|
3
|
-
import { facetableFields, filterableFields, ID_FIELD,
|
|
3
|
+
import { facetableFields, filterableFields, ID_FIELD, identityFieldOf, inheritedFacetPolicies, referencedTargetsOf, nestedReferenceType, outputFields, } 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
|
|
@@ -212,8 +212,16 @@ export function describeSearchEngineContract(name, engine) {
|
|
|
212
212
|
// second assertion bites.
|
|
213
213
|
for (const searchType of types()) {
|
|
214
214
|
for (const field of facetableFields(searchType)) {
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
// A bucket keyed on an id of one target is admitted by that
|
|
216
|
+
// target’s policy, and a target declaring none admits every one of
|
|
217
|
+
// its ids. Which target a bucket belongs to is not visible here, so
|
|
218
|
+
// the contract is checked only where every target constrains: then
|
|
219
|
+
// a bucket failing every policy belongs to no admitting target.
|
|
220
|
+
const policies = [
|
|
221
|
+
...inheritedFacetPolicies(field, engine().schema).values(),
|
|
222
|
+
];
|
|
223
|
+
const targets = referencedTargetsOf(identityFieldOf(field, engine().schema) ?? field, engine().schema);
|
|
224
|
+
if (policies.length === 0 || policies.length < targets.length) {
|
|
217
225
|
continue;
|
|
218
226
|
}
|
|
219
227
|
const result = await engine().search(searchType, {
|
|
@@ -223,7 +231,7 @@ export function describeSearchEngineContract(name, engine) {
|
|
|
223
231
|
});
|
|
224
232
|
const buckets = result.facets[field.name] ?? [];
|
|
225
233
|
for (const bucket of buckets) {
|
|
226
|
-
expect(policy.only(bucket.value)).toBe(true);
|
|
234
|
+
expect(policies.some((policy) => policy.only(bucket.value))).toBe(true);
|
|
227
235
|
expect(bucket.label).toBeDefined();
|
|
228
236
|
}
|
|
229
237
|
if (field.output !== true || field.filterable !== true) {
|
|
@@ -231,7 +239,7 @@ export function describeSearchEngineContract(name, engine) {
|
|
|
231
239
|
}
|
|
232
240
|
const excluded = result.hits
|
|
233
241
|
.flatMap((hit) => referenceIds(hit.document[field.name]))
|
|
234
|
-
.find((id) => !policy.only(id));
|
|
242
|
+
.find((id) => !policies.some((policy) => policy.only(id)));
|
|
235
243
|
if (excluded === undefined) {
|
|
236
244
|
continue;
|
|
237
245
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.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",
|