@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 +138 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +21 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +54 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/schema-module.d.ts +34 -0
- package/dist/schema-module.d.ts.map +1 -0
- package/dist/schema-module.js +27 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +62 -0
- package/package.json +61 -0
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 @@
|
|
|
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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -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
|
+
}
|