@lde/search-api-graphql 0.21.1 → 0.22.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/build-schema.d.ts.map +1 -1
- package/dist/build-schema.js +218 -70
- package/dist/facet-batch.d.ts.map +1 -1
- package/dist/facet-batch.js +17 -3
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-schema.d.ts","sourceRoot":"","sources":["../src/build-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAWL,aAAa,EAQd,MAAM,SAAS,CAAC;AACjB,OAAO,EAKL,KAAK,YAAY,EAEjB,KAAK,WAAW,EAChB,KAAK,YAAY,EAElB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"build-schema.d.ts","sourceRoot":"","sources":["../src/build-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAWL,aAAa,EAQd,MAAM,SAAS,CAAC;AACjB,OAAO,EAKL,KAAK,YAAY,EAEjB,KAAK,WAAW,EAChB,KAAK,YAAY,EAElB,MAAM,aAAa,CAAC;AAqBrB,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,eAAe,CAAC;AAGvB,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B;;sDAEkD;IAClD,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,mFAAmF;IACnF,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACjE;AAED;2EAC2E;AAC3E,MAAM,WAAW,iBAAiB;IAChC;yCACqC;IACrC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;uBACmB;IACnB,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,aAAa,KACnB,WAAW,CAAC;CAClB;AAED,MAAM,WAAW,yBAAyB;IACxC;;kEAE8D;IAC9D,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAC7D,+EAA+E;IAC/E,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC;;gDAE4C;IAC5C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAqGD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,yBAA8B,GACtC,aAAa,CA6uBf;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,yBAA8B,GACtC,MAAM,CAER"}
|
package/dist/build-schema.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, printSchema, } from 'graphql';
|
|
2
|
-
import { AND_KEY, DEFAULT_LABEL_FIELD, facetableFields, filterableFields, filterOn, filterOperatorFor, ID_FIELD, isAbsoluteIri, OR_KEY, isRangeFacet, labelFieldNameOf, nestedReferenceType, outputFields, pageForOffset, sortableFields, unixSecondsToIso, } from '@lde/search/adapter';
|
|
2
|
+
import { AND_KEY, DEFAULT_LABEL_FIELD, facetableFields, filterableFields, filterOn, filterOperatorFor, ID_FIELD, isAbsoluteIri, OR_KEY, isRangeFacet, joinGraph, labelFieldNameOf, nestedReferenceType, outputFields, pageForOffset, sortableFields, unixSecondsToIso, } from '@lde/search/adapter';
|
|
3
3
|
import { defaultLanguageOrder, toLanguageStrings, } from './language.js';
|
|
4
4
|
import { createFacetLoader } from './facet-batch.js';
|
|
5
5
|
const nonNullListOf = (type) => new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(type)));
|
|
@@ -268,6 +268,12 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
268
268
|
? DEFAULT_LABEL_FIELD
|
|
269
269
|
: labelFieldNameOf(rootTypesByName.get(field.labelSource));
|
|
270
270
|
}
|
|
271
|
+
// The declared joins, and the input types they make shareable. Each map is
|
|
272
|
+
// keyed by the SearchType `name`, so a type reached as a join target and the
|
|
273
|
+
// same type queried in its own right meet exactly one `‹Name›Where`.
|
|
274
|
+
const joins = joinGraph(schema);
|
|
275
|
+
const whereInputs = new Map();
|
|
276
|
+
const referenceFilters = new Map();
|
|
271
277
|
/**
|
|
272
278
|
* Register the GraphQL type one reference field is served as, once per
|
|
273
279
|
* referenced shape. A **surfaced inline** reference is served as the nested
|
|
@@ -421,10 +427,22 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
421
427
|
};
|
|
422
428
|
}
|
|
423
429
|
}
|
|
430
|
+
function whereFieldType(field, owner) {
|
|
431
|
+
// A joinable reference is filterable in two ways at once – by the ids it
|
|
432
|
+
// holds, and by a condition on the referent – so it earns a richer input
|
|
433
|
+
// than the plain membership every other reference gets. Making the
|
|
434
|
+
// difference visible in the schema is the point: a consumer sees which
|
|
435
|
+
// references can be filtered through, instead of discovering it from a
|
|
436
|
+
// runtime error.
|
|
437
|
+
const target = joins.resolve(owner, [field.name]);
|
|
438
|
+
return target !== undefined
|
|
439
|
+
? referenceFilterFor(target)
|
|
440
|
+
: plainWhereFieldType(field);
|
|
441
|
+
}
|
|
424
442
|
/** The filter input one field accepts, fixed by what the field keys on: a
|
|
425
443
|
* `reference` keys on identity (its target’s filter, or `IRIFilter` when it
|
|
426
444
|
* names no target), a `keyword` on literals. */
|
|
427
|
-
function
|
|
445
|
+
function plainWhereFieldType(field) {
|
|
428
446
|
switch (filterOperatorFor(field.kind)) {
|
|
429
447
|
case 'in':
|
|
430
448
|
if (field.kind !== 'reference') {
|
|
@@ -443,58 +461,88 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
443
461
|
return GraphQLBoolean;
|
|
444
462
|
}
|
|
445
463
|
}
|
|
446
|
-
/**
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
464
|
+
/**
|
|
465
|
+
* The filter input a joinable reference to `target` takes, created once per
|
|
466
|
+
* target type and shared by EVERY field pointing at it – `publisher` and
|
|
467
|
+
* `creator` both resolving to `Organization` take the same
|
|
468
|
+
* `OrganizationReferenceFilter`, because what it can express is a property of
|
|
469
|
+
* the referenced type, not of the referring field.
|
|
470
|
+
*
|
|
471
|
+
* `@oneOf`, so a criterion stays an atom: filter by the ids the field holds,
|
|
472
|
+
* **or** by a condition on the referent, never both in one term.
|
|
473
|
+
*/
|
|
474
|
+
function referenceFilterFor(target) {
|
|
475
|
+
const existing = referenceFilters.get(target.name);
|
|
476
|
+
if (existing !== undefined) {
|
|
477
|
+
return existing;
|
|
478
|
+
}
|
|
479
|
+
const created = new GraphQLInputObjectType({
|
|
480
|
+
name: `${target.name}ReferenceFilter`,
|
|
481
|
+
description: `A condition on a joinable reference to ${target.name}: the ids it holds, or a condition on the referenced ${target.name} itself.`,
|
|
482
|
+
isOneOf: true,
|
|
483
|
+
// A thunk: the target’s own `where` may point back through further
|
|
484
|
+
// joinable references (the join graph is acyclic, so this terminates).
|
|
485
|
+
fields: () => ({
|
|
486
|
+
// `IRI`, exactly as `‹Target›Filter` types it: this arm asks the same
|
|
487
|
+
// question that filter asks – which of the target’s ids the field
|
|
488
|
+
// holds – so it must accept the same variable a consumer declares
|
|
489
|
+
// `[IRI!]` for. Typing it `String` here would make the join arm the
|
|
490
|
+
// one place identity is not `IRI`-keyed.
|
|
491
|
+
in: { type: new GraphQLList(new GraphQLNonNull(iriScalar)) },
|
|
492
|
+
where: { type: whereInputFor(target) },
|
|
493
|
+
}),
|
|
460
494
|
});
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
[ID_FIELD]: { type: targetFilter(typeName) },
|
|
480
|
-
};
|
|
481
|
-
for (const field of filterable) {
|
|
482
|
-
fields[field.name] = {
|
|
483
|
-
type: whereFieldType(field),
|
|
484
|
-
description: field.description,
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
return fields;
|
|
495
|
+
referenceFilters.set(target.name, created);
|
|
496
|
+
return created;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* One key per filterable field (plus the undeclared `id`), each typed by its
|
|
500
|
+
* OWN kind – so the operator a key accepts is fixed by the field it names,
|
|
501
|
+
* and a range on a keyword field cannot be written at all. This is the
|
|
502
|
+
* single vocabulary every level of `where` is built from: the same keys
|
|
503
|
+
* appear on the clause and on a criterion, so a field is never named a
|
|
504
|
+
* second way.
|
|
505
|
+
*/
|
|
506
|
+
function fieldKeys(searchType) {
|
|
507
|
+
const fields = {
|
|
508
|
+
// Typed self-referentially – `TermWhere.id: TermFilter` – so a consumer
|
|
509
|
+
// can resolve “the collection I am browsing” to “the filter type that
|
|
510
|
+
// accepts its IRIs”, which is what makes the refined discovery strategy
|
|
511
|
+
// work without hardcoding a single domain name.
|
|
512
|
+
[ID_FIELD]: { type: targetFilter(searchType.name) },
|
|
488
513
|
};
|
|
514
|
+
for (const field of filterableFields(searchType)) {
|
|
515
|
+
fields[field.name] = {
|
|
516
|
+
type: whereFieldType(field, searchType),
|
|
517
|
+
description: field.description,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
return fields;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* The `where` input of one root type, created once and shared: it is both the
|
|
524
|
+
* argument of that type’s own query field and – through a
|
|
525
|
+
* {@link referenceFilterFor} – the shape a *joined* condition on it takes
|
|
526
|
+
* from another type. One input either way, so the vocabulary a consumer
|
|
527
|
+
* learns for `datasets(where: …)` is the same one they write inside
|
|
528
|
+
* `creativeWorks(where: { dataset: { where: … } })`.
|
|
529
|
+
*/
|
|
530
|
+
function whereInputFor(searchType) {
|
|
531
|
+
const existing = whereInputs.get(searchType.name);
|
|
532
|
+
if (existing !== undefined) {
|
|
533
|
+
return existing;
|
|
534
|
+
}
|
|
489
535
|
// A criterion is an ATOM: exactly one field, enforced by `@oneOf`. That is
|
|
490
536
|
// what keeps `where` a flat conjunction of disjunctions – a criterion that
|
|
491
537
|
// could carry two keys would be a conjunction nested inside an `or`, and
|
|
492
538
|
// skip-own-filter (ADR 5) has no answer for a clause buried inside another.
|
|
539
|
+
// Created here rather than on its own, because it exists for exactly one
|
|
540
|
+
// `where` and shares its memoization.
|
|
493
541
|
const criterionInput = new GraphQLInputObjectType({
|
|
494
|
-
name: `${
|
|
542
|
+
name: `${searchType.name}Criterion`,
|
|
495
543
|
description: 'A condition on exactly one field. Used inside `or`, where the criteria are alternatives.',
|
|
496
544
|
isOneOf: true,
|
|
497
|
-
fields: fieldKeys,
|
|
545
|
+
fields: () => fieldKeys(searchType),
|
|
498
546
|
});
|
|
499
547
|
const orKey = {
|
|
500
548
|
type: new GraphQLList(new GraphQLNonNull(criterionInput)),
|
|
@@ -506,18 +554,41 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
506
554
|
// `or` holds only `@oneOf` criteria. So `where` stays a flat conjunction of
|
|
507
555
|
// disjunctions at any nesting depth, and a reader meets one input type
|
|
508
556
|
// instead of two near-identical ones.
|
|
509
|
-
const
|
|
510
|
-
name: `${
|
|
557
|
+
const created = new GraphQLInputObjectType({
|
|
558
|
+
name: `${searchType.name}Where`,
|
|
511
559
|
description: 'Sibling keys are combined with AND. Use `or` for a disjunction, and `and` when a query needs more than one of them.',
|
|
512
560
|
fields: () => ({
|
|
513
|
-
...fieldKeys(),
|
|
561
|
+
...fieldKeys(searchType),
|
|
514
562
|
[OR_KEY]: orKey,
|
|
515
563
|
[AND_KEY]: {
|
|
516
|
-
type: new GraphQLList(new GraphQLNonNull(
|
|
564
|
+
type: new GraphQLList(new GraphQLNonNull(created)),
|
|
517
565
|
description: 'Further groups of conditions, all of which apply. The way to carry a second `or` disjunction alongside the first.',
|
|
518
566
|
},
|
|
519
567
|
}),
|
|
520
568
|
});
|
|
569
|
+
whereInputs.set(searchType.name, created);
|
|
570
|
+
return created;
|
|
571
|
+
}
|
|
572
|
+
/** The root query field for one {@link RootType}, with its derived types. */
|
|
573
|
+
function rootField(searchType, typeOptions) {
|
|
574
|
+
const typeName = searchType.name;
|
|
575
|
+
const outputType = new GraphQLObjectType({
|
|
576
|
+
name: typeName,
|
|
577
|
+
fields: () => {
|
|
578
|
+
const fields = {
|
|
579
|
+
id: { type: new GraphQLNonNull(iriScalar) },
|
|
580
|
+
};
|
|
581
|
+
for (const field of outputFields(searchType)) {
|
|
582
|
+
fields[field.name] = outputFieldConfig(field);
|
|
583
|
+
}
|
|
584
|
+
return fields;
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
// Every type is filterable on `id`, so the input always has at least one
|
|
588
|
+
// field and always exists – no type is unaddressable by IRI, whatever it
|
|
589
|
+
// declares. It may already have been created as a joined type’s filter
|
|
590
|
+
// shape; `whereInputFor` hands back the same instance either way.
|
|
591
|
+
const whereInput = whereInputFor(searchType);
|
|
521
592
|
const sortValues = {
|
|
522
593
|
RELEVANCE: { value: 'relevance' },
|
|
523
594
|
};
|
|
@@ -593,7 +664,7 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
593
664
|
},
|
|
594
665
|
},
|
|
595
666
|
resolve: async (_source, args, context) => {
|
|
596
|
-
const built = argsToQuery(args, context, searchType, maxPerPage);
|
|
667
|
+
const built = argsToQuery(args, context, searchType, maxPerPage, joins);
|
|
597
668
|
const finalQuery = typeOptions?.queryDefaults
|
|
598
669
|
? typeOptions.queryDefaults(built, context)
|
|
599
670
|
: built;
|
|
@@ -673,7 +744,7 @@ export function printGraphQLSchema(schema, options = {}) {
|
|
|
673
744
|
/** Pure args → {@link SearchQuery} mapping. Rejects out-of-bounds paging
|
|
674
745
|
* (`page < 1`, `perPage` outside `1..maxPerPage`) with a clear error – a
|
|
675
746
|
* negative offset or an unbounded page size must not reach the engine. */
|
|
676
|
-
function argsToQuery(args, context, searchType, maxPerPage) {
|
|
747
|
+
function argsToQuery(args, context, searchType, maxPerPage, joins) {
|
|
677
748
|
const perPage = args.perPage ?? 20;
|
|
678
749
|
const page = args.page ?? 1;
|
|
679
750
|
if (page < 1) {
|
|
@@ -685,7 +756,7 @@ function argsToQuery(args, context, searchType, maxPerPage) {
|
|
|
685
756
|
}
|
|
686
757
|
return {
|
|
687
758
|
text: args.query,
|
|
688
|
-
where: whereToFilters(args.where, searchType),
|
|
759
|
+
where: whereToFilters(args.where, searchType, joins),
|
|
689
760
|
orderBy: args.orderBy
|
|
690
761
|
? [{ field: args.orderBy.field, direction: args.orderBy.direction }]
|
|
691
762
|
: [],
|
|
@@ -696,18 +767,37 @@ function argsToQuery(args, context, searchType, maxPerPage) {
|
|
|
696
767
|
locale: context.acceptLanguage[0] ?? 'und',
|
|
697
768
|
};
|
|
698
769
|
}
|
|
699
|
-
|
|
770
|
+
/**
|
|
771
|
+
* Compile one `Where` input into the flat conjunction of disjunctions the IR
|
|
772
|
+
* holds, at `on` hops out from the searched type (`[]` at the top level).
|
|
773
|
+
*
|
|
774
|
+
* A **joined** key carrying `where` recurses with the path extended by that
|
|
775
|
+
* key, and its clauses join this list – exactly how `and` already flattens. The
|
|
776
|
+
* result is still flat: the nesting lives in each criterion’s `on` path, never
|
|
777
|
+
* in the clause structure, so skip-own-filter (ADR 5) still scans one level.
|
|
778
|
+
*/
|
|
779
|
+
function whereToFilters(where, searchType, joins, on = []) {
|
|
700
780
|
// `== null` deliberately: an explicit `where: null` is as absent as an omitted
|
|
701
781
|
// one, and reading keys off it would throw.
|
|
702
782
|
if (where == null) {
|
|
703
783
|
return [];
|
|
704
784
|
}
|
|
705
|
-
const filters =
|
|
785
|
+
const filters = [];
|
|
706
786
|
// Sibling field keys AND, so each becomes a one-criterion filter of its own,
|
|
707
787
|
// while `or` becomes a SINGLE filter carrying every alternative. That is the
|
|
708
788
|
// whole AND/OR mapping: which bucket a criterion lands in, never a combinator
|
|
709
789
|
// inferred from how deeply it is nested.
|
|
710
|
-
const
|
|
790
|
+
for (const entry of keyEntriesOf(where, searchType, joins)) {
|
|
791
|
+
if ('nested' in entry) {
|
|
792
|
+
filters.push(...whereToFilters(entry.nested, entry.target, joins, [
|
|
793
|
+
...on,
|
|
794
|
+
entry.name,
|
|
795
|
+
]));
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
filters.push(filterOn(withPath(entry.criterion, on)));
|
|
799
|
+
}
|
|
800
|
+
const alternatives = (where[OR_KEY] ?? []).flatMap((criterion) => criteriaOf(criterion, searchType, joins, on));
|
|
711
801
|
if (alternatives.length > 0) {
|
|
712
802
|
filters.push({ or: alternatives });
|
|
713
803
|
}
|
|
@@ -716,47 +806,105 @@ function whereToFilters(where, searchType) {
|
|
|
716
806
|
// result flat: nested conjunctions collapse into this one list, and an `and`
|
|
717
807
|
// can never sit inside an `or` (which holds only criteria).
|
|
718
808
|
for (const nested of where[AND_KEY] ?? []) {
|
|
719
|
-
filters.push(...whereToFilters(nested, searchType));
|
|
809
|
+
filters.push(...whereToFilters(nested, searchType, joins, on));
|
|
720
810
|
}
|
|
721
811
|
return filters;
|
|
722
812
|
}
|
|
813
|
+
/** A criterion with its join path attached, or unchanged at the top level –
|
|
814
|
+
* so an ordinary query’s IR is byte-for-byte what it was. */
|
|
815
|
+
function withPath(criterion, on) {
|
|
816
|
+
return on.length === 0 ? criterion : { ...criterion, on };
|
|
817
|
+
}
|
|
723
818
|
/**
|
|
724
|
-
* The criteria a keyed object carries, in declaration order
|
|
725
|
-
* `@oneOf`, so it yields exactly one; a `Where
|
|
726
|
-
* key it sets.
|
|
727
|
-
*
|
|
819
|
+
* The criteria a keyed object carries, in declaration order, `on` hops out. A
|
|
820
|
+
* `Criterion` is `@oneOf`, so it yields exactly one; a `Where` yields one per
|
|
821
|
+
* field key it sets.
|
|
822
|
+
*
|
|
823
|
+
* A joined key carrying `where` is compiled through {@link whereToFilters} and
|
|
824
|
+
* must come back as exactly ONE one-criterion clause. Anything else – two field
|
|
825
|
+
* keys, an `or`, an `and` – is a conjunction, and a conjunction cannot sit
|
|
826
|
+
* inside the `or` this feeds (ADR 18’s flat IR has nowhere to put it), so it is
|
|
827
|
+
* rejected with the rewrite that expresses it instead.
|
|
728
828
|
*/
|
|
729
|
-
function criteriaOf(keyed, searchType) {
|
|
829
|
+
function criteriaOf(keyed, searchType, joins, on = []) {
|
|
730
830
|
const criteria = [];
|
|
831
|
+
for (const entry of keyEntriesOf(keyed, searchType, joins)) {
|
|
832
|
+
if (!('nested' in entry)) {
|
|
833
|
+
criteria.push(withPath(entry.criterion, on));
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
const path = [...on, entry.name];
|
|
837
|
+
const nested = whereToFilters(entry.nested, entry.target, joins, path);
|
|
838
|
+
const [only] = nested;
|
|
839
|
+
// An `or` alternative must be exactly one criterion. Empty and
|
|
840
|
+
// more-than-one are opposite mistakes, so they are named separately rather
|
|
841
|
+
// than sharing a message that fits only one of them.
|
|
842
|
+
if (only === undefined) {
|
|
843
|
+
throw new Error(`The joined condition on “${entry.name}” states no criterion, so it constrains nothing – and an “or” alternative that constrains nothing would make the whole disjunction match everything. Give it a condition, or leave the alternative out.`);
|
|
844
|
+
}
|
|
845
|
+
if (nested.length !== 1 || only.or.length !== 1) {
|
|
846
|
+
throw new Error(`The joined condition on “${entry.name}” states more than one criterion, so it is a conjunction – which cannot sit inside an “or”. Write one “or” alternative per criterion, or move the conjunction into “and”.`);
|
|
847
|
+
}
|
|
848
|
+
criteria.push(only.or[0]);
|
|
849
|
+
}
|
|
850
|
+
return criteria;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* The field keys a keyed input sets, in declaration order. `id` is read first:
|
|
854
|
+
* no type declares it, so the field loop never sees it.
|
|
855
|
+
*
|
|
856
|
+
* A joinable reference takes a `‹Target›ReferenceFilter` rather than a plain
|
|
857
|
+
* `StringFilter`, and its two `@oneOf` arms mean different things: `in` filters
|
|
858
|
+
* by the ids the field itself holds – no join, the same membership every other
|
|
859
|
+
* reference gets – while `where` states a condition on the referent and yields
|
|
860
|
+
* a nested entry the caller walks into.
|
|
861
|
+
*/
|
|
862
|
+
function keyEntriesOf(keyed, searchType, joins) {
|
|
863
|
+
const entries = [];
|
|
731
864
|
const id = keyed[ID_FIELD];
|
|
732
865
|
if (id !== undefined && id !== null) {
|
|
733
|
-
|
|
866
|
+
entries.push({ criterion: { field: ID_FIELD, in: id.in ?? [] } });
|
|
734
867
|
}
|
|
735
868
|
for (const field of filterableFields(searchType)) {
|
|
736
869
|
const value = keyed[field.name];
|
|
737
870
|
if (value === undefined || value === null) {
|
|
738
871
|
continue;
|
|
739
872
|
}
|
|
873
|
+
const target = joins.resolve(searchType, [field.name]);
|
|
874
|
+
if (target !== undefined) {
|
|
875
|
+
const nested = value
|
|
876
|
+
.where;
|
|
877
|
+
if (nested !== undefined && nested !== null) {
|
|
878
|
+
entries.push({ name: field.name, nested, target });
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
740
882
|
switch (filterOperatorFor(field.kind)) {
|
|
741
883
|
case 'in':
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
884
|
+
entries.push({
|
|
885
|
+
criterion: {
|
|
886
|
+
field: field.name,
|
|
887
|
+
in: value.in ?? [],
|
|
888
|
+
},
|
|
745
889
|
});
|
|
746
890
|
break;
|
|
747
891
|
case 'range': {
|
|
748
892
|
const range = value;
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
893
|
+
entries.push({
|
|
894
|
+
criterion: {
|
|
895
|
+
field: field.name,
|
|
896
|
+
range: { min: range.min, max: range.max },
|
|
897
|
+
},
|
|
752
898
|
});
|
|
753
899
|
break;
|
|
754
900
|
}
|
|
755
901
|
default:
|
|
756
|
-
|
|
902
|
+
entries.push({
|
|
903
|
+
criterion: { field: field.name, is: value },
|
|
904
|
+
});
|
|
757
905
|
}
|
|
758
906
|
}
|
|
759
|
-
return
|
|
907
|
+
return entries;
|
|
760
908
|
}
|
|
761
909
|
function rangeInput(name, bound) {
|
|
762
910
|
return new GraphQLInputObjectType({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"facet-batch.d.ts","sourceRoot":"","sources":["../src/facet-batch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"facet-batch.d.ts","sourceRoot":"","sources":["../src/facet-batch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,WAAW,EAEX,QAAQ,EACR,YAAY,EACZ,WAAW,EACZ,MAAM,aAAa,CAAC;AAErB,uFAAuF;AACvF,MAAM,MAAM,WAAW,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,WAAW,EAAE,CAAC,CAAC;AAE7E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,QAAQ,EACpB,KAAK,EAAE,WAAW,EAClB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,GACrD,WAAW,CAoCb;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,SAAS,MAAM,EAAE,GACxB,WAAW,EAAE,CAoBf"}
|
package/dist/facet-batch.js
CHANGED
|
@@ -115,7 +115,21 @@ function ownedField(filter) {
|
|
|
115
115
|
if (first === undefined) {
|
|
116
116
|
return undefined;
|
|
117
117
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
const key = axisOf(first);
|
|
119
|
+
return rest.every((criterion) => axisOf(criterion) === key) ? key : undefined;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The axis one criterion constrains: its field, qualified by the join path it
|
|
123
|
+
* reaches that field through. Two criteria are the same axis only when both
|
|
124
|
+
* agree – `dataset.publisher` and a local `publisher` are different questions,
|
|
125
|
+
* so a selection on one is no selection on the other.
|
|
126
|
+
*
|
|
127
|
+
* Facets are not yet offered on joined fields, so a joined clause’s key can
|
|
128
|
+
* never equal a requested facet name and such a clause simply never drops. That
|
|
129
|
+
* falls out of the key rather than being a rule of its own, and stays right if
|
|
130
|
+
* joined facets land later.
|
|
131
|
+
*/
|
|
132
|
+
function axisOf(criterion) {
|
|
133
|
+
const on = criterion.on ?? [];
|
|
134
|
+
return on.length === 0 ? criterion.field : [...on, criterion.field].join('.');
|
|
121
135
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search-api-graphql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Engine- and domain-agnostic GraphQL surface for @lde/search: builds an executable GraphQLSchema from a whole SearchSchema at runtime (no codegen) – one root query field per SearchType – served by generic resolvers over any SearchEngine – and serves it as a framework-agnostic fetch handler with a self-contained playground, SDL endpoint, CORS and depth/cost limits. You supply the schema and per-type typeNames; it names neither your domain nor your engine.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "git+https://github.com/ldelements/lde.git",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@escape.tech/graphql-armor-cost-limit": "^2.4.3",
|
|
38
38
|
"@escape.tech/graphql-armor-max-depth": "^2.4.2",
|
|
39
39
|
"@graphql-yoga/render-graphiql": "^5.21.2",
|
|
40
|
-
"@lde/search": "^0.
|
|
40
|
+
"@lde/search": "^0.20.0",
|
|
41
41
|
"commander": "^15.0.0",
|
|
42
42
|
"dataloader": "^2.2.3",
|
|
43
43
|
"graphql": "^16.9.0",
|