@lde/search-api-graphql 0.22.0 → 0.23.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.
@@ -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;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"}
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;AA4GD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,yBAA8B,GACtC,aAAa,CAsxBf;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,yBAA8B,GACtC,MAAM,CAER"}
@@ -15,21 +15,28 @@ const scalarOutput = (scalar, field) => field.required === true ? new GraphQLNon
15
15
  * distinguishable. Consequence for consumers: a variable must be declared
16
16
  * `[IRI!]`, not `[String!]`, because GraphQL checks variable usage nominally.
17
17
  *
18
- * **Input coercion validates; output serialization does not.** Rejecting a bare
19
- * token on the way in is what turns `where: { material: { in: ["boerenbont"] } }`
20
- * from a silent empty result into an error saying why – the same job
21
- * `argsToQuery` already does for an out-of-range `perPage`.
18
+ * **It validates in both directions.** Rejecting a bare token on the way in is
19
+ * what turns `where: { material: { in: ["boerenbont"] } }` from a silent empty
20
+ * result into an error saying why – the same job `argsToQuery` already does for
21
+ * an out-of-range `perPage`. Outbound matters for the same reason: a type
22
+ * enforced in one direction only is not one a consumer can rely on, and the
23
+ * coarse discovery strategy reads `IRI` as the promise that a value is a
24
+ * selection key.
22
25
  *
23
- * It validates on the way **out** too, and deliberately: a type that is enforced
24
- * in one direction only is not a type a consumer can rely on, and the coarse
25
- * discovery strategy reads `IRI` as the promise that a value is a selection key.
26
- * Serving a non-IRI under it would make the promise false exactly where a
27
- * consumer acts on it. The projection applies the same rule at the source, so
28
- * the only way to reach this error is an index written before it existed – which
29
- * a reindex fixes, and which failing names instead of hiding.
26
+ * **Where a bad value is dropped rather than raised.** Raising is right where
27
+ * the value IS the document’s identity (`id`, a reference’s `id`): such a
28
+ * document cannot be selected, so serving it would hand back something
29
+ * unusable. It is wrong for a facet bucket – `value: IRI!` sits inside a chain
30
+ * of non-nulls up to the root field, so raising there nulls the WHOLE response
31
+ * and discards `items`, which is precisely what facet-batch.ts’s degradation
32
+ * contract forbids a supplementary sidebar count from doing. The facet
33
+ * resolvers therefore filter unselectable buckets out instead.
30
34
  *
31
35
  * What counts as an IRI is {@link isAbsoluteIri} – shared with the projection,
32
- * so the surface cannot promise something the index contradicts.
36
+ * which applies it on every route a reference value can take (graph path,
37
+ * `derive`, `from`, `transform`), so the surface cannot promise something the
38
+ * index contradicts and the outbound error is reachable only from an index
39
+ * written before that rule existed.
33
40
  */
34
41
  const iriScalar = new GraphQLScalarType({
35
42
  name: 'IRI',
@@ -133,7 +140,7 @@ export function buildGraphQLSchema(schema, options = {}) {
133
140
  // that selects it, with no `String`-to-`IRI` boundary in between – the
134
141
  // contract BooleanBucket already keeps for `is`.
135
142
  const iriBucket = new GraphQLObjectType({
136
- name: 'IriBucket',
143
+ name: 'IRIBucket',
137
144
  fields: valueBucketFields(iriScalar),
138
145
  });
139
146
  // A numeric range-facet bin: half-open `[min, max)` bounds (max null on an
@@ -254,7 +261,28 @@ export function buildGraphQLSchema(schema, options = {}) {
254
261
  // collide: searchSchema resolves its typeName to a declared Reference Type
255
262
  // and rejects duplicate names schema-wide.
256
263
  const referenceTypes = new Map();
257
- const takenTypeNames = new Set(rootTypeNames);
264
+ // Seeded with the shared types every schema carries, not just the root type
265
+ // names: a declaration is free to name a type `IRI` or `ValueBucket`, and
266
+ // without this it would pass both collision checks and fail at
267
+ // `new GraphQLSchema` with graphql-js’s “Schema must contain uniquely named
268
+ // types” – naming neither the declaration nor the field, which is the opaque
269
+ // failure these checks exist to replace.
270
+ const takenTypeNames = new Set([
271
+ ...rootTypeNames,
272
+ iriScalar.name,
273
+ keywordFilter.name,
274
+ iriFilter.name,
275
+ languageString.name,
276
+ valueBucket.name,
277
+ iriBucket.name,
278
+ rangeBucket.name,
279
+ booleanBucket.name,
280
+ paginationType.name,
281
+ sortDirection.name,
282
+ intRange.name,
283
+ floatRange.name,
284
+ dateRange.name,
285
+ ]);
258
286
  /** The label key each id-plus-label reference type was registered with; no
259
287
  * entry for a surfaced inline one, which carries fields instead of a label. */
260
288
  const labelKeys = new Map();
@@ -611,7 +639,7 @@ export function buildGraphQLSchema(schema, options = {}) {
611
639
  });
612
640
  // Keyed facets object: one field per facetable field, typed by its kind
613
641
  // (range fields → [RangeBucket!], boolean fields → [BooleanBucket!],
614
- // reference fields → [IriBucket!], else [ValueBucket!]). Only the selected
642
+ // reference fields → [IRIBucket!], else [ValueBucket!]). Only the selected
615
643
  // fields are resolved (GraphQL prunes the rest), so the selection IS the
616
644
  // request; how they are computed – skip-own-filter, batched into one
617
645
  // engine dispatch – lives in facet-batch.ts.
@@ -705,12 +733,28 @@ export function buildGraphQLSchema(schema, options = {}) {
705
733
  fields: () => {
706
734
  const fields = {};
707
735
  for (const field of facetable) {
736
+ const bucketType = bucketTypeFor(field);
708
737
  fields[field.name] = {
709
- type: nonNullListOf(bucketTypeFor(field)),
738
+ type: nonNullListOf(bucketType),
710
739
  // The skip-own-filter query building, the batching into one
711
740
  // engine dispatch and the degrade-to-[] error handling all live
712
741
  // in the loader (facet-batch.ts).
713
- resolve: (source) => source.loadFacet(field.name),
742
+ resolve: async (source) => {
743
+ const buckets = await source.loadFacet(field.name);
744
+ if (bucketType !== iriBucket) {
745
+ return buckets;
746
+ }
747
+ // A bucket keyed on something that is not an IRI is not a
748
+ // selection key – it cannot be sent back as the filter that
749
+ // selects it – so it is dropped rather than served under `IRI`.
750
+ // Dropping, not throwing: `value: IRI!` sits inside a chain of
751
+ // non-nulls up to the root field, so a raising coercion here
752
+ // would null the WHOLE response and take `items` with it, which
753
+ // is exactly what facet-batch.ts’s degradation contract forbids
754
+ // a supplementary sidebar count from doing.
755
+ return buckets.filter((bucket) => typeof bucket.value === 'string' &&
756
+ isAbsoluteIri(bucket.value));
757
+ },
714
758
  };
715
759
  }
716
760
  return fields;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/search-api-graphql",
3
- "version": "0.22.0",
3
+ "version": "0.23.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.2",
40
- "@lde/search": "^0.20.0",
40
+ "@lde/search": "^0.21.1",
41
41
  "commander": "^15.0.0",
42
42
  "dataloader": "^2.2.3",
43
43
  "graphql": "^16.9.0",