@lde/search-api-graphql 0.25.2 → 0.26.1
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 +188 -19
- package/dist/projection.d.ts +2 -5
- package/dist/projection.d.ts.map +1 -1
- package/dist/projection.js +31 -11
- 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,
|
|
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,EAIjB,KAAK,WAAW,EAChB,KAAK,YAAY,EAElB,MAAM,aAAa,CAAC;AAsBrB,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,eAAe,CAAC;AAIvB,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;AA4GD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,yBAA8B,GACtC,aAAa,CA85Bf;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, facetableFields, filterableFields, filterOn, filterOperatorFor, ID_FIELD, isAbsoluteIri, OR_KEY, isRangeFacet, joinGraph, nestedReferenceType, outputFields, pageForOffset, sortableFields, unixSecondsToIso, } from '@lde/search/adapter';
|
|
2
|
+
import { AND_KEY, facetableFields, filterableFields, labelTargetNameOf, localLookupTypeOf, referenceFields, filterOn, filterOperatorFor, ID_FIELD, isAbsoluteIri, OR_KEY, isRangeFacet, joinGraph, 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
|
import { projectionFor } from './projection.js';
|
|
@@ -262,6 +262,45 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
262
262
|
// collide: searchSchema resolves its typeName to a declared Reference Type
|
|
263
263
|
// and rejects duplicate names schema-wide.
|
|
264
264
|
const referenceTypes = new Map();
|
|
265
|
+
/**
|
|
266
|
+
* The lookup targets whose emitted type must carry a NULLABLE `id`: those
|
|
267
|
+
* some field reaches through a {@link ReferenceStrategy.local local} lookup,
|
|
268
|
+
* which stores what a document says about an endpoint whether or not the
|
|
269
|
+
* endpoint is identified.
|
|
270
|
+
*
|
|
271
|
+
* Computed over the whole schema before any type is registered, because one
|
|
272
|
+
* emitted type is SHARED by every field pointing at its target. Decided per
|
|
273
|
+
* field instead, the answer would depend on which field happened to register
|
|
274
|
+
* first: a plain `publisher` lookup registering before a `local` creator
|
|
275
|
+
* would make `id` non-null and then fail the response for every unidentified
|
|
276
|
+
* creator – exactly the endpoint `local` exists to serve.
|
|
277
|
+
*
|
|
278
|
+
* Nullability is therefore a property of the type, and takes the weakest
|
|
279
|
+
* guarantee any of its users can keep.
|
|
280
|
+
*/
|
|
281
|
+
const nullableIdTargets = new Set();
|
|
282
|
+
{
|
|
283
|
+
const walked = new Set();
|
|
284
|
+
const collect = (searchType) => {
|
|
285
|
+
if (walked.has(searchType.name)) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
walked.add(searchType.name);
|
|
289
|
+
for (const field of referenceFields(searchType)) {
|
|
290
|
+
if (field.ref?.strategy === 'lookup' && field.ref.local === true) {
|
|
291
|
+
nullableIdTargets.add(field.ref.target);
|
|
292
|
+
}
|
|
293
|
+
const nested = nestedReferenceType(schema, field) ??
|
|
294
|
+
localLookupTypeOf(field, schema);
|
|
295
|
+
if (nested !== undefined) {
|
|
296
|
+
collect(nested);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
for (const rootType of schema.values()) {
|
|
301
|
+
collect(rootType);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
265
304
|
// Seeded with the shared types every schema carries, not just the root type
|
|
266
305
|
// names: a declaration is free to name a type `IRI` or `ValueBucket`, and
|
|
267
306
|
// without this it would pass both collision checks and fail at
|
|
@@ -289,6 +328,7 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
289
328
|
// same type queried in its own right meet exactly one `‹Name›Where`.
|
|
290
329
|
const joins = joinGraph(schema);
|
|
291
330
|
const whereInputs = new Map();
|
|
331
|
+
const nestedFilters = new Map();
|
|
292
332
|
const referenceFilters = new Map();
|
|
293
333
|
/** The name a reference’s emitted type is keyed under: a `lookup`’s target,
|
|
294
334
|
* an `inline`’s reference type, an `idOnly`’s declared name (which names a
|
|
@@ -321,8 +361,11 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
321
361
|
const typeName = referencedTypeName(field.ref);
|
|
322
362
|
if (referenceTypes.has(typeName)) {
|
|
323
363
|
// Fields sharing a referent share one emitted type, and cannot disagree
|
|
324
|
-
// about
|
|
325
|
-
// an inline reference's from the Reference Type that does.
|
|
364
|
+
// about its FIELDS: a lookup's come from the target that names the type,
|
|
365
|
+
// an inline reference's from the Reference Type that does. They can
|
|
366
|
+
// disagree about `id` nullability, which is why that is settled over the
|
|
367
|
+
// whole schema up front ({@link nullableIdTargets}) rather than read off
|
|
368
|
+
// whichever field registers the type first.
|
|
326
369
|
return;
|
|
327
370
|
}
|
|
328
371
|
const graphQLName = rootTypeNames.has(typeName)
|
|
@@ -349,14 +392,30 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
349
392
|
// An inline referent’s is nullable, and load-bearing: a referent
|
|
350
393
|
// needs no identity, so a blank-node one nests exactly like a named
|
|
351
394
|
// one, minus this.
|
|
395
|
+
//
|
|
396
|
+
// A `local` lookup is the exception among lookups, for that same
|
|
397
|
+
// reason: it stores what the referring document says about the
|
|
398
|
+
// endpoint whether or not the endpoint is identified, so an entry
|
|
399
|
+
// may legitimately arrive without one. Declared non-null, every such
|
|
400
|
+
// entry would fail the response instead of serving what it has.
|
|
352
401
|
id: {
|
|
353
|
-
type: field.ref?.strategy === 'lookup'
|
|
402
|
+
type: field.ref?.strategy === 'lookup' &&
|
|
403
|
+
!nullableIdTargets.has(typeName)
|
|
354
404
|
? new GraphQLNonNull(iriScalar)
|
|
355
405
|
: iriScalar,
|
|
356
406
|
},
|
|
357
407
|
...Object.fromEntries(outputFields(nested).map((nestedField) => [
|
|
358
408
|
nestedField.name,
|
|
359
|
-
|
|
409
|
+
// `required` is relaxed for the same reason `id` is above: a
|
|
410
|
+
// `local` lookup carries what a document SAYS about an endpoint,
|
|
411
|
+
// and a document that named one inline states no more than it
|
|
412
|
+
// states. A field the target promises is non-null on the
|
|
413
|
+
// target's own document, not on a referrer's account of it, so
|
|
414
|
+
// holding this one to it fails the whole response for exactly
|
|
415
|
+
// the endpoint `local` exists to serve.
|
|
416
|
+
outputFieldConfig(nullableIdTargets.has(typeName)
|
|
417
|
+
? { ...nestedField, required: false }
|
|
418
|
+
: nestedField),
|
|
360
419
|
])),
|
|
361
420
|
}),
|
|
362
421
|
}));
|
|
@@ -450,6 +509,14 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
450
509
|
}
|
|
451
510
|
}
|
|
452
511
|
function whereFieldType(field, owner) {
|
|
512
|
+
// An inline reference is filterable in two ways at once, exactly as a
|
|
513
|
+
// joinable one is: by the ids its entries hold (its identity companion),
|
|
514
|
+
// and by a condition on an entry. Same input shape, because it is the same
|
|
515
|
+
// question one hop out – only what the hop costs differs.
|
|
516
|
+
const nested = nestedReferenceType(schema, field);
|
|
517
|
+
if (nested !== undefined) {
|
|
518
|
+
return nestedFilterFor(nested, field);
|
|
519
|
+
}
|
|
453
520
|
// A joinable reference is filterable in two ways at once – by the ids it
|
|
454
521
|
// holds, and by a condition on the referent – so it earns a richer input
|
|
455
522
|
// than the plain membership every other reference gets. Making the
|
|
@@ -461,6 +528,58 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
461
528
|
? referenceFilterFor(target)
|
|
462
529
|
: plainWhereFieldType(field);
|
|
463
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* The filter input an inline reference takes: the ids its entries hold, or a
|
|
533
|
+
* condition on one entry. `@oneOf`, so a criterion stays an atom.
|
|
534
|
+
*
|
|
535
|
+
* The `in` arm is typed by the target the reference’s
|
|
536
|
+
* {@link ReferenceStrategy.identity identity companion} names, so a bucket a
|
|
537
|
+
* consumer clicks feeds straight back in – the same round trip an ordinary
|
|
538
|
+
* facetable reference already guarantees. A reference declaring no identity
|
|
539
|
+
* has no ids to filter, so it offers the `where` arm alone.
|
|
540
|
+
*/
|
|
541
|
+
function nestedFilterFor(referenceType, field) {
|
|
542
|
+
const existing = nestedFilters.get(referenceType.name);
|
|
543
|
+
if (existing !== undefined) {
|
|
544
|
+
return existing;
|
|
545
|
+
}
|
|
546
|
+
const identityTarget = labelTargetNameOf(field, schema);
|
|
547
|
+
const nestedWhere = nestedWhereInputFor(referenceType);
|
|
548
|
+
const created = new GraphQLInputObjectType({
|
|
549
|
+
name: `${referenceType.name}Filter`,
|
|
550
|
+
description: `A condition on ${referenceType.name}: the ids its entries reference, or a condition on one entry.`,
|
|
551
|
+
isOneOf: true,
|
|
552
|
+
fields: () => ({
|
|
553
|
+
...(identityTarget !== undefined && {
|
|
554
|
+
in: { type: new GraphQLList(new GraphQLNonNull(iriScalar)) },
|
|
555
|
+
}),
|
|
556
|
+
where: { type: nestedWhere },
|
|
557
|
+
}),
|
|
558
|
+
});
|
|
559
|
+
nestedFilters.set(referenceType.name, created);
|
|
560
|
+
return created;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* The `where` input of a Reference Type – its filterable fields and nothing
|
|
564
|
+
* else. Deliberately without the `id` key every Root Type's `where` carries:
|
|
565
|
+
* an entry is read, not addressed, so it has no document key to filter on
|
|
566
|
+
* ({@link validateQuery} refuses one).
|
|
567
|
+
*/
|
|
568
|
+
function nestedWhereInputFor(referenceType) {
|
|
569
|
+
// Memoized by its only caller, which is memoized itself – so this is built
|
|
570
|
+
// once per Reference Type however many fields nest it.
|
|
571
|
+
return new GraphQLInputObjectType({
|
|
572
|
+
name: `${referenceType.name}Where`,
|
|
573
|
+
description: 'Sibling keys are combined with AND, and all of them must hold of the SAME entry.',
|
|
574
|
+
fields: () => Object.fromEntries(filterableFields(referenceType).map((nestedField) => [
|
|
575
|
+
nestedField.name,
|
|
576
|
+
{
|
|
577
|
+
type: whereFieldType(nestedField, referenceType),
|
|
578
|
+
description: nestedField.description,
|
|
579
|
+
},
|
|
580
|
+
])),
|
|
581
|
+
});
|
|
582
|
+
}
|
|
464
583
|
/** The filter input one field accepts, fixed by what the field keys on: a
|
|
465
584
|
* `reference` keys on identity (its target’s filter, or `IRIFilter` when it
|
|
466
585
|
* names no target), a `keyword` on literals. */
|
|
@@ -688,10 +807,10 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
688
807
|
},
|
|
689
808
|
},
|
|
690
809
|
resolve: async (_source, args, context, info) => {
|
|
691
|
-
const built = argsToQuery(args, context, searchType, maxPerPage, joins);
|
|
810
|
+
const built = argsToQuery(args, context, searchType, maxPerPage, joins, schema);
|
|
692
811
|
// What the client selected decides what each lookup fetches, so the
|
|
693
812
|
// engine carries the referent fields this query asked for and no more.
|
|
694
|
-
const resolve = projectionFor(info, searchType,
|
|
813
|
+
const resolve = projectionFor(info, searchType, schema);
|
|
695
814
|
const finalQuery = typeOptions?.queryDefaults
|
|
696
815
|
? typeOptions.queryDefaults(built, context)
|
|
697
816
|
: built;
|
|
@@ -790,7 +909,7 @@ export function printGraphQLSchema(schema, options = {}) {
|
|
|
790
909
|
/** Pure args → {@link SearchQuery} mapping. Rejects out-of-bounds paging
|
|
791
910
|
* (`page < 1`, `perPage` outside `1..maxPerPage`) with a clear error – a
|
|
792
911
|
* negative offset or an unbounded page size must not reach the engine. */
|
|
793
|
-
function argsToQuery(args, context, searchType, maxPerPage, joins) {
|
|
912
|
+
function argsToQuery(args, context, searchType, maxPerPage, joins, schema) {
|
|
794
913
|
const perPage = args.perPage ?? 20;
|
|
795
914
|
const page = args.page ?? 1;
|
|
796
915
|
if (page < 1) {
|
|
@@ -802,7 +921,7 @@ function argsToQuery(args, context, searchType, maxPerPage, joins) {
|
|
|
802
921
|
}
|
|
803
922
|
return {
|
|
804
923
|
text: args.query,
|
|
805
|
-
where: whereToFilters(args.where, searchType, joins),
|
|
924
|
+
where: whereToFilters(args.where, searchType, joins, schema),
|
|
806
925
|
orderBy: args.orderBy
|
|
807
926
|
? [{ field: args.orderBy.field, direction: args.orderBy.direction }]
|
|
808
927
|
: [],
|
|
@@ -822,7 +941,7 @@ function argsToQuery(args, context, searchType, maxPerPage, joins) {
|
|
|
822
941
|
* result is still flat: the nesting lives in each criterion’s `on` path, never
|
|
823
942
|
* in the clause structure, so skip-own-filter (ADR 5) still scans one level.
|
|
824
943
|
*/
|
|
825
|
-
function whereToFilters(where, searchType, joins, on = []) {
|
|
944
|
+
function whereToFilters(where, searchType, joins, schema, on = []) {
|
|
826
945
|
// `== null` deliberately: an explicit `where: null` is as absent as an omitted
|
|
827
946
|
// one, and reading keys off it would throw.
|
|
828
947
|
if (where == null) {
|
|
@@ -833,9 +952,22 @@ function whereToFilters(where, searchType, joins, on = []) {
|
|
|
833
952
|
// while `or` becomes a SINGLE filter carrying every alternative. That is the
|
|
834
953
|
// whole AND/OR mapping: which bucket a criterion lands in, never a combinator
|
|
835
954
|
// inferred from how deeply it is nested.
|
|
836
|
-
for (const entry of keyEntriesOf(where, searchType, joins)) {
|
|
955
|
+
for (const entry of keyEntriesOf(where, searchType, joins, schema)) {
|
|
837
956
|
if ('nested' in entry) {
|
|
838
|
-
|
|
957
|
+
// A NESTING hop welds: every condition inside one edge's `where` must
|
|
958
|
+
// hold of the SAME entry, which is the whole point of indexing an edge.
|
|
959
|
+
// Written as separate criteria they would be satisfiable by different
|
|
960
|
+
// entries – the parallel-array answer, silently wider than what was
|
|
961
|
+
// asked. A JOIN hop keeps recursing: those conditions are about another
|
|
962
|
+
// document, where there is nothing to weld them to.
|
|
963
|
+
if (entry.target.class === undefined) {
|
|
964
|
+
const welded = weldedCriterionOf(entry, joins, schema, on);
|
|
965
|
+
if (welded !== undefined) {
|
|
966
|
+
filters.push(filterOn(welded));
|
|
967
|
+
}
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
filters.push(...whereToFilters(entry.nested, entry.target, joins, schema, [
|
|
839
971
|
...on,
|
|
840
972
|
entry.name,
|
|
841
973
|
]));
|
|
@@ -843,7 +975,7 @@ function whereToFilters(where, searchType, joins, on = []) {
|
|
|
843
975
|
}
|
|
844
976
|
filters.push(filterOn(withPath(entry.criterion, on)));
|
|
845
977
|
}
|
|
846
|
-
const alternatives = (where[OR_KEY] ?? []).flatMap((criterion) => criteriaOf(criterion, searchType, joins, on));
|
|
978
|
+
const alternatives = (where[OR_KEY] ?? []).flatMap((criterion) => criteriaOf(criterion, searchType, joins, schema, on));
|
|
847
979
|
if (alternatives.length > 0) {
|
|
848
980
|
filters.push({ or: alternatives });
|
|
849
981
|
}
|
|
@@ -852,10 +984,30 @@ function whereToFilters(where, searchType, joins, on = []) {
|
|
|
852
984
|
// result flat: nested conjunctions collapse into this one list, and an `and`
|
|
853
985
|
// can never sit inside an `or` (which holds only criteria).
|
|
854
986
|
for (const nested of where[AND_KEY] ?? []) {
|
|
855
|
-
filters.push(...whereToFilters(nested, searchType, joins, on));
|
|
987
|
+
filters.push(...whereToFilters(nested, searchType, joins, schema, on));
|
|
856
988
|
}
|
|
857
989
|
return filters;
|
|
858
990
|
}
|
|
991
|
+
/**
|
|
992
|
+
* The one criterion an edge's `where` compiles to: its conditions welded to a
|
|
993
|
+
* single entry.
|
|
994
|
+
*
|
|
995
|
+
* Every key of that `where` becomes a condition. It carries no `or`/`and` of
|
|
996
|
+
* its own – `‹Edge›Where` declares neither, so the surface rejects them before
|
|
997
|
+
* a resolver runs: a disjunction inside a weld is not a weld, and the flat IR
|
|
998
|
+
* has nowhere to put one. `undefined` when the `where` states nothing, which
|
|
999
|
+
* constrains nothing and so contributes no clause.
|
|
1000
|
+
*/
|
|
1001
|
+
function weldedCriterionOf(entry, joins, schema, on) {
|
|
1002
|
+
// Every entry here is a plain criterion, and can only be: a hop needs either
|
|
1003
|
+
// a joinable reference (`joins.resolve` answers nothing from a Reference
|
|
1004
|
+
// Type) or a filterable inline one (which needs an `identity`, refused inside
|
|
1005
|
+
// a Reference Type). So there is nothing to skip.
|
|
1006
|
+
const conditions = keyEntriesOf(entry.nested, entry.target, joins, schema).map((nested) => nested.criterion);
|
|
1007
|
+
return conditions.length === 0
|
|
1008
|
+
? undefined
|
|
1009
|
+
: withPath({ field: entry.name, entry: conditions }, on);
|
|
1010
|
+
}
|
|
859
1011
|
/** A criterion with its join path attached, or unchanged at the top level –
|
|
860
1012
|
* so an ordinary query’s IR is byte-for-byte what it was. */
|
|
861
1013
|
function withPath(criterion, on) {
|
|
@@ -872,15 +1024,27 @@ function withPath(criterion, on) {
|
|
|
872
1024
|
* inside the `or` this feeds (ADR 18’s flat IR has nowhere to put it), so it is
|
|
873
1025
|
* rejected with the rewrite that expresses it instead.
|
|
874
1026
|
*/
|
|
875
|
-
function criteriaOf(keyed, searchType, joins, on = []) {
|
|
1027
|
+
function criteriaOf(keyed, searchType, joins, schema, on = []) {
|
|
876
1028
|
const criteria = [];
|
|
877
|
-
for (const entry of keyEntriesOf(keyed, searchType, joins)) {
|
|
1029
|
+
for (const entry of keyEntriesOf(keyed, searchType, joins, schema)) {
|
|
878
1030
|
if (!('nested' in entry)) {
|
|
879
1031
|
criteria.push(withPath(entry.criterion, on));
|
|
880
1032
|
continue;
|
|
881
1033
|
}
|
|
1034
|
+
// A NESTING hop welds, in an `or` alternative exactly as anywhere else –
|
|
1035
|
+
// and it is the one place a weld belongs most naturally, since a welded
|
|
1036
|
+
// criterion IS an atom. Compiled as separate joined criteria it would be
|
|
1037
|
+
// rejected below as “a conjunction inside an or”, which is precisely the
|
|
1038
|
+
// thing welding avoids.
|
|
1039
|
+
if (entry.target.class === undefined) {
|
|
1040
|
+
const welded = weldedCriterionOf(entry, joins, schema, on);
|
|
1041
|
+
if (welded !== undefined) {
|
|
1042
|
+
criteria.push(welded);
|
|
1043
|
+
}
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
882
1046
|
const path = [...on, entry.name];
|
|
883
|
-
const nested = whereToFilters(entry.nested, entry.target, joins, path);
|
|
1047
|
+
const nested = whereToFilters(entry.nested, entry.target, joins, schema, path);
|
|
884
1048
|
const [only] = nested;
|
|
885
1049
|
// An `or` alternative must be exactly one criterion. Empty and
|
|
886
1050
|
// more-than-one are opposite mistakes, so they are named separately rather
|
|
@@ -905,7 +1069,7 @@ function criteriaOf(keyed, searchType, joins, on = []) {
|
|
|
905
1069
|
* reference gets – while `where` states a condition on the referent and yields
|
|
906
1070
|
* a nested entry the caller walks into.
|
|
907
1071
|
*/
|
|
908
|
-
function keyEntriesOf(keyed, searchType, joins) {
|
|
1072
|
+
function keyEntriesOf(keyed, searchType, joins, schema) {
|
|
909
1073
|
const entries = [];
|
|
910
1074
|
const id = keyed[ID_FIELD];
|
|
911
1075
|
if (id !== undefined && id !== null) {
|
|
@@ -916,7 +1080,12 @@ function keyEntriesOf(keyed, searchType, joins) {
|
|
|
916
1080
|
if (value === undefined || value === null) {
|
|
917
1081
|
continue;
|
|
918
1082
|
}
|
|
919
|
-
|
|
1083
|
+
// Both hop kinds take the same `@oneOf` input – `in` (the ids this field
|
|
1084
|
+
// holds) or `where` (a condition one hop out) – so both are read here, and
|
|
1085
|
+
// only what they resolve to differs: another collection, or this
|
|
1086
|
+
// document's own entries.
|
|
1087
|
+
const target = nestedReferenceType(schema, field) ??
|
|
1088
|
+
joins.resolve(searchType, [field.name]);
|
|
920
1089
|
if (target !== undefined) {
|
|
921
1090
|
const nested = value
|
|
922
1091
|
.where;
|
package/dist/projection.d.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { type GraphQLResolveInfo } from 'graphql';
|
|
2
|
-
import { type ReferenceProjection, type SearchType } from '@lde/search';
|
|
3
|
-
/** How a `lookup` field’s `target` resolves to the Root Type it reads from –
|
|
4
|
-
* supplied by the caller, which holds the schema. */
|
|
5
|
-
export type TargetResolver = (target: string) => SearchType | undefined;
|
|
2
|
+
import { type ReferenceProjection, type SearchSchema, type SearchType } from '@lde/search';
|
|
6
3
|
/**
|
|
7
4
|
* Build the {@link ReferenceProjection} a query’s **selection set** asks for, so
|
|
8
5
|
* the engine fetches each referent’s fields at the depth the client selected and
|
|
@@ -20,5 +17,5 @@ export type TargetResolver = (target: string) => SearchType | undefined;
|
|
|
20
17
|
* omitted and then missing. Over-fetching a field is recoverable; a null where
|
|
21
18
|
* the client asked for a value is not.
|
|
22
19
|
*/
|
|
23
|
-
export declare function projectionFor(info: Pick<GraphQLResolveInfo, 'fieldNodes' | 'fragments'>, searchType: SearchType,
|
|
20
|
+
export declare function projectionFor(info: Pick<GraphQLResolveInfo, 'fieldNodes' | 'fragments'>, searchType: SearchType, schema: SearchSchema): ReferenceProjection | undefined;
|
|
24
21
|
//# sourceMappingURL=projection.d.ts.map
|
package/dist/projection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,kBAAkB,EAExB,MAAM,SAAS,CAAC;AACjB,OAAO,
|
|
1
|
+
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,kBAAkB,EAExB,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,YAAY,EACjB,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AASrB;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,IAAI,CAAC,kBAAkB,EAAE,YAAY,GAAG,WAAW,CAAC,EAC1D,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,YAAY,GACnB,mBAAmB,GAAG,SAAS,CAQjC"}
|
package/dist/projection.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Kind, } from 'graphql';
|
|
2
|
-
import { fieldNamed } from '@lde/search/adapter';
|
|
2
|
+
import { fieldNamed, nestedReferenceType, rootTypeNamed, } from '@lde/search/adapter';
|
|
3
3
|
/**
|
|
4
4
|
* Build the {@link ReferenceProjection} a query’s **selection set** asks for, so
|
|
5
5
|
* the engine fetches each referent’s fields at the depth the client selected and
|
|
@@ -17,11 +17,11 @@ import { fieldNamed } from '@lde/search/adapter';
|
|
|
17
17
|
* omitted and then missing. Over-fetching a field is recoverable; a null where
|
|
18
18
|
* the client asked for a value is not.
|
|
19
19
|
*/
|
|
20
|
-
export function projectionFor(info, searchType,
|
|
20
|
+
export function projectionFor(info, searchType, schema) {
|
|
21
21
|
// `items` carries the hits; the root field’s other selections (pagination,
|
|
22
22
|
// facets) are served without touching a referent.
|
|
23
23
|
const items = info.fieldNodes.flatMap((node) => selectionsNamed(node.selectionSet, 'items', info.fragments));
|
|
24
|
-
const projection = fromSelections(items, searchType, info.fragments,
|
|
24
|
+
const projection = fromSelections(items, searchType, info.fragments, schema);
|
|
25
25
|
return Object.keys(projection).length === 0 ? undefined : projection;
|
|
26
26
|
}
|
|
27
27
|
/** The selection sets of every field named `name`, fragments followed. */
|
|
@@ -81,30 +81,50 @@ function mergeProjections(left, right) {
|
|
|
81
81
|
return merged;
|
|
82
82
|
}
|
|
83
83
|
/** One level: which lookups were selected, and what each asked for. */
|
|
84
|
-
function fromSelections(selectionSets, searchType, fragments,
|
|
84
|
+
function fromSelections(selectionSets, searchType, fragments, schema) {
|
|
85
85
|
const projection = {};
|
|
86
86
|
for (const selectionSet of selectionSets) {
|
|
87
87
|
for (const selected of fieldsOf(selectionSet, fragments)) {
|
|
88
88
|
const field = fieldNamed(searchType, selected.name.value);
|
|
89
|
-
if (field === undefined
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
if (field === undefined) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// An INLINE reference is already carried by the hit, so it asks for
|
|
93
|
+
// nothing itself – but a lookup may sit inside it, and that one has to be
|
|
94
|
+
// reached. So the level is recorded with no `fields`, purely to carry
|
|
95
|
+
// what is selected below it.
|
|
96
|
+
const nested = nestedReferenceType(schema, field);
|
|
97
|
+
if (nested !== undefined) {
|
|
98
|
+
if (selected.selectionSet === undefined) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const below = fromSelections([selected.selectionSet], nested, fragments, schema);
|
|
102
|
+
if (Object.keys(below).length > 0) {
|
|
103
|
+
const entry = (projection[selected.name.value] ??= {});
|
|
104
|
+
entry.resolve = mergeProjections(entry.resolve, below);
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (field.kind !== 'reference' || field.ref?.strategy !== 'lookup') {
|
|
92
109
|
continue;
|
|
93
110
|
}
|
|
94
111
|
// The target's own declaration decides what its selections mean, so the
|
|
95
112
|
// level below is read against it rather than against this type.
|
|
96
|
-
|
|
113
|
+
// `searchSchema` rejects a lookup whose target it cannot resolve, so a
|
|
114
|
+
// schema always resolves its own – which is why this reads the schema
|
|
115
|
+
// rather than taking a resolver that could answer nothing.
|
|
116
|
+
const target = rootTypeNamed(schema, field.ref.target);
|
|
97
117
|
// Only what the target actually serves. A selection carries more than
|
|
98
118
|
// that: `id` is on the referring document already, and every GraphQL
|
|
99
119
|
// client worth the name injects `__typename` into every selection set –
|
|
100
120
|
// asking the engine for either would fail the query at the port's guard.
|
|
101
121
|
const wanted = fieldsOf(selected.selectionSet, fragments)
|
|
102
122
|
.map((node) => node.name.value)
|
|
103
|
-
.filter((name) =>
|
|
123
|
+
.filter((name) => servesField(target, name));
|
|
104
124
|
const entry = (projection[selected.name.value] ??= {});
|
|
105
125
|
entry.fields = [...new Set([...(entry.fields ?? []), ...wanted])];
|
|
106
|
-
if (
|
|
107
|
-
const below = fromSelections([selected.selectionSet], target, fragments,
|
|
126
|
+
if (selected.selectionSet !== undefined) {
|
|
127
|
+
const below = fromSelections([selected.selectionSet], target, fragments, schema);
|
|
108
128
|
if (Object.keys(below).length > 0) {
|
|
109
129
|
// Merged level by level, not key by key: one lookup selected twice –
|
|
110
130
|
// two fragments each spreading it – must union what each asked for,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search-api-graphql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
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.3",
|
|
40
|
-
"@lde/search": "^0.
|
|
40
|
+
"@lde/search": "^0.24.1",
|
|
41
41
|
"commander": "^15.0.0",
|
|
42
42
|
"dataloader": "^2.2.3",
|
|
43
43
|
"graphql": "^16.9.0",
|