@databehave/server 0.1.4 → 0.2.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/CHANGELOG.md +17 -0
- package/README.md +85 -83
- package/dist/cors.d.ts +1 -1
- package/dist/cors.js +1 -1
- package/dist/cors.js.map +1 -1
- package/dist/http.d.ts +94 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +23 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +10 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -18
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +9 -49
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +16 -92
- package/dist/log.js.map +1 -1
- package/dist/node-authority.d.ts +12 -0
- package/dist/node-authority.d.ts.map +1 -0
- package/dist/node-authority.js +72 -0
- package/dist/node-authority.js.map +1 -0
- package/dist/node-body.d.ts +11 -0
- package/dist/node-body.d.ts.map +1 -0
- package/dist/node-body.js +61 -0
- package/dist/node-body.js.map +1 -0
- package/dist/node-http.d.ts +19 -0
- package/dist/node-http.d.ts.map +1 -0
- package/dist/node-http.js +210 -0
- package/dist/node-http.js.map +1 -0
- package/dist/plugins/access-log.d.ts +11 -0
- package/dist/plugins/access-log.d.ts.map +1 -0
- package/dist/plugins/access-log.js +27 -0
- package/dist/plugins/access-log.js.map +1 -0
- package/dist/plugins/cors.d.ts +5 -0
- package/dist/plugins/cors.d.ts.map +1 -0
- package/dist/plugins/cors.js +35 -0
- package/dist/plugins/cors.js.map +1 -0
- package/dist/plugins/routes.d.ts +5 -0
- package/dist/plugins/routes.d.ts.map +1 -0
- package/dist/plugins/routes.js +10 -0
- package/dist/plugins/routes.js.map +1 -0
- package/dist/request.d.ts +2 -2
- package/dist/request.d.ts.map +1 -1
- package/dist/request.js +6 -4
- package/dist/request.js.map +1 -1
- package/dist/route-key.d.ts +2 -0
- package/dist/route-key.d.ts.map +1 -1
- package/dist/route-key.js +5 -1
- package/dist/route-key.js.map +1 -1
- package/dist/server.d.ts +7 -59
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +289 -466
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +10 -156
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/package.json +9 -22
- package/dist/admin/admin-routes.d.ts +0 -42
- package/dist/admin/admin-routes.d.ts.map +0 -1
- package/dist/admin/admin-routes.js +0 -472
- package/dist/admin/admin-routes.js.map +0 -1
- package/dist/admin/admin-types.d.ts +0 -170
- package/dist/admin/admin-types.d.ts.map +0 -1
- package/dist/admin/admin-types.js +0 -30
- package/dist/admin/admin-types.js.map +0 -1
- package/dist/admin/banner.d.ts +0 -11
- package/dist/admin/banner.d.ts.map +0 -1
- package/dist/admin/banner.js +0 -5
- package/dist/admin/banner.js.map +0 -1
- package/dist/admin/header-parser.d.ts +0 -14
- package/dist/admin/header-parser.d.ts.map +0 -1
- package/dist/admin/header-parser.js +0 -171
- package/dist/admin/header-parser.js.map +0 -1
- package/dist/admin/inject.d.ts +0 -48
- package/dist/admin/inject.d.ts.map +0 -1
- package/dist/admin/inject.js +0 -167
- package/dist/admin/inject.js.map +0 -1
- package/dist/admin/overrides-store.d.ts +0 -18
- package/dist/admin/overrides-store.d.ts.map +0 -1
- package/dist/admin/overrides-store.js +0 -100
- package/dist/admin/overrides-store.js.map +0 -1
- package/dist/admin/resolve-cors.d.ts +0 -22
- package/dist/admin/resolve-cors.d.ts.map +0 -1
- package/dist/admin/resolve-cors.js +0 -28
- package/dist/admin/resolve-cors.js.map +0 -1
- package/dist/admin/scenarios-store.d.ts +0 -15
- package/dist/admin/scenarios-store.d.ts.map +0 -1
- package/dist/admin/scenarios-store.js +0 -109
- package/dist/admin/scenarios-store.js.map +0 -1
- package/dist/admin/ui.css +0 -1
- package/dist/admin/ui.html +0 -13
- package/dist/admin/ui.js +0 -240
- package/dist/bin-helpers.d.ts +0 -46
- package/dist/bin-helpers.d.ts.map +0 -1
- package/dist/bin-helpers.js +0 -61
- package/dist/bin-helpers.js.map +0 -1
- package/dist/bin.d.ts +0 -3
- package/dist/bin.d.ts.map +0 -1
- package/dist/bin.js +0 -86
- package/dist/bin.js.map +0 -1
- package/dist/define.d.ts +0 -26
- package/dist/define.d.ts.map +0 -1
- package/dist/define.js +0 -55
- package/dist/define.js.map +0 -1
- package/dist/json-config.d.ts +0 -170
- package/dist/json-config.d.ts.map +0 -1
- package/dist/json-config.js +0 -684
- package/dist/json-config.js.map +0 -1
- package/dist/jsonc.d.ts +0 -24
- package/dist/jsonc.d.ts.map +0 -1
- package/dist/jsonc.js +0 -125
- package/dist/jsonc.js.map +0 -1
- package/dist/middleware/request-validation.d.ts +0 -58
- package/dist/middleware/request-validation.d.ts.map +0 -1
- package/dist/middleware/request-validation.js +0 -328
- package/dist/middleware/request-validation.js.map +0 -1
- package/dist/mock-mode.d.ts +0 -95
- package/dist/mock-mode.d.ts.map +0 -1
- package/dist/mock-mode.js +0 -95
- package/dist/mock-mode.js.map +0 -1
- package/dist/openapi/auto-schema.d.ts +0 -83
- package/dist/openapi/auto-schema.d.ts.map +0 -1
- package/dist/openapi/auto-schema.js +0 -232
- package/dist/openapi/auto-schema.js.map +0 -1
- package/dist/openapi/generate.d.ts +0 -20
- package/dist/openapi/generate.d.ts.map +0 -1
- package/dist/openapi/generate.js +0 -121
- package/dist/openapi/generate.js.map +0 -1
- package/dist/openapi/loader.d.ts +0 -18
- package/dist/openapi/loader.d.ts.map +0 -1
- package/dist/openapi/loader.js +0 -25
- package/dist/openapi/loader.js.map +0 -1
- package/dist/openapi/register.d.ts +0 -58
- package/dist/openapi/register.d.ts.map +0 -1
- package/dist/openapi/register.js +0 -166
- package/dist/openapi/register.js.map +0 -1
- package/dist/openapi/seed.d.ts +0 -16
- package/dist/openapi/seed.d.ts.map +0 -1
- package/dist/openapi/seed.js +0 -58
- package/dist/openapi/seed.js.map +0 -1
- package/dist/openapi/translate.d.ts +0 -55
- package/dist/openapi/translate.d.ts.map +0 -1
- package/dist/openapi/translate.js +0 -319
- package/dist/openapi/translate.js.map +0 -1
- package/dist/openapi/walker.d.ts +0 -44
- package/dist/openapi/walker.d.ts.map +0 -1
- package/dist/openapi/walker.js +0 -15
- package/dist/openapi/walker.js.map +0 -1
- package/dist/run.d.ts +0 -27
- package/dist/run.d.ts.map +0 -1
- package/dist/run.js +0 -87
- package/dist/run.js.map +0 -1
- package/dist/validation/validate.d.ts +0 -38
- package/dist/validation/validate.d.ts.map +0 -1
- package/dist/validation/validate.js +0 -372
- package/dist/validation/validate.js.map +0 -1
package/dist/json-config.js
DELETED
|
@@ -1,684 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JSON / JSONC config loader for @databehave/server.
|
|
3
|
-
*
|
|
4
|
-
* Reads a `databehave.json` (or `.jsonc`) file from disk, validates
|
|
5
|
-
* its shape, resolves the OpenAPI document, dynamically imports every
|
|
6
|
-
* handler referenced by `endpoints`, and wires the built-in mock-mode
|
|
7
|
-
* middleware. The returned value is a regular `Config` ready
|
|
8
|
-
* for `createServer`.
|
|
9
|
-
*
|
|
10
|
-
* JSONC features (`//` comments, `/* block * /` comments, trailing
|
|
11
|
-
* commas) are accepted in any config file regardless of extension.
|
|
12
|
-
*
|
|
13
|
-
* All paths inside the JSON are resolved relative to the JSON file's
|
|
14
|
-
* directory. Handlers must be ESM modules whose `default` export is
|
|
15
|
-
* a `Handler`.
|
|
16
|
-
*/
|
|
17
|
-
import { readFile, stat } from 'node:fs/promises';
|
|
18
|
-
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
19
|
-
import { pathToFileURL } from 'node:url';
|
|
20
|
-
import { buildMockModeHooks, } from './mock-mode.js';
|
|
21
|
-
import { _deepFreezeForDev } from './define.js';
|
|
22
|
-
import { stripJsonc } from './jsonc.js';
|
|
23
|
-
import { buildAutoSchemaRoutes, loadSchemaModule, normaliseSchemaConfig, } from './openapi/auto-schema.js';
|
|
24
|
-
import { parseOpenApi } from './openapi/loader.js';
|
|
25
|
-
import { buildOpenApiResponseGenerators } from './openapi/register.js';
|
|
26
|
-
import { matchPattern, parseRouteKey, parseRoutePattern } from './route-key.js';
|
|
27
|
-
const DEFAULT_PORT = 3000;
|
|
28
|
-
const DEFAULT_HOST = '127.0.0.1';
|
|
29
|
-
const resolvePath = (baseDir, p) => isAbsolute(p) ? p : resolve(baseDir, p);
|
|
30
|
-
/**
|
|
31
|
-
* Resolve `${VAR}` / `${VAR:default}` placeholders inside any string
|
|
32
|
-
* value of the JSON tree. Walks recursively; non-string nodes are
|
|
33
|
-
* returned as-is. Missing env var without default → empty string.
|
|
34
|
-
*/
|
|
35
|
-
const interpolateEnv = (value, env = process.env) => {
|
|
36
|
-
if (typeof value === 'string') {
|
|
37
|
-
// Accept the conventional `[A-Z0-9_]+` shell-style names AND
|
|
38
|
-
// lowercase / mixed-case names (e.g. `${path}`, `${myVar}`).
|
|
39
|
-
// The leading char must still be a letter or underscore so we
|
|
40
|
-
// don't accidentally interpolate `${1abc}` template fragments.
|
|
41
|
-
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::([^}]*))?\}/g, (_, name, dflt) => {
|
|
42
|
-
const v = env[name];
|
|
43
|
-
if (v !== undefined && v !== '')
|
|
44
|
-
return v;
|
|
45
|
-
return dflt ?? '';
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
if (Array.isArray(value))
|
|
49
|
-
return value.map((v) => interpolateEnv(v, env));
|
|
50
|
-
if (value !== null && typeof value === 'object') {
|
|
51
|
-
const out = {};
|
|
52
|
-
for (const [k, v] of Object.entries(value)) {
|
|
53
|
-
out[k] = interpolateEnv(v, env);
|
|
54
|
-
}
|
|
55
|
-
return out;
|
|
56
|
-
}
|
|
57
|
-
return value;
|
|
58
|
-
};
|
|
59
|
-
/**
|
|
60
|
-
* Coerce a port value that may have come from a `${VAR}` placeholder
|
|
61
|
-
* (i.e. is now a numeric string).
|
|
62
|
-
*/
|
|
63
|
-
const coercePort = (value, fallback) => {
|
|
64
|
-
if (typeof value === 'number' && Number.isFinite(value))
|
|
65
|
-
return value;
|
|
66
|
-
if (typeof value === 'string' && value.trim() !== '') {
|
|
67
|
-
const n = Number.parseInt(value, 10);
|
|
68
|
-
if (Number.isFinite(n))
|
|
69
|
-
return n;
|
|
70
|
-
}
|
|
71
|
-
return fallback;
|
|
72
|
-
};
|
|
73
|
-
const fileExists = async (path) => {
|
|
74
|
-
try {
|
|
75
|
-
const s = await stat(path);
|
|
76
|
-
return s.isFile();
|
|
77
|
-
}
|
|
78
|
-
catch (err) {
|
|
79
|
-
// Only swallow “does not exist”. Permission errors (EACCES, EPERM)
|
|
80
|
-
// are real misconfigurations — surface them instead of pretending
|
|
81
|
-
// the file is missing and falling through to a confusing
|
|
82
|
-
// ERR_MODULE_NOT_FOUND.
|
|
83
|
-
const code = err.code;
|
|
84
|
-
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
85
|
-
return false;
|
|
86
|
-
throw err;
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
/**
|
|
90
|
-
* Normalize a handler module path so override-map lookups tolerate
|
|
91
|
-
* trivial spelling differences between the JSONC value and the key
|
|
92
|
-
* the caller built (typically from `import.meta.glob`).
|
|
93
|
-
*
|
|
94
|
-
* Strips a single leading `./`, collapses repeated `/`, and drops a
|
|
95
|
-
* trailing `.js` / `.mjs` / `.cjs` / `.ts` / `.tsx` / `.mts` / `.cts`
|
|
96
|
-
* so `./src/routes/x.js`, `src/routes/x`, and `./src/routes/x.ts` all
|
|
97
|
-
* collapse to the same canonical form.
|
|
98
|
-
*/
|
|
99
|
-
const canonicalizeHandlerKey = (modulePath) => modulePath
|
|
100
|
-
.replace(/^\.\//, '')
|
|
101
|
-
.replace(/\/{2,}/g, '/')
|
|
102
|
-
.replace(/\.(?:js|mjs|cjs|ts|tsx|mts|cts)$/, '');
|
|
103
|
-
/**
|
|
104
|
-
* Resolve a handler module path to a URL that `import()` can load.
|
|
105
|
-
*
|
|
106
|
-
* Config files reference handlers by their compiled `.js` path
|
|
107
|
-
* (e.g. `./src/routes/health.js`) so they stay valid for production
|
|
108
|
-
* `tsc`-built deployments. In dev/test scenarios where only the
|
|
109
|
-
* TypeScript source exists, fall back to the same path with a `.ts`
|
|
110
|
-
* (or `.tsx`, `.mjs`, `.cjs`) extension so test runners that register
|
|
111
|
-
* a TS loader (vitest, tsx, ts-node) can still resolve it.
|
|
112
|
-
*
|
|
113
|
-
* Set `DATABEHAVE_PREFER_TS=1` to prefer `.ts` siblings over the
|
|
114
|
-
* declared `.js` even when both exist on disk (handy in dev when an
|
|
115
|
-
* outdated `dist/` is still around and would otherwise win).
|
|
116
|
-
*/
|
|
117
|
-
const resolveHandlerUrl = async (absolute) => {
|
|
118
|
-
const preferTs = process.env.DATABEHAVE_PREFER_TS === '1';
|
|
119
|
-
const m = absolute.match(/\.(js|mjs|cjs)$/);
|
|
120
|
-
if (preferTs && m) {
|
|
121
|
-
const stem = absolute.slice(0, -m[0].length);
|
|
122
|
-
for (const ext of ['.ts', '.tsx', '.mts', '.cts']) {
|
|
123
|
-
const candidate = `${stem}${ext}`;
|
|
124
|
-
if (await fileExists(candidate))
|
|
125
|
-
return pathToFileURL(candidate).href;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (await fileExists(absolute))
|
|
129
|
-
return pathToFileURL(absolute).href;
|
|
130
|
-
if (m) {
|
|
131
|
-
const stem = absolute.slice(0, -m[0].length);
|
|
132
|
-
for (const ext of ['.ts', '.tsx', '.mts', '.cts']) {
|
|
133
|
-
const candidate = `${stem}${ext}`;
|
|
134
|
-
if (await fileExists(candidate))
|
|
135
|
-
return pathToFileURL(candidate).href;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
// No fallback hit — return the original URL so `import()` raises the
|
|
139
|
-
// canonical ERR_MODULE_NOT_FOUND with the requested path.
|
|
140
|
-
return pathToFileURL(absolute).href;
|
|
141
|
-
};
|
|
142
|
-
const importHandler = async (baseDir, modulePath, routeKey, handlerOverrides) => {
|
|
143
|
-
// 1. Pre-resolved override map wins. The caller-supplied object is
|
|
144
|
-
// flattened into a `Map` keyed by `canonicalizeHandlerKey` once at
|
|
145
|
-
// load time (see `buildHandlerOverrideMap`), so lookup is O(1)
|
|
146
|
-
// regardless of the number of overrides.
|
|
147
|
-
if (handlerOverrides) {
|
|
148
|
-
const candidate = handlerOverrides.get(canonicalizeHandlerKey(modulePath));
|
|
149
|
-
if (candidate !== undefined) {
|
|
150
|
-
if (typeof candidate !== 'function') {
|
|
151
|
-
throw new Error(`@databehave/server/config: handlers override for ${JSON.stringify(modulePath)} (mapped to ${routeKey}) is not a function (got ${typeof candidate})`);
|
|
152
|
-
}
|
|
153
|
-
return candidate;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
// 2. Dynamic import from disk, with `.js` → `.ts/.tsx/.mts/.cts`
|
|
157
|
-
// sibling fallback for TS-loader environments (tsx, ts-node).
|
|
158
|
-
const absolute = resolvePath(baseDir, modulePath);
|
|
159
|
-
const url = await resolveHandlerUrl(absolute);
|
|
160
|
-
const mod = (await import(/* @vite-ignore */ url));
|
|
161
|
-
const handler = mod.default;
|
|
162
|
-
if (typeof handler !== 'function') {
|
|
163
|
-
throw new Error(`@databehave/server/config: ${modulePath} (mapped to ${routeKey}) has no default-exported handler function`);
|
|
164
|
-
}
|
|
165
|
-
return handler;
|
|
166
|
-
};
|
|
167
|
-
/**
|
|
168
|
-
* Convert an `EndpointResponse` (JSON-safe DTO) into a `MockResponse`
|
|
169
|
-
* (discriminated union: either body OR empty).
|
|
170
|
-
*/
|
|
171
|
-
const toResponse = (r) => {
|
|
172
|
-
if (r.empty) {
|
|
173
|
-
return {
|
|
174
|
-
status: r.status,
|
|
175
|
-
empty: true,
|
|
176
|
-
...(r.headers ? { headers: r.headers } : {}),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
return {
|
|
180
|
-
status: r.status,
|
|
181
|
-
json: r.json ?? null,
|
|
182
|
-
...(r.headers ? { headers: r.headers } : {}),
|
|
183
|
-
};
|
|
184
|
-
};
|
|
185
|
-
const synthesizeResponseHandler = (r) => {
|
|
186
|
-
const frozen = toResponse(r);
|
|
187
|
-
return () => frozen;
|
|
188
|
-
};
|
|
189
|
-
const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
190
|
-
const validateEndpointResponse = (v, routeKey) => {
|
|
191
|
-
if (!isPlainObject(v)) {
|
|
192
|
-
throw new Error(`@databehave/server/config: endpoints["${routeKey}"].response must be an object`);
|
|
193
|
-
}
|
|
194
|
-
if (typeof v.status !== 'number' || !Number.isFinite(v.status)) {
|
|
195
|
-
throw new Error(`@databehave/server/config: endpoints["${routeKey}"].response.status must be a number`);
|
|
196
|
-
}
|
|
197
|
-
const out = { status: v.status };
|
|
198
|
-
if (v.json !== undefined)
|
|
199
|
-
out.json = v.json;
|
|
200
|
-
if (v.empty !== undefined) {
|
|
201
|
-
if (typeof v.empty !== 'boolean') {
|
|
202
|
-
throw new Error(`@databehave/server/config: endpoints["${routeKey}"].response.empty must be a boolean`);
|
|
203
|
-
}
|
|
204
|
-
out.empty = v.empty;
|
|
205
|
-
}
|
|
206
|
-
if (v.headers !== undefined) {
|
|
207
|
-
if (!isPlainObject(v.headers)) {
|
|
208
|
-
throw new Error(`@databehave/server/config: endpoints["${routeKey}"].response.headers must be an object`);
|
|
209
|
-
}
|
|
210
|
-
const h = {};
|
|
211
|
-
for (const [k, val] of Object.entries(v.headers)) {
|
|
212
|
-
if (typeof val !== 'string') {
|
|
213
|
-
throw new Error(`@databehave/server/config: endpoints["${routeKey}"].response.headers["${k}"] must be a string`);
|
|
214
|
-
}
|
|
215
|
-
h[k] = val;
|
|
216
|
-
}
|
|
217
|
-
out.headers = h;
|
|
218
|
-
}
|
|
219
|
-
return out;
|
|
220
|
-
};
|
|
221
|
-
const validateEndpointSpec = (raw, routeKey) => {
|
|
222
|
-
if (typeof raw === 'string')
|
|
223
|
-
return raw;
|
|
224
|
-
if (!isPlainObject(raw)) {
|
|
225
|
-
throw new Error(`@databehave/server/config: endpoints["${routeKey}"] must be a string or object`);
|
|
226
|
-
}
|
|
227
|
-
const hasHandler = raw.handler !== undefined;
|
|
228
|
-
const hasResponse = raw.response !== undefined;
|
|
229
|
-
if (hasHandler && hasResponse) {
|
|
230
|
-
throw new Error(`@databehave/server/config: endpoints["${routeKey}"] cannot have both "handler" and "response"`);
|
|
231
|
-
}
|
|
232
|
-
if (!hasHandler && !hasResponse) {
|
|
233
|
-
throw new Error(`@databehave/server/config: endpoints["${routeKey}"] must have either "handler" or "response"`);
|
|
234
|
-
}
|
|
235
|
-
if (hasHandler) {
|
|
236
|
-
if (typeof raw.handler !== 'string') {
|
|
237
|
-
throw new Error(`@databehave/server/config: endpoints["${routeKey}"].handler must be a string`);
|
|
238
|
-
}
|
|
239
|
-
if (raw.status !== undefined) {
|
|
240
|
-
if (typeof raw.status !== 'number' || !Number.isFinite(raw.status)) {
|
|
241
|
-
throw new Error(`@databehave/server/config: endpoints["${routeKey}"].status must be a number`);
|
|
242
|
-
}
|
|
243
|
-
return { handler: raw.handler, status: raw.status };
|
|
244
|
-
}
|
|
245
|
-
return { handler: raw.handler };
|
|
246
|
-
}
|
|
247
|
-
// hasResponse
|
|
248
|
-
if (raw.status !== undefined) {
|
|
249
|
-
throw new Error(`@databehave/server/config: endpoints["${routeKey}"].status cannot be combined with "response" (put status inside response)`);
|
|
250
|
-
}
|
|
251
|
-
return { response: validateEndpointResponse(raw.response, routeKey) };
|
|
252
|
-
};
|
|
253
|
-
const validate = (raw) => {
|
|
254
|
-
if (raw === null || typeof raw !== 'object') {
|
|
255
|
-
throw new Error('@databehave/server/config: JSON root must be an object');
|
|
256
|
-
}
|
|
257
|
-
const obj = raw;
|
|
258
|
-
const out = {};
|
|
259
|
-
if (obj.openapi !== undefined) {
|
|
260
|
-
if (typeof obj.openapi !== 'string') {
|
|
261
|
-
throw new Error('@databehave/server/config: "openapi" must be a string path');
|
|
262
|
-
}
|
|
263
|
-
out.openapi = obj.openapi;
|
|
264
|
-
}
|
|
265
|
-
if (obj.server !== undefined) {
|
|
266
|
-
if (typeof obj.server !== 'object' || obj.server === null) {
|
|
267
|
-
throw new Error('@databehave/server/config: "server" must be an object');
|
|
268
|
-
}
|
|
269
|
-
out.server = obj.server;
|
|
270
|
-
}
|
|
271
|
-
if (obj.basePath !== undefined) {
|
|
272
|
-
if (typeof obj.basePath !== 'string') {
|
|
273
|
-
throw new Error('@databehave/server/config: "basePath" must be a string');
|
|
274
|
-
}
|
|
275
|
-
if (!obj.basePath.startsWith('/')) {
|
|
276
|
-
throw new Error('@databehave/server/config: "basePath" must start with "/"');
|
|
277
|
-
}
|
|
278
|
-
if (obj.basePath.length > 1 && obj.basePath.endsWith('/')) {
|
|
279
|
-
throw new Error('@databehave/server/config: "basePath" must not end with "/"');
|
|
280
|
-
}
|
|
281
|
-
out.basePath = obj.basePath;
|
|
282
|
-
}
|
|
283
|
-
if (obj.cors !== undefined) {
|
|
284
|
-
if (typeof obj.cors !== 'object' || obj.cors === null) {
|
|
285
|
-
throw new Error('@databehave/server/config: "cors" must be an object');
|
|
286
|
-
}
|
|
287
|
-
out.cors = obj.cors;
|
|
288
|
-
}
|
|
289
|
-
if (obj.mockMode !== undefined) {
|
|
290
|
-
if (typeof obj.mockMode !== 'object' || obj.mockMode === null) {
|
|
291
|
-
throw new Error('@databehave/server/config: "mockMode" must be an object');
|
|
292
|
-
}
|
|
293
|
-
const mm = obj.mockMode;
|
|
294
|
-
if (mm.pathOverrides !== undefined) {
|
|
295
|
-
if (typeof mm.pathOverrides !== 'object' ||
|
|
296
|
-
mm.pathOverrides === null ||
|
|
297
|
-
Array.isArray(mm.pathOverrides)) {
|
|
298
|
-
throw new Error('@databehave/server/config: "mockMode.pathOverrides" must be an object');
|
|
299
|
-
}
|
|
300
|
-
// Keys must be either `<METHOD> <absolute-path>` or `<absolute-path>`.
|
|
301
|
-
// Anything else silently never matches at runtime, which is the
|
|
302
|
-
// exact class of typo that this validation exists to catch.
|
|
303
|
-
const KEY_RE = /^(?:(?:GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+)?\/[^\s]*$/i;
|
|
304
|
-
// The mock-mode matcher uppercases the request method before
|
|
305
|
-
// lookup (see `mock-mode.ts`), so a lowercase config key
|
|
306
|
-
// (e.g. `"get /api/x"`) would never match. Normalize the method
|
|
307
|
-
// portion here so the validator stays case-insensitive while the
|
|
308
|
-
// stored map keys remain canonical.
|
|
309
|
-
const normalized = {};
|
|
310
|
-
for (const [k, v] of Object.entries(mm.pathOverrides)) {
|
|
311
|
-
if (!KEY_RE.test(k)) {
|
|
312
|
-
throw new Error(`@databehave/server/config: "mockMode.pathOverrides" key ${JSON.stringify(k)} must be "<METHOD> /path" or "/path" (path must start with "/")`);
|
|
313
|
-
}
|
|
314
|
-
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
315
|
-
throw new Error(`@databehave/server/config: "mockMode.pathOverrides[${JSON.stringify(k)}]" must be a number (HTTP status code)`);
|
|
316
|
-
}
|
|
317
|
-
const spaceIdx = k.indexOf(' ');
|
|
318
|
-
const canonical = spaceIdx > 0
|
|
319
|
-
? `${k.slice(0, spaceIdx).toUpperCase()}${k.slice(spaceIdx)}`
|
|
320
|
-
: k;
|
|
321
|
-
normalized[canonical] = v;
|
|
322
|
-
}
|
|
323
|
-
mm.pathOverrides = normalized;
|
|
324
|
-
}
|
|
325
|
-
out.mockMode = mm;
|
|
326
|
-
}
|
|
327
|
-
if (obj.endpoints !== undefined) {
|
|
328
|
-
if (typeof obj.endpoints !== 'object' || obj.endpoints === null) {
|
|
329
|
-
throw new Error('@databehave/server/config: "endpoints" must be an object');
|
|
330
|
-
}
|
|
331
|
-
const map = {};
|
|
332
|
-
for (const [k, v] of Object.entries(obj.endpoints)) {
|
|
333
|
-
map[k] = validateEndpointSpec(v, k);
|
|
334
|
-
}
|
|
335
|
-
out.endpoints = map;
|
|
336
|
-
}
|
|
337
|
-
if (obj.admin !== undefined) {
|
|
338
|
-
if (typeof obj.admin !== 'object' || obj.admin === null) {
|
|
339
|
-
throw new Error('@databehave/server/config: "admin" must be an object');
|
|
340
|
-
}
|
|
341
|
-
// Pass-through; createServer validates the shape.
|
|
342
|
-
out.admin = obj.admin;
|
|
343
|
-
}
|
|
344
|
-
if (obj.log !== undefined) {
|
|
345
|
-
const v = obj.log;
|
|
346
|
-
if (typeof v !== 'boolean' &&
|
|
347
|
-
(typeof v !== 'object' || v === null || Array.isArray(v))) {
|
|
348
|
-
throw new Error('@databehave/server/config: "log" must be a boolean or an object');
|
|
349
|
-
}
|
|
350
|
-
out.log = v;
|
|
351
|
-
}
|
|
352
|
-
if (obj.validation !== undefined) {
|
|
353
|
-
if (typeof obj.validation !== 'object' || obj.validation === null || Array.isArray(obj.validation)) {
|
|
354
|
-
throw new Error('@databehave/server/config: "validation" must be an object');
|
|
355
|
-
}
|
|
356
|
-
out.validation = obj.validation;
|
|
357
|
-
}
|
|
358
|
-
if (obj.schema !== undefined) {
|
|
359
|
-
// Re-validate via `normaliseSchemaConfig` at load time; here we
|
|
360
|
-
// just pass-through the raw shape so the loader can call the
|
|
361
|
-
// normaliser once and reuse the resolved object. Reject the
|
|
362
|
-
// removed boolean shorthand fail-fast at validation time so
|
|
363
|
-
// the migration error surfaces with the same wording the
|
|
364
|
-
// normaliser would emit.
|
|
365
|
-
if (typeof obj.schema === 'boolean') {
|
|
366
|
-
throw new Error('@databehave/server/config: "schema" must be an object like ' +
|
|
367
|
-
'{ "enabled": true }; the boolean shorthand was removed (got ' +
|
|
368
|
-
JSON.stringify(obj.schema) +
|
|
369
|
-
')');
|
|
370
|
-
}
|
|
371
|
-
out.schema = obj.schema;
|
|
372
|
-
}
|
|
373
|
-
return out;
|
|
374
|
-
};
|
|
375
|
-
/**
|
|
376
|
-
* Rewrite an endpoint key by prepending `basePath` to its path
|
|
377
|
-
* segment. Convention:
|
|
378
|
-
* - Keys whose path starts with `/` are absolute and left untouched
|
|
379
|
-
* (use this for cross-cutting endpoints like `/health`).
|
|
380
|
-
* - Keys whose path does NOT start with `/` are treated as
|
|
381
|
-
* relative to `basePath` and rewritten to `${basePath}/${path}`.
|
|
382
|
-
* When `basePath` is empty or `/`, every key is forced to absolute
|
|
383
|
-
* form by prepending a single `/` if missing.
|
|
384
|
-
*/
|
|
385
|
-
const applyBasePath = (key, basePath) => {
|
|
386
|
-
const sp = key.indexOf(' ');
|
|
387
|
-
if (sp <= 0)
|
|
388
|
-
return key;
|
|
389
|
-
const method = key.slice(0, sp);
|
|
390
|
-
const path = key.slice(sp + 1);
|
|
391
|
-
if (path.startsWith('/'))
|
|
392
|
-
return key;
|
|
393
|
-
if (basePath === '' || basePath === '/')
|
|
394
|
-
return `${method} /${path}`;
|
|
395
|
-
return `${method} ${basePath}/${path}`;
|
|
396
|
-
};
|
|
397
|
-
/**
|
|
398
|
-
* Load and fully resolve a `databehave.json`.
|
|
399
|
-
*
|
|
400
|
-
* Returns a ready-to-run `Config` plus the listen options the
|
|
401
|
-
* CLI should bind to. Errors are thrown synchronously after parsing
|
|
402
|
-
* to fail fast on bad configs.
|
|
403
|
-
*/
|
|
404
|
-
export const loadConfig = async (configPath, options) => {
|
|
405
|
-
const absoluteConfigPath = isAbsolute(configPath)
|
|
406
|
-
? configPath
|
|
407
|
-
: resolve(process.cwd(), configPath);
|
|
408
|
-
const baseDir = dirname(absoluteConfigPath);
|
|
409
|
-
const raw = JSON.parse(stripJsonc(await readFile(absoluteConfigPath, 'utf8')));
|
|
410
|
-
const json = validate(interpolateEnv(raw));
|
|
411
|
-
// 0. Canonicalise the handler-override map once. Lookup inside
|
|
412
|
-
// `importHandler` is then O(1) per route instead of scanning the
|
|
413
|
-
// entire map for every endpoint that doesn't hit a direct key.
|
|
414
|
-
const handlerOverrides = options?.handlers
|
|
415
|
-
? (() => {
|
|
416
|
-
const m = new Map();
|
|
417
|
-
for (const [key, fn] of Object.entries(options.handlers)) {
|
|
418
|
-
m.set(canonicalizeHandlerKey(key), fn);
|
|
419
|
-
}
|
|
420
|
-
return m;
|
|
421
|
-
})()
|
|
422
|
-
: undefined;
|
|
423
|
-
// 1. OpenAPI body (loaded from disk into a string). JSON only;
|
|
424
|
-
// convert YAML up-front with `yq -o=json`. The raw file bytes
|
|
425
|
-
// flow through unchanged so callers see the same source-mapped
|
|
426
|
-
// error positions a standalone tool would report.
|
|
427
|
-
let openapiBody;
|
|
428
|
-
if (json.openapi !== undefined) {
|
|
429
|
-
const specPath = resolvePath(baseDir, json.openapi);
|
|
430
|
-
openapiBody = await readFile(specPath, 'utf8');
|
|
431
|
-
}
|
|
432
|
-
// 2. endpoints → routes map + per-endpoint status overrides.
|
|
433
|
-
const routes = {};
|
|
434
|
-
const endpointStatusOverrides = {};
|
|
435
|
-
if (json.endpoints) {
|
|
436
|
-
const basePath = json.basePath ?? '';
|
|
437
|
-
const entries = Object.entries(json.endpoints);
|
|
438
|
-
// Detect collisions where two endpoint keys collapse to the same
|
|
439
|
-
// `METHOD path` after applying `basePath` — silent overwrite would
|
|
440
|
-
// mask a real configuration bug.
|
|
441
|
-
const seenKeys = new Map();
|
|
442
|
-
for (const [rawKey] of entries) {
|
|
443
|
-
const finalKey = applyBasePath(rawKey, basePath);
|
|
444
|
-
const prior = seenKeys.get(finalKey);
|
|
445
|
-
if (prior !== undefined && prior !== rawKey) {
|
|
446
|
-
throw new Error(`@databehave/server/config: endpoints keys "${prior}" and "${rawKey}" both resolve to "${finalKey}" after applying basePath`);
|
|
447
|
-
}
|
|
448
|
-
seenKeys.set(finalKey, rawKey);
|
|
449
|
-
}
|
|
450
|
-
// Resolve every endpoint concurrently. `allSettled` collects all
|
|
451
|
-
// failures so one bad path doesn't hide the rest — a misconfigured
|
|
452
|
-
// config surfaces every broken handler at once instead of forcing
|
|
453
|
-
// an iterative trial-and-error loop.
|
|
454
|
-
const settled = await Promise.allSettled(entries.map(async ([rawKey, spec]) => {
|
|
455
|
-
const key = applyBasePath(rawKey, basePath);
|
|
456
|
-
parseRouteKey(key); // validate after prefixing
|
|
457
|
-
if (typeof spec === 'string') {
|
|
458
|
-
routes[key] = await importHandler(baseDir, spec, key, handlerOverrides);
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
if (spec.handler !== undefined) {
|
|
462
|
-
routes[key] = await importHandler(baseDir, spec.handler, key, handlerOverrides);
|
|
463
|
-
if (spec.status !== undefined) {
|
|
464
|
-
endpointStatusOverrides[key] = spec.status;
|
|
465
|
-
}
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
routes[key] = synthesizeResponseHandler(spec.response);
|
|
469
|
-
}));
|
|
470
|
-
const failures = settled.flatMap((r, i) => r.status === 'rejected'
|
|
471
|
-
? [{ reason: r.reason, rawKey: entries[i][0] }]
|
|
472
|
-
: []);
|
|
473
|
-
if (failures.length > 0) {
|
|
474
|
-
const errors = failures.map((f) => f.reason instanceof Error ? f.reason : new Error(String(f.reason)));
|
|
475
|
-
const messages = failures.map((f, i) => `${f.rawKey}: ${errors[i].message}`);
|
|
476
|
-
// Node ≥ 18.17 always has AggregateError.
|
|
477
|
-
throw new AggregateError(errors, `@databehave/server/config: ${failures.length} endpoint(s) failed to load:\n - ${messages.join('\n - ')}`);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
const warn = options?.logger?.warn ?? ((m) => {
|
|
481
|
-
// eslint-disable-next-line no-console
|
|
482
|
-
console.warn(m);
|
|
483
|
-
});
|
|
484
|
-
// 3. Built-in mock-mode hooks (with OAS-backed body resolver when
|
|
485
|
-
// an OpenAPI document is present). Per-endpoint `status`
|
|
486
|
-
// overrides are merged on top of any user-supplied
|
|
487
|
-
// `mockMode.pathOverrides` (user values take precedence to keep
|
|
488
|
-
// explicit JSON authoritative).
|
|
489
|
-
let hooks;
|
|
490
|
-
if (json.mockMode) {
|
|
491
|
-
const resolvedLogger = options?.logger ?? json.mockMode.logger;
|
|
492
|
-
const mergedConfig = {
|
|
493
|
-
...json.mockMode,
|
|
494
|
-
pathOverrides: {
|
|
495
|
-
...endpointStatusOverrides,
|
|
496
|
-
...(json.mockMode.pathOverrides ?? {}),
|
|
497
|
-
},
|
|
498
|
-
// Thread the caller-supplied logger so mock-mode warnings
|
|
499
|
-
// (e.g. out-of-range x-mock-status header overrides) flow
|
|
500
|
-
// through the same channel as JSON/OAS warnings instead of
|
|
501
|
-
// leaking to stdout. Only set the key when defined so
|
|
502
|
-
// `exactOptionalPropertyTypes` is honoured.
|
|
503
|
-
...(resolvedLogger ? { logger: resolvedLogger } : {}),
|
|
504
|
-
};
|
|
505
|
-
let bodyResolver;
|
|
506
|
-
if (openapiBody !== undefined) {
|
|
507
|
-
try {
|
|
508
|
-
const doc = parseOpenApi(openapiBody);
|
|
509
|
-
const generators = buildOpenApiResponseGenerators(doc, {
|
|
510
|
-
onWalkError: (method, path, err) => {
|
|
511
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
512
|
-
warn(`[@databehave/server] mock-mode body resolver: openapi walk failed for ${method.toUpperCase()} ${path}: ${msg}`);
|
|
513
|
-
},
|
|
514
|
-
});
|
|
515
|
-
// Build a single pattern index, sorted so the dispatch
|
|
516
|
-
// result does not depend on OAS declaration order:
|
|
517
|
-
// 1. Static patterns first (exact paths beat parametric).
|
|
518
|
-
// 2. Within the same kind, longer paths first (more
|
|
519
|
-
// specific routes shadow shorter wildcards — e.g.
|
|
520
|
-
// `/users/me` is preferred over `/users/:id`).
|
|
521
|
-
// 3. Stable fallback on the raw path string for ties.
|
|
522
|
-
// Without this sort, two OAS paths that could both match a
|
|
523
|
-
// request resolve in `paths` insertion order — i.e. the
|
|
524
|
-
// schema author's typing order silently picks the winner.
|
|
525
|
-
const patternIndex = [];
|
|
526
|
-
for (const [key, perStatus] of generators) {
|
|
527
|
-
const { method, path } = parseRouteKey(key);
|
|
528
|
-
patternIndex.push({
|
|
529
|
-
method: method.toUpperCase(),
|
|
530
|
-
pattern: parseRoutePattern(path),
|
|
531
|
-
perStatus,
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
patternIndex.sort((a, b) => {
|
|
535
|
-
if (a.pattern.isStatic !== b.pattern.isStatic) {
|
|
536
|
-
return a.pattern.isStatic ? -1 : 1;
|
|
537
|
-
}
|
|
538
|
-
if (a.pattern.segments.length !== b.pattern.segments.length) {
|
|
539
|
-
return b.pattern.segments.length - a.pattern.segments.length;
|
|
540
|
-
}
|
|
541
|
-
return a.pattern.path.localeCompare(b.pattern.path);
|
|
542
|
-
});
|
|
543
|
-
bodyResolver = (method, path, status) => {
|
|
544
|
-
const upper = method.toUpperCase();
|
|
545
|
-
for (const entry of patternIndex) {
|
|
546
|
-
if (entry.method !== upper)
|
|
547
|
-
continue;
|
|
548
|
-
if (entry.pattern.isStatic) {
|
|
549
|
-
if (entry.pattern.path !== path)
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
else if (matchPattern(entry.pattern, path) === null) {
|
|
553
|
-
continue;
|
|
554
|
-
}
|
|
555
|
-
const gen = entry.perStatus.get(status);
|
|
556
|
-
if (gen !== undefined)
|
|
557
|
-
return gen();
|
|
558
|
-
}
|
|
559
|
-
return undefined;
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
catch {
|
|
563
|
-
// If OAS parsing fails, mock-mode still works with fallback envelope.
|
|
564
|
-
bodyResolver = undefined;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
hooks = buildMockModeHooks(mergedConfig, bodyResolver);
|
|
568
|
-
}
|
|
569
|
-
// 4. Build a `cors` value with a sensible default `origin` so the
|
|
570
|
-
// JSON config never has to declare a function. When `cors.origin`
|
|
571
|
-
// is declared in JSON it may be either a single allowed origin
|
|
572
|
-
// string or an allowlist of strings — anything not on the
|
|
573
|
-
// allowlist returns an empty value so the runtime omits the
|
|
574
|
-
// `Access-Control-Allow-Origin` header entirely (the de-facto
|
|
575
|
-
// “reject” signal). Allowlist comparison is case-insensitive on
|
|
576
|
-
// the scheme/host portion so trivial typos don't silently lock
|
|
577
|
-
// callers out.
|
|
578
|
-
let cors;
|
|
579
|
-
if (json.cors) {
|
|
580
|
-
const rawOrigin = json.cors.origin;
|
|
581
|
-
const allowlist = typeof rawOrigin === 'string'
|
|
582
|
-
? [rawOrigin]
|
|
583
|
-
: Array.isArray(rawOrigin) && rawOrigin.every((v) => typeof v === 'string')
|
|
584
|
-
? rawOrigin
|
|
585
|
-
: undefined;
|
|
586
|
-
const normalisedAllowlist = allowlist?.map((o) => o.toLowerCase());
|
|
587
|
-
const { origin: _origin, ...rest } = json.cors;
|
|
588
|
-
cors = {
|
|
589
|
-
...rest,
|
|
590
|
-
origin: (o) => {
|
|
591
|
-
if (normalisedAllowlist) {
|
|
592
|
-
// With a declared allowlist: only echo back exact matches.
|
|
593
|
-
// No request Origin and `credentials: true` is the canonical
|
|
594
|
-
// "don't send ACAO" case per CORS §3.2.1 — returning empty
|
|
595
|
-
// lets the response builder omit the header entirely instead
|
|
596
|
-
// of echoing some arbitrary allowlist entry.
|
|
597
|
-
if (o && normalisedAllowlist.includes(o.toLowerCase()))
|
|
598
|
-
return o;
|
|
599
|
-
return '';
|
|
600
|
-
}
|
|
601
|
-
return o ? o : '*';
|
|
602
|
-
},
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
// admin: pass through; the CLI loader supplements openapiBody
|
|
606
|
-
// from the resolved OpenAPI document so the UI route picker is
|
|
607
|
-
// populated without the consumer having to wire it manually.
|
|
608
|
-
let admin;
|
|
609
|
-
if (json.admin !== undefined) {
|
|
610
|
-
admin = {
|
|
611
|
-
...json.admin,
|
|
612
|
-
...(openapiBody !== undefined && json.admin.openapiBody === undefined
|
|
613
|
-
? { openapiBody }
|
|
614
|
-
: {}),
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
// 5. Auto-schema dispatch (Phase 2C / brief #11). When the JSONC
|
|
618
|
-
// config sets a top-level `schema:` field, dynamic-import
|
|
619
|
-
// `@databehave/schema` (suppressing the native "Cannot find
|
|
620
|
-
// module" with the friendly install hint), pre-translate every
|
|
621
|
-
// OAS path/method whose 200 response carries a JSON schema, and
|
|
622
|
-
// inject synthesized handlers into `routes`. Hand-written
|
|
623
|
-
// `endpoints` always win (auto-mode skips routes already in
|
|
624
|
-
// `routes`). When `schema:` is absent everything below this
|
|
625
|
-
// block is a no-op and the OAS-fallback walker in `server.ts`
|
|
626
|
-
// keeps using `generateFromOasSchema` (the zero-dep
|
|
627
|
-
// placeholder generator) — byte-identical legacy behaviour.
|
|
628
|
-
// 5. Auto-schema dispatch (Phase 2C / brief #11). When the JSONC
|
|
629
|
-
// config sets `"schema": { "enabled": true }`, dynamic-import
|
|
630
|
-
// `@databehave/schema` (suppressing the native "Cannot find
|
|
631
|
-
// module" with the friendly install hint), pre-translate every
|
|
632
|
-
// OAS path/method whose 200 response carries a JSON schema, and
|
|
633
|
-
// inject synthesized handlers into `routes`. Hand-written
|
|
634
|
-
// `endpoints` always win (auto-mode skips routes already in
|
|
635
|
-
// `routes`). When `schema.enabled` is `false` (or the field is
|
|
636
|
-
// absent) everything below this block is a no-op and the
|
|
637
|
-
// OAS-fallback walker in `server.ts` keeps using
|
|
638
|
-
// `generateFromOasSchema` (the zero-dep placeholder generator)
|
|
639
|
-
// — byte-identical legacy behaviour.
|
|
640
|
-
if (json.schema !== undefined) {
|
|
641
|
-
const schemaCfg = normaliseSchemaConfig(json.schema);
|
|
642
|
-
if (schemaCfg !== null) {
|
|
643
|
-
// Cache the imported module on the loaded-config closure for
|
|
644
|
-
// later use; throws fail-fast (with the friendly install-hint
|
|
645
|
-
// template) if the optional peer is not installed. Native
|
|
646
|
-
// "Cannot find module" stack traces never reach the consumer.
|
|
647
|
-
const schemaModule = await loadSchemaModule();
|
|
648
|
-
if (openapiBody !== undefined) {
|
|
649
|
-
const doc = parseOpenApi(openapiBody);
|
|
650
|
-
const declaredKeys = new Set(Object.keys(routes).filter((k) => routes[k] !== undefined));
|
|
651
|
-
const autoRoutes = await buildAutoSchemaRoutes(doc, schemaCfg, schemaModule, declaredKeys);
|
|
652
|
-
for (const [key, handler] of autoRoutes) {
|
|
653
|
-
routes[key] = handler;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
const config = {
|
|
659
|
-
routes,
|
|
660
|
-
...(openapiBody !== undefined ? { openapi: openapiBody } : {}),
|
|
661
|
-
...(hooks !== undefined ? { hooks } : {}),
|
|
662
|
-
...(cors !== undefined ? { cors } : {}),
|
|
663
|
-
...(admin !== undefined ? { admin } : {}),
|
|
664
|
-
...(json.log !== undefined ? { log: json.log } : {}),
|
|
665
|
-
...(json.validation !== undefined ? { validation: json.validation } : {}),
|
|
666
|
-
onOpenApiWalkError: (method, path, err) => {
|
|
667
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
668
|
-
warn(`[@databehave/server] openapi walk failed for ${method.toUpperCase()} ${path}: ${msg}`);
|
|
669
|
-
},
|
|
670
|
-
onOpenApiEmptySchema: (method, path, status) => {
|
|
671
|
-
warn(`[@databehave/server] OAS response schema is empty for ${method.toUpperCase()} ${path} (status ${status}) — serving stub. Consider filling the schema in your OpenAPI document.`);
|
|
672
|
-
},
|
|
673
|
-
};
|
|
674
|
-
const server = {
|
|
675
|
-
host: json.server?.host ?? DEFAULT_HOST,
|
|
676
|
-
port: coercePort(json.server?.port, DEFAULT_PORT),
|
|
677
|
-
};
|
|
678
|
-
// Deep-freeze in dev so the resolved config behaves identically to
|
|
679
|
-
// one returned by `defineConfig()` — accidental late mutation
|
|
680
|
-
// throws loudly instead of corrupting the running server.
|
|
681
|
-
_deepFreezeForDev(config);
|
|
682
|
-
return { config, server };
|
|
683
|
-
};
|
|
684
|
-
//# sourceMappingURL=json-config.js.map
|