@lde/search-api-graphql 0.17.0 → 0.19.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 +107 -22
- package/dist/facet-batch.d.ts +11 -0
- package/dist/facet-batch.d.ts.map +1 -1
- package/dist/facet-batch.js +43 -2
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-schema.d.ts","sourceRoot":"","sources":["../src/build-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,aAAa,EAQd,MAAM,SAAS,CAAC;AACjB,OAAO,
|
|
1
|
+
{"version":3,"file":"build-schema.d.ts","sourceRoot":"","sources":["../src/build-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,aAAa,EAQd,MAAM,SAAS,CAAC;AACjB,OAAO,EAKL,KAAK,YAAY,EAEjB,KAAK,WAAW,EAChB,KAAK,YAAY,EAElB,MAAM,aAAa,CAAC;AAgBrB,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;AAkBD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,yBAA8B,GACtC,aAAa,CAkef;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, GraphQLFloat, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLSchema, GraphQLString, printSchema, } from 'graphql';
|
|
2
|
-
import { facetableFields, filterableFields, filterOperatorFor, ID_FIELD, isRangeFacet, nestedReferenceType, outputFields, pageForOffset, sortableFields, unixSecondsToIso, } from '@lde/search/adapter';
|
|
2
|
+
import { AND_KEY, facetableFields, filterableFields, filterOn, filterOperatorFor, ID_FIELD, OR_KEY, isRangeFacet, 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)));
|
|
@@ -63,6 +63,20 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
63
63
|
count: { type: new GraphQLNonNull(GraphQLInt) },
|
|
64
64
|
},
|
|
65
65
|
});
|
|
66
|
+
// A boolean-facet bucket: the value as a real boolean, so the bucket a client
|
|
67
|
+
// selects round-trips straight into the `is` filter that selects it. No label
|
|
68
|
+
// – a boolean has no data label to resolve, and the sensible rendering (“with
|
|
69
|
+
// an image” / “without one”) is knowable only by the consumer.
|
|
70
|
+
const booleanBucket = new GraphQLObjectType({
|
|
71
|
+
name: 'BooleanBucket',
|
|
72
|
+
fields: {
|
|
73
|
+
value: {
|
|
74
|
+
type: new GraphQLNonNull(GraphQLBoolean),
|
|
75
|
+
resolve: (bucket) => bucket.is,
|
|
76
|
+
},
|
|
77
|
+
count: { type: new GraphQLNonNull(GraphQLInt) },
|
|
78
|
+
},
|
|
79
|
+
});
|
|
66
80
|
// The pagination actually applied (after queryDefaults), shared across every
|
|
67
81
|
// ‹Type›SearchResult so one client pager fragment serves all root types.
|
|
68
82
|
const paginationType = new GraphQLObjectType({
|
|
@@ -241,17 +255,54 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
241
255
|
// field and always exists – no type is unaddressable by IRI, whatever it
|
|
242
256
|
// declares.
|
|
243
257
|
const filterable = filterableFields(searchType);
|
|
258
|
+
/**
|
|
259
|
+
* One key per filterable field (plus the undeclared `id`), each typed by its
|
|
260
|
+
* OWN kind – so the operator a key accepts is fixed by the field it names,
|
|
261
|
+
* and a range on a keyword field cannot be written at all. This is the
|
|
262
|
+
* single vocabulary every level of `where` is built from: the same keys
|
|
263
|
+
* appear on the clause and on a criterion, so a field is never named a
|
|
264
|
+
* second way.
|
|
265
|
+
*/
|
|
266
|
+
const fieldKeys = () => {
|
|
267
|
+
const fields = {
|
|
268
|
+
[ID_FIELD]: { type: stringFilter },
|
|
269
|
+
};
|
|
270
|
+
for (const field of filterable) {
|
|
271
|
+
fields[field.name] = { type: whereFieldType(field) };
|
|
272
|
+
}
|
|
273
|
+
return fields;
|
|
274
|
+
};
|
|
275
|
+
// A criterion is an ATOM: exactly one field, enforced by `@oneOf`. That is
|
|
276
|
+
// what keeps `where` a flat conjunction of disjunctions – a criterion that
|
|
277
|
+
// could carry two keys would be a conjunction nested inside an `or`, and
|
|
278
|
+
// skip-own-filter (ADR 5) has no answer for a clause buried inside another.
|
|
279
|
+
const criterionInput = new GraphQLInputObjectType({
|
|
280
|
+
name: `${typeName}Criterion`,
|
|
281
|
+
description: 'A condition on exactly one field. Used inside `or`, where the criteria are alternatives.',
|
|
282
|
+
isOneOf: true,
|
|
283
|
+
fields: fieldKeys,
|
|
284
|
+
});
|
|
285
|
+
const orKey = {
|
|
286
|
+
type: new GraphQLList(new GraphQLNonNull(criterionInput)),
|
|
287
|
+
description: 'A disjunction: a document matches when ANY of these criteria holds. Combined with the sibling keys by AND, so it widens across fields without widening the query as a whole.',
|
|
288
|
+
};
|
|
289
|
+
// `and` carries further `Where`s rather than a separate clause type. Safe to
|
|
290
|
+
// be recursive: a conjunction inside a conjunction FLATTENS, and the one
|
|
291
|
+
// shape that would not – an `and` inside an `or` – is unreachable, because
|
|
292
|
+
// `or` holds only `@oneOf` criteria. So `where` stays a flat conjunction of
|
|
293
|
+
// disjunctions at any nesting depth, and a reader meets one input type
|
|
294
|
+
// instead of two near-identical ones.
|
|
244
295
|
const whereInput = new GraphQLInputObjectType({
|
|
245
296
|
name: `${typeName}Where`,
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
},
|
|
297
|
+
description: 'Sibling keys are combined with AND. Use `or` for a disjunction, and `and` when a query needs more than one of them.',
|
|
298
|
+
fields: () => ({
|
|
299
|
+
...fieldKeys(),
|
|
300
|
+
[OR_KEY]: orKey,
|
|
301
|
+
[AND_KEY]: {
|
|
302
|
+
type: new GraphQLList(new GraphQLNonNull(whereInput)),
|
|
303
|
+
description: 'Further groups of conditions, all of which apply. The way to carry a second `or` disjunction alongside the first.',
|
|
304
|
+
},
|
|
305
|
+
}),
|
|
255
306
|
});
|
|
256
307
|
const sortValues = {
|
|
257
308
|
RELEVANCE: { value: 'relevance' },
|
|
@@ -274,7 +325,8 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
274
325
|
},
|
|
275
326
|
});
|
|
276
327
|
// Keyed facets object: one field per facetable field, typed by its kind
|
|
277
|
-
// (range fields → [RangeBucket!],
|
|
328
|
+
// (range fields → [RangeBucket!], boolean fields → [BooleanBucket!], else
|
|
329
|
+
// [ValueBucket!]). Only the selected
|
|
278
330
|
// fields are resolved (GraphQL prunes the rest), so the selection IS the
|
|
279
331
|
// request; how they are computed – skip-own-filter, batched into one
|
|
280
332
|
// engine dispatch – lives in facet-batch.ts.
|
|
@@ -329,6 +381,13 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
329
381
|
},
|
|
330
382
|
};
|
|
331
383
|
}
|
|
384
|
+
/** The bucket type a facet’s kind earns: bins for a range facet, a real
|
|
385
|
+
* boolean for a boolean one, a keyed value otherwise. */
|
|
386
|
+
function bucketTypeFor(field) {
|
|
387
|
+
if (isRangeFacet(field))
|
|
388
|
+
return rangeBucket;
|
|
389
|
+
return field.kind === 'boolean' ? booleanBucket : valueBucket;
|
|
390
|
+
}
|
|
332
391
|
/** The keyed facets object for one type (only called with ≥ 1 facetable field). */
|
|
333
392
|
function facetsTypeFor(typeName, facetable) {
|
|
334
393
|
return new GraphQLObjectType({
|
|
@@ -337,7 +396,7 @@ export function buildGraphQLSchema(schema, options = {}) {
|
|
|
337
396
|
const fields = {};
|
|
338
397
|
for (const field of facetable) {
|
|
339
398
|
fields[field.name] = {
|
|
340
|
-
type: nonNullListOf(
|
|
399
|
+
type: nonNullListOf(bucketTypeFor(field)),
|
|
341
400
|
// The skip-own-filter query building, the batching into one
|
|
342
401
|
// engine dispatch and the degrade-to-[] error handling all live
|
|
343
402
|
// in the loader (facet-batch.ts).
|
|
@@ -399,40 +458,66 @@ function argsToQuery(args, context, searchType, maxPerPage) {
|
|
|
399
458
|
};
|
|
400
459
|
}
|
|
401
460
|
function whereToFilters(where, searchType) {
|
|
402
|
-
|
|
461
|
+
// `== null` deliberately: an explicit `where: null` is as absent as an omitted
|
|
462
|
+
// one, and reading keys off it would throw.
|
|
463
|
+
if (where == null) {
|
|
403
464
|
return [];
|
|
404
465
|
}
|
|
405
|
-
const filters =
|
|
406
|
-
//
|
|
407
|
-
|
|
466
|
+
const filters = criteriaOf(where, searchType).map(filterOn);
|
|
467
|
+
// Sibling field keys AND, so each becomes a one-criterion filter of its own,
|
|
468
|
+
// while `or` becomes a SINGLE filter carrying every alternative. That is the
|
|
469
|
+
// whole AND/OR mapping: which bucket a criterion lands in, never a combinator
|
|
470
|
+
// inferred from how deeply it is nested.
|
|
471
|
+
const alternatives = (where[OR_KEY] ?? []).flatMap((criterion) => criteriaOf(criterion, searchType));
|
|
472
|
+
if (alternatives.length > 0) {
|
|
473
|
+
filters.push({ or: alternatives });
|
|
474
|
+
}
|
|
475
|
+
// `and` carries further groups, each contributing its own filters – which is
|
|
476
|
+
// how a query states a second `or` alongside the first. Recursing keeps the
|
|
477
|
+
// result flat: nested conjunctions collapse into this one list, and an `and`
|
|
478
|
+
// can never sit inside an `or` (which holds only criteria).
|
|
479
|
+
for (const nested of where[AND_KEY] ?? []) {
|
|
480
|
+
filters.push(...whereToFilters(nested, searchType));
|
|
481
|
+
}
|
|
482
|
+
return filters;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* The criteria a keyed object carries, in declaration order. A `Criterion` is
|
|
486
|
+
* `@oneOf`, so it yields exactly one; a `Where`/`Clause` yields one per field
|
|
487
|
+
* key it sets. `id` is read first: no type declares it, so the field loop below
|
|
488
|
+
* never sees it.
|
|
489
|
+
*/
|
|
490
|
+
function criteriaOf(keyed, searchType) {
|
|
491
|
+
const criteria = [];
|
|
492
|
+
const id = keyed[ID_FIELD];
|
|
408
493
|
if (id !== undefined && id !== null) {
|
|
409
|
-
|
|
494
|
+
criteria.push({ field: ID_FIELD, in: id.in ?? [] });
|
|
410
495
|
}
|
|
411
496
|
for (const field of filterableFields(searchType)) {
|
|
412
|
-
const value =
|
|
497
|
+
const value = keyed[field.name];
|
|
413
498
|
if (value === undefined || value === null) {
|
|
414
499
|
continue;
|
|
415
500
|
}
|
|
416
501
|
switch (filterOperatorFor(field.kind)) {
|
|
417
502
|
case 'in':
|
|
418
|
-
|
|
503
|
+
criteria.push({
|
|
419
504
|
field: field.name,
|
|
420
505
|
in: value.in ?? [],
|
|
421
506
|
});
|
|
422
507
|
break;
|
|
423
508
|
case 'range': {
|
|
424
509
|
const range = value;
|
|
425
|
-
|
|
510
|
+
criteria.push({
|
|
426
511
|
field: field.name,
|
|
427
512
|
range: { min: range.min, max: range.max },
|
|
428
513
|
});
|
|
429
514
|
break;
|
|
430
515
|
}
|
|
431
516
|
default:
|
|
432
|
-
|
|
517
|
+
criteria.push({ field: field.name, is: value });
|
|
433
518
|
}
|
|
434
519
|
}
|
|
435
|
-
return
|
|
520
|
+
return criteria;
|
|
436
521
|
}
|
|
437
522
|
function rangeInput(name, bound) {
|
|
438
523
|
return new GraphQLInputObjectType({
|
package/dist/facet-batch.d.ts
CHANGED
|
@@ -28,6 +28,17 @@ export declare function createFacetLoader(engine: SearchEngine, searchType: Root
|
|
|
28
28
|
* a policy default on that field, e.g. valid-only `status`, so the facet
|
|
29
29
|
* counts across every value.) The queries are facet-only: no hits (`limit:
|
|
30
30
|
* 0`) and, with no hits to order, no `orderBy`.
|
|
31
|
+
*
|
|
32
|
+
* A clause is a facet’s **own** only when it carries exactly one criterion, on
|
|
33
|
+
* that field ({@link ownsFacet}). A disjunction (`or`) is nobody’s own and
|
|
34
|
+
* always stays: the user constrained the document as a whole, never that one
|
|
35
|
+
* axis, so there is no selection on it to widen. Keeping it is what leaves each facet
|
|
36
|
+
* *complete on its own field* – every value it offers is one the user can pick,
|
|
37
|
+
* with the count they will actually get. Dropping the clause’s own disjunct
|
|
38
|
+
* instead would hide values that do return hits (on an entity page: the very
|
|
39
|
+
* entity the page is about, absent from a sidebar beside results full of it),
|
|
40
|
+
* and dropping it whole would count a corpus the sibling facets do not.
|
|
41
|
+
* It also keeps a multi-field clause from ever splitting the batch.
|
|
31
42
|
*/
|
|
32
43
|
export declare function groupFacetQueries(query: SearchQuery, fields: readonly string[]): SearchQuery[];
|
|
33
44
|
//# sourceMappingURL=facet-batch.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"facet-batch.d.ts","sourceRoot":"","sources":["../src/facet-batch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,
|
|
1
|
+
{"version":3,"file":"facet-batch.d.ts","sourceRoot":"","sources":["../src/facet-batch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,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
|
@@ -60,9 +60,22 @@ export function createFacetLoader(engine, searchType, query, onFacetError) {
|
|
|
60
60
|
* a policy default on that field, e.g. valid-only `status`, so the facet
|
|
61
61
|
* counts across every value.) The queries are facet-only: no hits (`limit:
|
|
62
62
|
* 0`) and, with no hits to order, no `orderBy`.
|
|
63
|
+
*
|
|
64
|
+
* A clause is a facet’s **own** only when it carries exactly one criterion, on
|
|
65
|
+
* that field ({@link ownsFacet}). A disjunction (`or`) is nobody’s own and
|
|
66
|
+
* always stays: the user constrained the document as a whole, never that one
|
|
67
|
+
* axis, so there is no selection on it to widen. Keeping it is what leaves each facet
|
|
68
|
+
* *complete on its own field* – every value it offers is one the user can pick,
|
|
69
|
+
* with the count they will actually get. Dropping the clause’s own disjunct
|
|
70
|
+
* instead would hide values that do return hits (on an entity page: the very
|
|
71
|
+
* entity the page is about, absent from a sidebar beside results full of it),
|
|
72
|
+
* and dropping it whole would count a corpus the sibling facets do not.
|
|
73
|
+
* It also keeps a multi-field clause from ever splitting the batch.
|
|
63
74
|
*/
|
|
64
75
|
export function groupFacetQueries(query, fields) {
|
|
65
|
-
const filteredFields = new Set(query.where
|
|
76
|
+
const filteredFields = new Set(query.where
|
|
77
|
+
.map(ownedField)
|
|
78
|
+
.filter((field) => field !== undefined));
|
|
66
79
|
const facetOnly = { ...query, orderBy: [], limit: 0, offset: 0 };
|
|
67
80
|
const sharedFields = fields.filter((field) => !filteredFields.has(field));
|
|
68
81
|
const queries = [];
|
|
@@ -72,9 +85,37 @@ export function groupFacetQueries(query, fields) {
|
|
|
72
85
|
for (const field of fields.filter((field) => filteredFields.has(field))) {
|
|
73
86
|
queries.push({
|
|
74
87
|
...facetOnly,
|
|
75
|
-
where: query.where.filter((filter) => filter
|
|
88
|
+
where: query.where.filter((filter) => !ownsFacet(filter, field)),
|
|
76
89
|
facets: [field],
|
|
77
90
|
});
|
|
78
91
|
}
|
|
79
92
|
return queries;
|
|
80
93
|
}
|
|
94
|
+
/** Whether a clause is `field`’s own – it constrains that field and nothing
|
|
95
|
+
* else, so skip-own-filter removes it when counting that facet. */
|
|
96
|
+
function ownsFacet(filter, field) {
|
|
97
|
+
return ownedField(filter) === field;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The field a clause belongs to, or `undefined` when it belongs to none.
|
|
101
|
+
*
|
|
102
|
+
* A clause owns a field when **every** criterion names it – which covers the
|
|
103
|
+
* ordinary one-criterion filter and equally a same-field disjunction
|
|
104
|
+
* (`or: [{ created: { max: … } }, { created: { min: … } }]`, or a multi-select
|
|
105
|
+
* spelled as alternatives). Those ARE a selection on one axis, so skip-own-filter
|
|
106
|
+
* must drop them; leaving them in would compute the facet with the user’s own
|
|
107
|
+
* selection applied, offering back only what they already picked.
|
|
108
|
+
*
|
|
109
|
+
* A clause spanning several fields is nobody’s own: the user constrained the
|
|
110
|
+
* document as a whole, never one axis, so there is nothing on any single facet
|
|
111
|
+
* to widen.
|
|
112
|
+
*/
|
|
113
|
+
function ownedField(filter) {
|
|
114
|
+
const [first, ...rest] = filter.or;
|
|
115
|
+
if (first === undefined) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
return rest.every((criterion) => criterion.field === first.field)
|
|
119
|
+
? first.field
|
|
120
|
+
: undefined;
|
|
121
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search-api-graphql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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",
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
"@escape.tech/graphql-armor-cost-limit": "^2.4.3",
|
|
29
29
|
"@escape.tech/graphql-armor-max-depth": "^2.4.2",
|
|
30
30
|
"@graphql-yoga/render-graphiql": "^5.21.2",
|
|
31
|
-
"@lde/search": "^0.
|
|
31
|
+
"@lde/search": "^0.18.0",
|
|
32
32
|
"dataloader": "^2.2.3",
|
|
33
|
-
"graphql": "^16.
|
|
33
|
+
"graphql": "^16.9.0",
|
|
34
34
|
"graphql-yoga": "^5.21.2",
|
|
35
35
|
"negotiator": "^1.0.0",
|
|
36
36
|
"tslib": "^2.3.0"
|