@lde/search-api-graphql 0.10.0 → 0.12.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/README.md +54 -0
- package/dist/handler.d.ts +72 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +60 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -59,6 +59,60 @@ const gqlSchema = buildGraphQLSchema(searchSchema(DATASET, PERSON), {
|
|
|
59
59
|
Shared types (`LanguageString`, the facet buckets, filter inputs and reference
|
|
60
60
|
types such as a common `Agent`) are created once and reused across root types.
|
|
61
61
|
|
|
62
|
+
## Serving the API
|
|
63
|
+
|
|
64
|
+
`createSearchGraphQLHandler` turns the schema into a **served API**: one
|
|
65
|
+
framework-agnostic `(request: Request) => Promise<Response>` handler (built on
|
|
66
|
+
[graphql-yoga](https://the-guild.dev/graphql/yoga-server), see
|
|
67
|
+
[ADR 14](../../docs/decisions/0014-serve-the-search-graphql-api-with-graphql-yoga.md))
|
|
68
|
+
covering POST execution, introspection, error shaping and per-request
|
|
69
|
+
`Accept-Language` parsing:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { createSearchGraphQLHandler } from '@lde/search-api-graphql';
|
|
73
|
+
|
|
74
|
+
const handler = createSearchGraphQLHandler({
|
|
75
|
+
searchSchema: searchSchema(DATASET, PERSON),
|
|
76
|
+
engine, // e.g. createTypesenseSearchEngine(…)
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// SvelteKit (src/routes/graphql/+server.ts):
|
|
80
|
+
export const GET = ({ request }) => handler(request);
|
|
81
|
+
export const POST = GET;
|
|
82
|
+
|
|
83
|
+
// Plain node:http:
|
|
84
|
+
import { createServerAdapter } from '@whatwg-node/server';
|
|
85
|
+
createServer(createServerAdapter(handler)).listen(4000);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Every host that speaks `Request`/`Response` (SvelteKit, Hono, Fastify via a
|
|
89
|
+
bridge, plain Node) mounts it the same way. Batteries included:
|
|
90
|
+
|
|
91
|
+
- **Playground**: `GET /graphql` serves the bundled GraphiQL –
|
|
92
|
+
self-contained (no external CDN) and sent without framing headers, so a docs
|
|
93
|
+
site can `<iframe>` the deployed playground as a live client. Disable it per
|
|
94
|
+
environment (`playground: false`) or swap the renderer (`renderPlayground`).
|
|
95
|
+
- **SDL**: `GET /graphql?sdl` returns the schema contract as SDL – publish it
|
|
96
|
+
or generate static docs in CI without a running introspection query.
|
|
97
|
+
- **CORS** for cross-origin browser clients (configurable via `cors`).
|
|
98
|
+
- **Depth and cost limits** ([graphql-armor](https://escape.tech/graphql-armor/);
|
|
99
|
+
`maxDepth`, default 15, and `maxCost`, default 5000) guard the public
|
|
100
|
+
endpoint against arbitrarily expensive queries; introspection stays exempt.
|
|
101
|
+
|
|
102
|
+
To serve **custom fields next to the generated search API**, merge your own
|
|
103
|
+
schema with `buildGraphQLSchema()`’s output (e.g. `@graphql-tools/schema`’s
|
|
104
|
+
`mergeSchemas`) and pass the union as `schema` instead of `searchSchema`; the
|
|
105
|
+
same endpoint and playground serve both:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const handler = createSearchGraphQLHandler({
|
|
109
|
+
schema: mergeSchemas({
|
|
110
|
+
schemas: [buildGraphQLSchema(searchSchema(DATASET)), myCustomSchema],
|
|
111
|
+
}),
|
|
112
|
+
engine,
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
62
116
|
## Serving a subset of the schema
|
|
63
117
|
|
|
64
118
|
`types` never filters: every `SearchType` in the schema you pass gets a root
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type GraphQLSchema } from 'graphql';
|
|
2
|
+
import { type CORSOptions, type GraphiQLOptions } from 'graphql-yoga';
|
|
3
|
+
import type { SearchEngine, SearchSchema } from '@lde/search';
|
|
4
|
+
import { type BuildGraphQLSchemaOptions, type SearchContext } from './build-schema.js';
|
|
5
|
+
/**
|
|
6
|
+
* A framework-agnostic request handler: SvelteKit, Fastify (via a
|
|
7
|
+
* `Request`/`Response` bridge), Hono and plain `node:http` (via
|
|
8
|
+
* `@whatwg-node/server`, which graphql-yoga uses internally) all mount it
|
|
9
|
+
* directly.
|
|
10
|
+
*/
|
|
11
|
+
export type SearchGraphQLHandler = (request: Request) => Promise<Response>;
|
|
12
|
+
/**
|
|
13
|
+
* Renders the playground page served on `GET` requests to the endpoint.
|
|
14
|
+
* The default is the self-contained GraphiQL bundled with graphql-yoga
|
|
15
|
+
* (no external CDN); supply your own to swap the renderer (e.g. Altair).
|
|
16
|
+
*/
|
|
17
|
+
export type PlaygroundRenderer = (options?: GraphiQLOptions) => BodyInit | Promise<BodyInit>;
|
|
18
|
+
export interface SearchGraphQLHandlerOptions {
|
|
19
|
+
/**
|
|
20
|
+
* The search schema to serve; the handler builds the GraphQL schema from it
|
|
21
|
+
* with {@link buildGraphQLSchema}. Provide either this or {@link schema}.
|
|
22
|
+
*/
|
|
23
|
+
readonly searchSchema?: SearchSchema;
|
|
24
|
+
/**
|
|
25
|
+
* A ready {@link GraphQLSchema} to serve instead – bring your own fields by
|
|
26
|
+
* merging a custom schema with {@link buildGraphQLSchema}’s output (e.g.
|
|
27
|
+
* `@graphql-tools/schema` `mergeSchemas`) and passing the union here.
|
|
28
|
+
* Provide either this or {@link searchSchema}.
|
|
29
|
+
*/
|
|
30
|
+
readonly schema?: GraphQLSchema;
|
|
31
|
+
/** The engine every request’s resolvers search with. */
|
|
32
|
+
readonly engine: SearchEngine;
|
|
33
|
+
/** Options for {@link buildGraphQLSchema} when {@link searchSchema} is given. */
|
|
34
|
+
readonly schemaOptions?: BuildGraphQLSchemaOptions;
|
|
35
|
+
/** Endpoint path the handler serves. @default '/graphql' */
|
|
36
|
+
readonly graphqlEndpoint?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Serve the playground on `GET` requests to the endpoint. The page is
|
|
39
|
+
* self-contained (no external CDN) and sends no framing headers, so a docs
|
|
40
|
+
* site can `<iframe>` it as a live client. Disable per environment.
|
|
41
|
+
* @default true
|
|
42
|
+
*/
|
|
43
|
+
readonly playground?: boolean;
|
|
44
|
+
/** Custom playground renderer; defaults to the bundled GraphiQL. */
|
|
45
|
+
readonly renderPlayground?: PlaygroundRenderer;
|
|
46
|
+
/**
|
|
47
|
+
* CORS headers for cross-origin browser clients. Defaults to graphql-yoga’s
|
|
48
|
+
* permissive default (reflects the request origin); pass `false` to send no
|
|
49
|
+
* CORS headers.
|
|
50
|
+
*/
|
|
51
|
+
readonly cors?: CORSOptions | boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Reject queries nested deeper than this – a public playground invites
|
|
54
|
+
* arbitrary queries. Introspection is exempt. @default 15
|
|
55
|
+
*/
|
|
56
|
+
readonly maxDepth?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Reject queries costlier than this (graphql-armor cost limit; introspection
|
|
59
|
+
* is exempt). @default 5000
|
|
60
|
+
*/
|
|
61
|
+
readonly maxCost?: number;
|
|
62
|
+
/** Forwarded to {@link SearchContext.onFacetError} on every request. */
|
|
63
|
+
readonly onFacetError?: SearchContext['onFacetError'];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The served search API: one `fetch` handler covering POST execution, the GET
|
|
67
|
+
* playground, introspection, CORS, depth/cost limits, `Accept-Language`
|
|
68
|
+
* parsing, and the SDL (`GET <endpoint>?sdl` – the schema contract without a
|
|
69
|
+
* running introspection query).
|
|
70
|
+
*/
|
|
71
|
+
export declare function createSearchGraphQLHandler(options: SearchGraphQLHandlerOptions): SearchGraphQLHandler;
|
|
72
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AAKtB,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAEL,KAAK,yBAAyB,EAC9B,KAAK,aAAa,EACnB,MAAM,mBAAmB,CAAC;AAE3B;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE3E;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAC/B,OAAO,CAAC,EAAE,eAAe,KACtB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElC,MAAM,WAAW,2BAA2B;IAC1C;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,YAAY,CAAC;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC,wDAAwD;IACxD,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,iFAAiF;IACjF,QAAQ,CAAC,aAAa,CAAC,EAAE,yBAAyB,CAAC;IACnD,4DAA4D;IAC5D,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,oEAAoE;IACpE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC;IAC/C;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;IACtC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,YAAY,CAAC,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;CACvD;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,2BAA2B,GACnC,oBAAoB,CA2CtB"}
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { printSchema } from 'graphql';
|
|
2
|
+
import { createYoga, } from 'graphql-yoga';
|
|
3
|
+
import { renderGraphiQL } from '@graphql-yoga/render-graphiql';
|
|
4
|
+
import { maxDepthPlugin } from '@escape.tech/graphql-armor-max-depth';
|
|
5
|
+
import { costLimitPlugin } from '@escape.tech/graphql-armor-cost-limit';
|
|
6
|
+
import Negotiator from 'negotiator';
|
|
7
|
+
import { buildGraphQLSchema, } from './build-schema.js';
|
|
8
|
+
/**
|
|
9
|
+
* The served search API: one `fetch` handler covering POST execution, the GET
|
|
10
|
+
* playground, introspection, CORS, depth/cost limits, `Accept-Language`
|
|
11
|
+
* parsing, and the SDL (`GET <endpoint>?sdl` – the schema contract without a
|
|
12
|
+
* running introspection query).
|
|
13
|
+
*/
|
|
14
|
+
export function createSearchGraphQLHandler(options) {
|
|
15
|
+
if ((options.searchSchema === undefined) === (options.schema === undefined)) {
|
|
16
|
+
throw new Error('Provide exactly one of searchSchema or schema.');
|
|
17
|
+
}
|
|
18
|
+
const schema = options.schema ??
|
|
19
|
+
buildGraphQLSchema(options.searchSchema, options.schemaOptions);
|
|
20
|
+
const graphqlEndpoint = options.graphqlEndpoint ?? '/graphql';
|
|
21
|
+
const yoga = createYoga({
|
|
22
|
+
schema,
|
|
23
|
+
graphqlEndpoint,
|
|
24
|
+
landingPage: false,
|
|
25
|
+
graphiql: options.playground !== false,
|
|
26
|
+
renderGraphiQL: options.renderPlayground ?? renderGraphiQL,
|
|
27
|
+
cors: options.cors,
|
|
28
|
+
plugins: [
|
|
29
|
+
maxDepthPlugin({ n: options.maxDepth ?? 15 }),
|
|
30
|
+
costLimitPlugin({ maxCost: options.maxCost ?? 5000 }),
|
|
31
|
+
],
|
|
32
|
+
context: ({ request }) => ({
|
|
33
|
+
engine: options.engine,
|
|
34
|
+
acceptLanguage: parseAcceptLanguage(request.headers.get('accept-language')),
|
|
35
|
+
onFacetError: options.onFacetError,
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
const sdl = printSchema(schema);
|
|
39
|
+
return async (request) => {
|
|
40
|
+
const url = new URL(request.url);
|
|
41
|
+
if (request.method === 'GET' &&
|
|
42
|
+
url.pathname === graphqlEndpoint &&
|
|
43
|
+
url.searchParams.has('sdl')) {
|
|
44
|
+
return new Response(sdl, {
|
|
45
|
+
headers: { 'content-type': 'application/graphql; charset=utf-8' },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return await yoga.fetch(request, {});
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Ordered languages from an `Accept-Language` header (best first, by q-value). */
|
|
52
|
+
function parseAcceptLanguage(header) {
|
|
53
|
+
if (header === null) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const negotiator = new Negotiator({
|
|
57
|
+
headers: { 'accept-language': header },
|
|
58
|
+
});
|
|
59
|
+
return negotiator.languages().filter((language) => language !== '*');
|
|
60
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { buildGraphQLSchema, printGraphQLSchema } from './build-schema.js';
|
|
2
|
+
export { createSearchGraphQLHandler } from './handler.js';
|
|
3
|
+
export type { SearchGraphQLHandler, SearchGraphQLHandlerOptions, PlaygroundRenderer, } from './handler.js';
|
|
2
4
|
export type { SearchContext, BuildGraphQLSchemaOptions, SearchTypeOptions, } from './build-schema.js';
|
|
3
5
|
export { defaultLanguageOrder } from './language.js';
|
|
4
6
|
export type { LanguageString, LanguageOrder } from './language.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,YAAY,EACV,aAAa,EACb,yBAAyB,EACzB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAC1D,YAAY,EACV,oBAAoB,EACpB,2BAA2B,EAC3B,kBAAkB,GACnB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,aAAa,EACb,yBAAyB,EACzB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search-api-graphql",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Engine- and domain-agnostic GraphQL surface for @lde/search: builds an executable GraphQLSchema from a whole SearchSchema at runtime (no codegen)
|
|
3
|
+
"version": "0.12.0",
|
|
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",
|
|
7
7
|
"directory": "packages/search-api-graphql"
|
|
@@ -25,9 +25,14 @@
|
|
|
25
25
|
"!**/*.tsbuildinfo"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@
|
|
28
|
+
"@escape.tech/graphql-armor-cost-limit": "^2.4.3",
|
|
29
|
+
"@escape.tech/graphql-armor-max-depth": "^2.4.2",
|
|
30
|
+
"@graphql-yoga/render-graphiql": "^5.21.2",
|
|
31
|
+
"@lde/search": "^0.12.0",
|
|
29
32
|
"dataloader": "^2.2.3",
|
|
30
|
-
"graphql": "^
|
|
33
|
+
"graphql": "^16.0.0",
|
|
34
|
+
"graphql-yoga": "^5.21.2",
|
|
35
|
+
"negotiator": "^0.6.4",
|
|
31
36
|
"tslib": "^2.3.0"
|
|
32
37
|
}
|
|
33
38
|
}
|