@lde/search-api-graphql 0.20.1 → 0.20.3

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,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,CAiff;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,EASL,aAAa,EAQd,MAAM,SAAS,CAAC;AACjB,OAAO,EAKL,KAAK,YAAY,EAEjB,KAAK,WAAW,EAChB,KAAK,YAAY,EAElB,MAAM,aAAa,CAAC;AAkBrB,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,CA6gBf;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,yBAA8B,GACtC,MAAM,CAER"}
@@ -1,5 +1,5 @@
1
1
  import { GraphQLBoolean, GraphQLEnumType, GraphQLFloat, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLSchema, GraphQLString, printSchema, } from 'graphql';
2
- import { AND_KEY, facetableFields, filterableFields, filterOn, filterOperatorFor, ID_FIELD, OR_KEY, isRangeFacet, nestedReferenceType, outputFields, pageForOffset, sortableFields, unixSecondsToIso, } from '@lde/search/adapter';
2
+ import { AND_KEY, DEFAULT_LABEL_FIELD, facetableFields, filterableFields, filterOn, filterOperatorFor, ID_FIELD, OR_KEY, isRangeFacet, 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)));
@@ -21,7 +21,8 @@ function screamingSnake(name) {
21
21
  export function buildGraphQLSchema(schema, options = {}) {
22
22
  const languageOrder = options.languageOrder ?? defaultLanguageOrder;
23
23
  const maxPerPage = options.maxPerPage ?? 100;
24
- const rootTypeNames = new Set([...schema.values()].map((searchType) => searchType.name));
24
+ const rootTypesByName = new Map([...schema.values()].map((searchType) => [searchType.name, searchType]));
25
+ const rootTypeNames = new Set(rootTypesByName.keys());
25
26
  for (const name of Object.keys(options.types ?? {})) {
26
27
  if (!rootTypeNames.has(name)) {
27
28
  throw new Error(`Options given for type “${name}”, which is not in the search schema.`);
@@ -121,6 +122,19 @@ export function buildGraphQLSchema(schema, options = {}) {
121
122
  // and rejects duplicate names schema-wide.
122
123
  const referenceTypes = new Map();
123
124
  const takenTypeNames = new Set(rootTypeNames);
125
+ /** The label key each id-plus-label reference type was registered with; no
126
+ * entry for a surfaced inline one, which carries fields instead of a label. */
127
+ const labelKeys = new Map();
128
+ /** The key a reference serves its resolved label under: the name its label
129
+ * source declares that label field with, so the reference and the type it
130
+ * resolves against agree on the word. An id-only reference resolves no
131
+ * label, so it keeps the default. `searchSchema` guarantees the source is a
132
+ * declared Root Type. */
133
+ function labelKeyOf(field) {
134
+ return field.labelSource === undefined
135
+ ? DEFAULT_LABEL_FIELD
136
+ : labelFieldNameOf(rootTypesByName.get(field.labelSource));
137
+ }
124
138
  /**
125
139
  * Register the GraphQL type one reference field is served as, once per
126
140
  * referenced shape. A **surfaced inline** reference is served as the nested
@@ -131,9 +145,17 @@ export function buildGraphQLSchema(schema, options = {}) {
131
145
  * stays the id-plus-label pair its strategy carries.
132
146
  */
133
147
  function registerReferenceType(field, owner) {
134
- if (field.kind !== 'reference' ||
135
- field.ref === undefined ||
136
- referenceTypes.has(field.ref.typeName)) {
148
+ if (field.kind !== 'reference' || field.ref === undefined) {
149
+ return;
150
+ }
151
+ if (referenceTypes.has(field.ref.typeName)) {
152
+ // Fields sharing a `ref.typeName` share one emitted type, so they must
153
+ // agree on the word it serves its label under – otherwise which one wins
154
+ // would come down to declaration order.
155
+ const registered = labelKeys.get(field.ref.typeName);
156
+ if (registered !== undefined && registered !== labelKeyOf(field)) {
157
+ throw new Error(`Reference “${owner.name}.${field.name}” serves its label as “${labelKeyOf(field)}”, but “${field.ref.typeName}” is already served with “${registered}”; fields sharing a reference type must resolve labels from sources that agree on their labelField.`);
158
+ }
137
159
  return;
138
160
  }
139
161
  const { typeName } = field.ref;
@@ -145,17 +167,20 @@ export function buildGraphQLSchema(schema, options = {}) {
145
167
  }
146
168
  takenTypeNames.add(graphQLName);
147
169
  const nested = nestedReferenceType(schema, field);
170
+ if (nested === undefined) {
171
+ labelKeys.set(typeName, labelKeyOf(field));
172
+ }
148
173
  referenceTypes.set(typeName, new GraphQLObjectType({
149
174
  name: graphQLName,
150
175
  // A thunk, so a Reference Type nesting another one resolves whatever
151
176
  // the registration order is (the graph is acyclic by searchSchema).
152
177
  fields: () => nested === undefined
153
178
  ? {
179
+ // The same word the label source declares its label field
180
+ // under (`label` by default): one resolved label, one name for
181
+ // it wherever it surfaces.
154
182
  id: { type: new GraphQLNonNull(GraphQLString) },
155
- // `label`, the same word the label source declares and a
156
- // reference facet’s bucket carries: one resolved label, one
157
- // name for it wherever it surfaces.
158
- label: labelList((source) => source.label),
183
+ [labelKeyOf(field)]: labelList((source) => source.label),
159
184
  }
160
185
  : {
161
186
  id: { type: GraphQLString },
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { Command } from 'commander';
4
+ import { printSchemaModuleSdl } from './print-sdl.js';
5
+ const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
6
+ const program = new Command()
7
+ .name('search-print-sdl')
8
+ .description('Print the GraphQL contract of a mounted @lde/search schema-declaration module: the published surface consumers meet. Commit the output so every pull request that moves the surface shows the move.')
9
+ .version(version)
10
+ .requiredOption('--module <path>', 'path to the schema-declaration module, the same file the indexer and the served API mount')
11
+ .option('--out <path>', 'file to write the SDL to (default: standard output)')
12
+ .option('--no-format', 'write the SDL as GraphQL prints it, instead of formatting it with the Prettier configuration that applies to the output file');
13
+ program.parse();
14
+ const options = program.opts();
15
+ try {
16
+ const sdl = await printSchemaModuleSdl({
17
+ modulePath: options.module,
18
+ outputPath: options.out,
19
+ format: options.format,
20
+ });
21
+ if (options.out === undefined) {
22
+ process.stdout.write(sdl);
23
+ }
24
+ else {
25
+ console.info(`Wrote ${options.out}`);
26
+ }
27
+ }
28
+ catch (error) {
29
+ console.error(error instanceof Error ? error.message : String(error));
30
+ process.exit(1);
31
+ }
@@ -0,0 +1,28 @@
1
+ /** What {@link printSchemaModuleSdl} needs to know. */
2
+ export interface PrintSchemaModuleSdlOptions {
3
+ /** Path to the mounted schema-declaration module – the same file the
4
+ * indexer and the served API mount, so the printed contract cannot describe
5
+ * a different API from the one served. Its optional `schemaOptions` export
6
+ * is forwarded to {@link printGraphQLSchema}. */
7
+ readonly modulePath: string;
8
+ /** Where to write the SDL. Omit to only return it. */
9
+ readonly outputPath?: string;
10
+ /** Format the SDL with Prettier, resolving the config that applies to
11
+ * {@link outputPath} (default `true`). Keeps the file byte-identical to what
12
+ * a repo’s own formatter would produce, so a formatting pre-commit hook and
13
+ * this writer cannot spell the same schema differently and overwrite each
14
+ * other in turn. Requires `prettier` to be installed alongside this package.
15
+ */
16
+ readonly format?: boolean;
17
+ }
18
+ /**
19
+ * Print the GraphQL contract of a mounted schema-declaration module, and
20
+ * optionally write it to a file – the published surface consumers meet, which
21
+ * a repository commits so every pull request that moves it shows the move.
22
+ *
23
+ * Returns the SDL. Throws with the module path in the message for every
24
+ * failure mode of the module itself (unreadable, wrong export shape, invalid
25
+ * declaration), so a broken contract build names the file to fix.
26
+ */
27
+ export declare function printSchemaModuleSdl({ modulePath, outputPath, format, }: PrintSchemaModuleSdlOptions): Promise<string>;
28
+ //# sourceMappingURL=print-sdl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"print-sdl.d.ts","sourceRoot":"","sources":["../src/print-sdl.ts"],"names":[],"mappings":"AAMA,uDAAuD;AACvD,MAAM,WAAW,2BAA2B;IAC1C;;;sDAGkD;IAClD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,sDAAsD;IACtD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,UAAU,EACV,UAAU,EACV,MAAa,GACd,EAAE,2BAA2B,GAAG,OAAO,CAAC,MAAM,CAAC,CAkB/C"}
@@ -0,0 +1,48 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { loadSchemaModule, optionalObjectExport } from '@lde/search/module';
4
+ import { printGraphQLSchema } from './build-schema.js';
5
+ /**
6
+ * Print the GraphQL contract of a mounted schema-declaration module, and
7
+ * optionally write it to a file – the published surface consumers meet, which
8
+ * a repository commits so every pull request that moves it shows the move.
9
+ *
10
+ * Returns the SDL. Throws with the module path in the message for every
11
+ * failure mode of the module itself (unreadable, wrong export shape, invalid
12
+ * declaration), so a broken contract build names the file to fix.
13
+ */
14
+ export async function printSchemaModuleSdl({ modulePath, outputPath, format = true, }) {
15
+ const { schema, moduleExports } = await loadSchemaModule(modulePath);
16
+ const sdl = printGraphQLSchema(schema, optionalObjectExport(moduleExports, 'schemaOptions', modulePath));
17
+ // Prettier resolves its config from the file it is about to format, so a
18
+ // stdout run still has to name the path the SDL would live at.
19
+ const filepath = resolve(outputPath ?? 'schema.graphql');
20
+ const output = format ? await formatWithPrettier(sdl, filepath) : sdl;
21
+ if (outputPath !== undefined) {
22
+ await writeFile(filepath, output, 'utf8');
23
+ }
24
+ return output;
25
+ }
26
+ /** Prettier is an optional peer: the consumer’s own version formats the file,
27
+ * because matching the consumer’s repository is the whole point. */
28
+ async function formatWithPrettier(sdl, filepath) {
29
+ let prettier;
30
+ try {
31
+ prettier = await import('prettier');
32
+ }
33
+ catch (cause) {
34
+ // Reports what went wrong rather than assuming it is missing: a broken
35
+ // install and a plugin that throws at module scope both land here, and
36
+ // “install prettier” is unhelpful advice for either.
37
+ throw new Error(`Formatting the SDL requires “prettier”, an optional peer dependency of @lde/search-api-graphql, which could not be loaded: ${String(cause)}. Install it, or turn formatting off.`, { cause });
38
+ }
39
+ return prettier.format(sdl, {
40
+ // The CLI reads `.editorconfig` by default and the Node API does not, so
41
+ // leaving it off would have `prettier --write` reformat what we just wrote.
42
+ ...(await prettier.resolveConfig(filepath, { editorconfig: true })),
43
+ filepath,
44
+ // Named outright, so an output path Prettier cannot map to a parser (say
45
+ // `.sdl`) formats instead of failing on an inferred-parser error.
46
+ parser: 'graphql',
47
+ });
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/search-api-graphql",
3
- "version": "0.20.1",
3
+ "version": "0.20.3",
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",
@@ -15,11 +15,20 @@
15
15
  "import": "./dist/index.js",
16
16
  "development": "./src/index.ts",
17
17
  "default": "./dist/index.js"
18
+ },
19
+ "./print-sdl": {
20
+ "types": "./dist/print-sdl.d.ts",
21
+ "import": "./dist/print-sdl.js",
22
+ "development": "./src/print-sdl.ts",
23
+ "default": "./dist/print-sdl.js"
18
24
  }
19
25
  },
20
26
  "main": "./dist/index.js",
21
27
  "module": "./dist/index.js",
22
28
  "types": "./dist/index.d.ts",
29
+ "bin": {
30
+ "search-print-sdl": "dist/cli.js"
31
+ },
23
32
  "files": [
24
33
  "dist",
25
34
  "!**/*.tsbuildinfo"
@@ -28,11 +37,20 @@
28
37
  "@escape.tech/graphql-armor-cost-limit": "^2.4.3",
29
38
  "@escape.tech/graphql-armor-max-depth": "^2.4.2",
30
39
  "@graphql-yoga/render-graphiql": "^5.21.2",
31
- "@lde/search": "^0.18.1",
40
+ "@lde/search": "^0.18.3",
41
+ "commander": "^15.0.0",
32
42
  "dataloader": "^2.2.3",
33
43
  "graphql": "^16.9.0",
34
44
  "graphql-yoga": "^5.21.2",
35
45
  "negotiator": "^1.0.0",
36
46
  "tslib": "^2.3.0"
47
+ },
48
+ "peerDependencies": {
49
+ "prettier": "^3.0.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "prettier": {
53
+ "optional": true
54
+ }
37
55
  }
38
56
  }