@lde/search-api-server 0.0.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/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # @lde/search-api-server
2
+
3
+ The served [@lde/search](../search) API as a bootable process and prebuilt
4
+ Docker image: mount a schema-declaration module, point it at Typesense, and it
5
+ serves `/graphql` – POST execution, the self-contained playground, the SDL –
6
+ plus `/health`, with CORS and depth/cost limits on by default.
7
+
8
+ This is the composition layer
9
+ ([#600](https://github.com/ldelements/lde/issues/600), layer 3) that binds the
10
+ engine-agnostic [`@lde/search-api-graphql`](../search-api-graphql) handler
11
+ (layer 2) to the [`@lde/search-typesense`](../search-typesense) engine. Use it
12
+ for turnkey, non-JS and ops-driven deployments; a JS host that wants custom
13
+ GraphQL fields mounts the handler itself instead (or builds `FROM` this image).
14
+
15
+ ## Run
16
+
17
+ ```sh
18
+ docker run --publish 4000:4000 \
19
+ --volume "$(pwd)/search-schema.mjs:/config/search-schema.mjs:ro" \
20
+ --env TYPESENSE_HOST=typesense.internal \
21
+ --env TYPESENSE_API_KEY=search-only-key \
22
+ ghcr.io/ldelements/search-api-server
23
+ ```
24
+
25
+ Or without Docker (the same environment variables apply):
26
+
27
+ ```sh
28
+ npx @lde/search-api-server
29
+ ```
30
+
31
+ ## The schema module
32
+
33
+ The mounted module default-exports the deployment’s search type declarations as
34
+ **plain data** – it must not import `@lde/search` (bare specifiers do not
35
+ resolve from a mounted file), and does not need to: the server validates the
36
+ declarations at boot, exactly as it would a SHACL generator’s output. Optional
37
+ functions (`derive`, `transform`) are allowed – it is a real JS module, only
38
+ without imports; they are projection-time declarations a serving process never
39
+ calls, carried so one module can drive both the indexer and this API.
40
+
41
+ Authoring in TypeScript works fine: compile the module to `.mjs` first
42
+ (`tsc` or esbuild) and mount the output – with `satisfies SearchType[]` you
43
+ get the full compile-time checking, and functions survive compilation. If the
44
+ module must import, bundle it (e.g. esbuild) before mounting.
45
+
46
+ ```js
47
+ // search-schema.mjs
48
+ export default [
49
+ {
50
+ name: 'Dataset',
51
+ class: 'http://www.w3.org/ns/dcat#Dataset',
52
+ fields: [
53
+ {
54
+ name: 'title',
55
+ kind: 'text',
56
+ locales: ['nl', 'en'],
57
+ output: true,
58
+ searchable: { weight: 5 },
59
+ },
60
+ {
61
+ name: 'keyword',
62
+ kind: 'keyword',
63
+ array: true,
64
+ facetable: true,
65
+ output: true,
66
+ },
67
+ ],
68
+ },
69
+ ];
70
+
71
+ // Optional: forwarded to buildGraphQLSchema (per-type options, maxPerPage, …).
72
+ export const schemaOptions = { maxPerPage: 50 };
73
+
74
+ // Optional: forwarded to createTypesenseSearchEngine (collection overrides, …).
75
+ export const engineOptions = {};
76
+ ```
77
+
78
+ Once the SHACL + `search:` generator lands
79
+ ([#495](https://github.com/ldelements/lde/issues/495)), mounted SHACL becomes
80
+ an additional source for the same schema.
81
+
82
+ ## Configuration
83
+
84
+ | Variable | Default | Meaning |
85
+ | -------------------- | --------------------------- | -------------------------------------------------- |
86
+ | `SCHEMA_MODULE` | `/config/search-schema.mjs` | Path of the mounted schema-declaration module |
87
+ | `PORT` | `4000` | TCP port the server binds |
88
+ | `GRAPHQL_ENDPOINT` | `/graphql` | Path serving GraphQL, the playground and the SDL |
89
+ | `PLAYGROUND` | `true` | Serve the playground on GET (`false`/`0` disables) |
90
+ | `MAX_DEPTH` | handler default (15) | Query depth cap |
91
+ | `MAX_COST` | handler default (5000) | Query cost cap |
92
+ | `TYPESENSE_HOST` | **required** | Typesense host |
93
+ | `TYPESENSE_PORT` | `8108` | Typesense port |
94
+ | `TYPESENSE_PROTOCOL` | `http` | `http` or `https` |
95
+ | `TYPESENSE_API_KEY` | **required** | Use a search-only key: the server only ever reads |
96
+
97
+ A misconfigured boot reports **all** problems in one error, not one per crash
98
+ loop.
99
+
100
+ ## Endpoints
101
+
102
+ - `POST /graphql` – GraphQL execution.
103
+ - `GET /graphql` – the self-contained playground (no external CDN, no framing
104
+ headers, so a docs site can `<iframe>` it as a live client).
105
+ - `GET /graphql?sdl` – the printed SDL: the contract without a running
106
+ introspection query.
107
+ - `GET /health` – liveness; the Docker image’s `HEALTHCHECK` uses it.
108
+ - `GET /` – redirects to the endpoint.
109
+
110
+ ## Building the image
111
+
112
+ The image is built from the workspace’s own outputs – the compiled package,
113
+ the same-commit builds of its `@lde/*` dependencies and a pruned lockfile –
114
+ never from npm, so it exists for any commit
115
+ ([ADR 15](../../docs/decisions/0015-ship-the-served-search-api-as-a-docker-image-built-from-the-workspace.md)):
116
+
117
+ ```sh
118
+ npx nx run @lde/search-api-server:docker:build # → packages-search-api-server
119
+ npx nx run @lde/search-api-server:docker:smoke # boots it and probes /health + ?sdl
120
+ ```
121
+
122
+ CI runs `docker:smoke` for affected PRs; each release tag rebuilds and pushes
123
+ `ghcr.io/ldelements/search-api-server:<version>` (`.github/workflows/docker.yml`).
124
+
125
+ ## Programmatic use
126
+
127
+ The bin is a thin wrapper over the exported API, usable in tests or a custom
128
+ boot:
129
+
130
+ ```ts
131
+ import {
132
+ configFromEnvironment,
133
+ createSearchApiServer,
134
+ } from '@lde/search-api-server';
135
+
136
+ const server = await createSearchApiServer(configFromEnvironment(process.env));
137
+ const port = await server.start();
138
+ ```
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,21 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { configFromEnvironment } from './config.js';
4
+ import { createSearchApiServer } from './server.js';
5
+ const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
6
+ try {
7
+ const config = configFromEnvironment(process.env);
8
+ const server = await createSearchApiServer(config);
9
+ const port = await server.start();
10
+ console.info(`@lde/search-api-server ${version} serving ${config.graphqlEndpoint} on port ${port}.`);
11
+ const shutdown = (signal) => {
12
+ console.info(`Received ${signal}; shutting down.`);
13
+ void server.stop().then(() => process.exit(0));
14
+ };
15
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
16
+ process.on('SIGINT', () => shutdown('SIGINT'));
17
+ }
18
+ catch (error) {
19
+ console.error(error instanceof Error ? error.message : String(error));
20
+ process.exit(1);
21
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The server’s complete runtime configuration, read from environment
3
+ * variables ({@link configFromEnvironment}) – the contract of the Docker
4
+ * image. Everything engine- or schema-shaped (query defaults, collection
5
+ * overrides, per-type GraphQL options) lives in the mounted schema module
6
+ * instead, next to the declarations it configures.
7
+ */
8
+ export interface ServerConfig {
9
+ /** Path of the mounted schema-declaration module (`SCHEMA_MODULE`). */
10
+ readonly schemaModulePath: string;
11
+ /** TCP port the HTTP server binds (`PORT`). */
12
+ readonly port: number;
13
+ /** Path serving GraphQL, the playground and the SDL (`GRAPHQL_ENDPOINT`). */
14
+ readonly graphqlEndpoint: string;
15
+ /** Serve the playground on GET requests to the endpoint (`PLAYGROUND`). */
16
+ readonly playground: boolean;
17
+ /** Query depth cap (`MAX_DEPTH`); the handler’s default when absent. */
18
+ readonly maxDepth?: number;
19
+ /** Query cost cap (`MAX_COST`); the handler’s default when absent. */
20
+ readonly maxCost?: number;
21
+ /** Where the Typesense engine reads (`TYPESENSE_*`). */
22
+ readonly typesense: TypesenseConnection;
23
+ }
24
+ /** Connection details of the Typesense node the engine searches. */
25
+ export interface TypesenseConnection {
26
+ readonly host: string;
27
+ readonly port: number;
28
+ readonly protocol: 'http' | 'https';
29
+ /** Use a search-only key: the server only ever reads. */
30
+ readonly apiKey: string;
31
+ }
32
+ /**
33
+ * Read the {@link ServerConfig} from environment variables. Reports **all**
34
+ * problems in one error – a misconfigured deployment learns everything
35
+ * wrong with it from a single boot attempt, not one variable per crash loop.
36
+ */
37
+ export declare function configFromEnvironment(environment: Readonly<Record<string, string | undefined>>): ServerConfig;
38
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,wDAAwD;IACxD,QAAQ,CAAC,SAAS,EAAE,mBAAmB,CAAC;CACzC;AAED,oEAAoE;AACpE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACpC,yDAAyD;IACzD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GACxD,YAAY,CA4Dd"}
package/dist/config.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Read the {@link ServerConfig} from environment variables. Reports **all**
3
+ * problems in one error – a misconfigured deployment learns everything
4
+ * wrong with it from a single boot attempt, not one variable per crash loop.
5
+ */
6
+ export function configFromEnvironment(environment) {
7
+ const problems = [];
8
+ const required = (name) => {
9
+ const value = environment[name];
10
+ if (value === undefined || value.length === 0) {
11
+ problems.push(`${name} is required`);
12
+ return '';
13
+ }
14
+ return value;
15
+ };
16
+ const integer = (name, fallback) => {
17
+ const value = environment[name];
18
+ if (value === undefined || value.length === 0) {
19
+ return fallback;
20
+ }
21
+ const parsed = Number(value);
22
+ if (!Number.isInteger(parsed) || parsed < 0) {
23
+ problems.push(`${name} must be a non-negative integer, got “${value}”`);
24
+ return fallback;
25
+ }
26
+ return parsed;
27
+ };
28
+ const graphqlEndpoint = environment['GRAPHQL_ENDPOINT'] ?? '/graphql';
29
+ if (!graphqlEndpoint.startsWith('/')) {
30
+ problems.push(`GRAPHQL_ENDPOINT must be an absolute path, got “${graphqlEndpoint}”`);
31
+ }
32
+ const protocol = environment['TYPESENSE_PROTOCOL'] ?? 'http';
33
+ if (protocol !== 'http' && protocol !== 'https') {
34
+ problems.push(`TYPESENSE_PROTOCOL must be “http” or “https”, got “${protocol}”`);
35
+ }
36
+ const config = {
37
+ schemaModulePath: environment['SCHEMA_MODULE'] ?? '/config/search-schema.mjs',
38
+ port: integer('PORT', 4000),
39
+ graphqlEndpoint,
40
+ playground: !['false', '0'].includes((environment['PLAYGROUND'] ?? '').toLowerCase()),
41
+ maxDepth: integer('MAX_DEPTH'),
42
+ maxCost: integer('MAX_COST'),
43
+ typesense: {
44
+ host: required('TYPESENSE_HOST'),
45
+ port: integer('TYPESENSE_PORT', 8108),
46
+ protocol: protocol,
47
+ apiKey: required('TYPESENSE_API_KEY'),
48
+ },
49
+ };
50
+ if (problems.length > 0) {
51
+ throw new Error(`Invalid configuration:\n${problems.map((problem) => `- ${problem}`).join('\n')}`);
52
+ }
53
+ return config;
54
+ }
@@ -0,0 +1,7 @@
1
+ export { createSearchApiServer } from './server.js';
2
+ export type { SearchApiServer } from './server.js';
3
+ export { configFromEnvironment } from './config.js';
4
+ export type { ServerConfig, TypesenseConnection } from './config.js';
5
+ export { loadSchemaModule } from './schema-module.js';
6
+ export type { SchemaModule } from './schema-module.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // The bootable served search API: the composition layer binding the
2
+ // engine-agnostic @lde/search-api-graphql handler to a Typesense engine, a
3
+ // mounted schema-declaration module and environment config. The `search-api-server`
4
+ // bin (src/cli.ts) and the Docker image are thin wrappers over these.
5
+ export { createSearchApiServer } from './server.js';
6
+ export { configFromEnvironment } from './config.js';
7
+ export { loadSchemaModule } from './schema-module.js';
@@ -0,0 +1,34 @@
1
+ import type { SearchSchema } from '@lde/search';
2
+ import type { BuildGraphQLSchemaOptions } from '@lde/search-api-graphql';
3
+ import type { TypesenseSearchEngineOptions } from '@lde/search-typesense';
4
+ /**
5
+ * What the mounted module declares: the deployment’s whole search
6
+ * declaration, plus the schema- and engine-shaped options that belong next to
7
+ * it. The module is **plain data with optional functions** (`derive`,
8
+ * `transform`) – it cannot `import '@lde/search'` (bare specifiers do not
9
+ * resolve from a mounted file), and does not need to: the declarations are
10
+ * validated here, the way a SHACL generator’s output would be. Once the
11
+ * SHACL + `search:` generator exists
12
+ * ([#495](https://github.com/ldelements/lde/issues/495)), a SHACL mount
13
+ * becomes an additional source for the same result.
14
+ */
15
+ export interface SchemaModule {
16
+ /** The validated schema built from the module’s default export. */
17
+ readonly searchSchema: SearchSchema;
18
+ /** The module’s optional `schemaOptions` export, forwarded to
19
+ * `buildGraphQLSchema` (per-type options, language order, `maxPerPage`). */
20
+ readonly schemaOptions?: BuildGraphQLSchemaOptions;
21
+ /** The module’s optional `engineOptions` export, forwarded to
22
+ * `createTypesenseSearchEngine` (collection overrides, query knobs). */
23
+ readonly engineOptions?: TypesenseSearchEngineOptions;
24
+ }
25
+ /**
26
+ * Load and validate a mounted schema-declaration module
27
+ * (`@lde/search/module`’s {@link loadDeclarations | loadSchemaModule} – the
28
+ * loader the indexer image shares, so both sides mount the same file), then
29
+ * validate the read side’s optional exports. Throws with the module path in
30
+ * the message for every failure mode, so a bad mount fails the boot with a
31
+ * diagnosis, never the first query.
32
+ */
33
+ export declare function loadSchemaModule(modulePath: string): Promise<SchemaModule>;
34
+ //# sourceMappingURL=schema-module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-module.d.ts","sourceRoot":"","sources":["../src/schema-module.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AACzE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE1E;;;;;;;;;;GAUG;AACH,MAAM,WAAW,YAAY;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC;iFAC6E;IAC7E,QAAQ,CAAC,aAAa,CAAC,EAAE,yBAAyB,CAAC;IACnD;6EACyE;IACzE,QAAQ,CAAC,aAAa,CAAC,EAAE,4BAA4B,CAAC;CACvD;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,CAAC,CAOvB"}
@@ -0,0 +1,27 @@
1
+ import { loadSchemaModule as loadDeclarations } from '@lde/search/module';
2
+ /**
3
+ * Load and validate a mounted schema-declaration module
4
+ * (`@lde/search/module`’s {@link loadDeclarations | loadSchemaModule} – the
5
+ * loader the indexer image shares, so both sides mount the same file), then
6
+ * validate the read side’s optional exports. Throws with the module path in
7
+ * the message for every failure mode, so a bad mount fails the boot with a
8
+ * diagnosis, never the first query.
9
+ */
10
+ export async function loadSchemaModule(modulePath) {
11
+ const { schema, moduleExports } = await loadDeclarations(modulePath);
12
+ return {
13
+ searchSchema: schema,
14
+ schemaOptions: optionalObject(moduleExports, 'schemaOptions', modulePath),
15
+ engineOptions: optionalObject(moduleExports, 'engineOptions', modulePath),
16
+ };
17
+ }
18
+ function optionalObject(moduleExports, name, modulePath) {
19
+ const value = moduleExports[name];
20
+ if (value === undefined) {
21
+ return undefined;
22
+ }
23
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
24
+ throw new Error(`Schema module “${modulePath}” export “${name}” must be an object.`);
25
+ }
26
+ return value;
27
+ }
@@ -0,0 +1,19 @@
1
+ import type { ServerConfig } from './config.js';
2
+ /** The bootable server: construct with {@link createSearchApiServer}. */
3
+ export interface SearchApiServer {
4
+ /** Start listening on the configured port; resolves with the bound port
5
+ * (useful with `port: 0`, which binds an ephemeral one). */
6
+ start(): Promise<number>;
7
+ /** Stop accepting connections and close the server. */
8
+ stop(): Promise<void>;
9
+ }
10
+ /**
11
+ * Compose the served search API from a {@link ServerConfig}: load and
12
+ * validate the mounted schema module, bind the Typesense engine, build the
13
+ * GraphQL handler, and wrap it all in a `node:http` server that also answers
14
+ * `GET /health` (liveness) and redirects `/` to the endpoint. Every
15
+ * misconfiguration – an invalid schema, an underivable collection name
16
+ * – throws here, at boot, never on the first query.
17
+ */
18
+ export declare function createSearchApiServer(config: ServerConfig): Promise<SearchApiServer>;
19
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B;iEAC6D;IAC7D,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,uDAAuD;IACvD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,eAAe,CAAC,CA8D1B"}
package/dist/server.js ADDED
@@ -0,0 +1,62 @@
1
+ import { createServer } from 'node:http';
2
+ import { createServerAdapter } from '@whatwg-node/server';
3
+ import { Client } from 'typesense';
4
+ import { createSearchGraphQLHandler } from '@lde/search-api-graphql';
5
+ import { createTypesenseSearchEngine } from '@lde/search-typesense';
6
+ import { loadSchemaModule } from './schema-module.js';
7
+ /**
8
+ * Compose the served search API from a {@link ServerConfig}: load and
9
+ * validate the mounted schema module, bind the Typesense engine, build the
10
+ * GraphQL handler, and wrap it all in a `node:http` server that also answers
11
+ * `GET /health` (liveness) and redirects `/` to the endpoint. Every
12
+ * misconfiguration – an invalid schema, an underivable collection name
13
+ * – throws here, at boot, never on the first query.
14
+ */
15
+ export async function createSearchApiServer(config) {
16
+ const { searchSchema, schemaOptions, engineOptions } = await loadSchemaModule(config.schemaModulePath);
17
+ const client = new Client({
18
+ nodes: [
19
+ {
20
+ host: config.typesense.host,
21
+ port: config.typesense.port,
22
+ protocol: config.typesense.protocol,
23
+ },
24
+ ],
25
+ apiKey: config.typesense.apiKey,
26
+ });
27
+ const engine = createTypesenseSearchEngine(client, searchSchema, engineOptions);
28
+ const graphql = createSearchGraphQLHandler({
29
+ searchSchema,
30
+ engine,
31
+ schemaOptions,
32
+ graphqlEndpoint: config.graphqlEndpoint,
33
+ playground: config.playground,
34
+ maxDepth: config.maxDepth,
35
+ maxCost: config.maxCost,
36
+ });
37
+ const adapter = createServerAdapter((request) => {
38
+ const url = new URL(request.url);
39
+ if (url.pathname === '/health') {
40
+ return Response.json({ status: 'ok' });
41
+ }
42
+ if (url.pathname === '/') {
43
+ return Response.redirect(new URL(config.graphqlEndpoint, url.origin).href, 302);
44
+ }
45
+ return graphql(request);
46
+ });
47
+ const server = createServer(adapter);
48
+ return {
49
+ start: () => new Promise((resolve, reject) => {
50
+ server.once('error', reject);
51
+ server.listen(config.port, () => {
52
+ resolve(server.address().port);
53
+ });
54
+ }),
55
+ stop: () => new Promise((resolve, reject) => {
56
+ server.close((error) => (error ? reject(error) : resolve()));
57
+ // In-flight requests finish; idle keep-alive sockets would otherwise
58
+ // hold the close (and a rolling restart) open indefinitely.
59
+ server.closeIdleConnections();
60
+ }),
61
+ };
62
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@lde/search-api-server",
3
+ "version": "0.0.1",
4
+ "description": "The served @lde/search API as a bootable process and prebuilt Docker image: loads a mounted schema-declaration module and Typesense connection details from the environment, then serves /graphql (execution, playground, SDL) plus /health over node:http. The composition layer that binds the engine-agnostic @lde/search-api-graphql handler to @lde/search-typesense – for turnkey, non-JS and ops-driven deployments.",
5
+ "repository": {
6
+ "url": "git+https://github.com/ldelements/lde.git",
7
+ "directory": "packages/search-api-server"
8
+ },
9
+ "license": "MIT",
10
+ "type": "module",
11
+ "exports": {
12
+ "./package.json": "./package.json",
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
16
+ "development": "./src/index.ts",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "main": "./dist/index.js",
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "bin": {
24
+ "search-api-server": "dist/cli.js"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "!**/*.tsbuildinfo"
29
+ ],
30
+ "dependencies": {
31
+ "@lde/search": "^0.12.2",
32
+ "@lde/search-api-graphql": "^0.12.2",
33
+ "@lde/search-typesense": "^0.15.4",
34
+ "@whatwg-node/server": "^0.11.0",
35
+ "tslib": "^2.3.0",
36
+ "typesense": "^3.0.6"
37
+ },
38
+ "nx": {
39
+ "targets": {
40
+ "docker:stage": {
41
+ "options": {
42
+ "cwd": "{projectRoot}",
43
+ "command": "rm -rf dist-docker && mkdir dist-docker && cp package.json dist-docker/ && cp -R dist dist-docker/dist"
44
+ }
45
+ },
46
+ "copy-workspace-modules": {},
47
+ "prune-lockfile": {},
48
+ "docker:lockfile": {},
49
+ "docker:smoke": {
50
+ "executor": "nx:run-commands",
51
+ "dependsOn": [
52
+ "docker:build"
53
+ ],
54
+ "cache": false,
55
+ "options": {
56
+ "command": "./tools/docker-smoke.sh packages-search-api-server"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }