@databehave/server 0.1.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 +28 -0
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/admin/admin-routes.d.ts +42 -0
- package/dist/admin/admin-routes.d.ts.map +1 -0
- package/dist/admin/admin-routes.js +472 -0
- package/dist/admin/admin-routes.js.map +1 -0
- package/dist/admin/admin-types.d.ts +170 -0
- package/dist/admin/admin-types.d.ts.map +1 -0
- package/dist/admin/admin-types.js +30 -0
- package/dist/admin/admin-types.js.map +1 -0
- package/dist/admin/banner.d.ts +11 -0
- package/dist/admin/banner.d.ts.map +1 -0
- package/dist/admin/banner.js +5 -0
- package/dist/admin/banner.js.map +1 -0
- package/dist/admin/header-parser.d.ts +14 -0
- package/dist/admin/header-parser.d.ts.map +1 -0
- package/dist/admin/header-parser.js +171 -0
- package/dist/admin/header-parser.js.map +1 -0
- package/dist/admin/inject.d.ts +48 -0
- package/dist/admin/inject.d.ts.map +1 -0
- package/dist/admin/inject.js +167 -0
- package/dist/admin/inject.js.map +1 -0
- package/dist/admin/overrides-store.d.ts +18 -0
- package/dist/admin/overrides-store.d.ts.map +1 -0
- package/dist/admin/overrides-store.js +100 -0
- package/dist/admin/overrides-store.js.map +1 -0
- package/dist/admin/resolve-cors.d.ts +22 -0
- package/dist/admin/resolve-cors.d.ts.map +1 -0
- package/dist/admin/resolve-cors.js +28 -0
- package/dist/admin/resolve-cors.js.map +1 -0
- package/dist/admin/scenarios-store.d.ts +15 -0
- package/dist/admin/scenarios-store.d.ts.map +1 -0
- package/dist/admin/scenarios-store.js +109 -0
- package/dist/admin/scenarios-store.js.map +1 -0
- package/dist/admin/ui.css +2 -0
- package/dist/admin/ui.html +13 -0
- package/dist/admin/ui.js +240 -0
- package/dist/bin-helpers.d.ts +46 -0
- package/dist/bin-helpers.d.ts.map +1 -0
- package/dist/bin-helpers.js +61 -0
- package/dist/bin-helpers.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +57 -0
- package/dist/bin.js.map +1 -0
- package/dist/cors.d.ts +57 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/cors.js +167 -0
- package/dist/cors.js.map +1 -0
- package/dist/define.d.ts +26 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +55 -0
- package/dist/define.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/json-config.d.ts +170 -0
- package/dist/json-config.d.ts.map +1 -0
- package/dist/json-config.js +684 -0
- package/dist/json-config.js.map +1 -0
- package/dist/jsonc.d.ts +24 -0
- package/dist/jsonc.d.ts.map +1 -0
- package/dist/jsonc.js +125 -0
- package/dist/jsonc.js.map +1 -0
- package/dist/log.d.ts +57 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +103 -0
- package/dist/log.js.map +1 -0
- package/dist/middleware/request-validation.d.ts +58 -0
- package/dist/middleware/request-validation.d.ts.map +1 -0
- package/dist/middleware/request-validation.js +328 -0
- package/dist/middleware/request-validation.js.map +1 -0
- package/dist/mock-mode.d.ts +95 -0
- package/dist/mock-mode.d.ts.map +1 -0
- package/dist/mock-mode.js +95 -0
- package/dist/mock-mode.js.map +1 -0
- package/dist/openapi/auto-schema.d.ts +83 -0
- package/dist/openapi/auto-schema.d.ts.map +1 -0
- package/dist/openapi/auto-schema.js +232 -0
- package/dist/openapi/auto-schema.js.map +1 -0
- package/dist/openapi/generate.d.ts +20 -0
- package/dist/openapi/generate.d.ts.map +1 -0
- package/dist/openapi/generate.js +121 -0
- package/dist/openapi/generate.js.map +1 -0
- package/dist/openapi/loader.d.ts +18 -0
- package/dist/openapi/loader.d.ts.map +1 -0
- package/dist/openapi/loader.js +25 -0
- package/dist/openapi/loader.js.map +1 -0
- package/dist/openapi/register.d.ts +58 -0
- package/dist/openapi/register.d.ts.map +1 -0
- package/dist/openapi/register.js +166 -0
- package/dist/openapi/register.js.map +1 -0
- package/dist/openapi/seed.d.ts +16 -0
- package/dist/openapi/seed.d.ts.map +1 -0
- package/dist/openapi/seed.js +58 -0
- package/dist/openapi/seed.js.map +1 -0
- package/dist/openapi/translate.d.ts +55 -0
- package/dist/openapi/translate.d.ts.map +1 -0
- package/dist/openapi/translate.js +319 -0
- package/dist/openapi/translate.js.map +1 -0
- package/dist/openapi/walker.d.ts +44 -0
- package/dist/openapi/walker.d.ts.map +1 -0
- package/dist/openapi/walker.js +15 -0
- package/dist/openapi/walker.js.map +1 -0
- package/dist/request.d.ts +19 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +66 -0
- package/dist/request.js.map +1 -0
- package/dist/response.d.ts +13 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +43 -0
- package/dist/response.js.map +1 -0
- package/dist/route-key.d.ts +60 -0
- package/dist/route-key.d.ts.map +1 -0
- package/dist/route-key.js +105 -0
- package/dist/route-key.js.map +1 -0
- package/dist/run.d.ts +27 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +87 -0
- package/dist/run.js.map +1 -0
- package/dist/server.d.ts +60 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +497 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +274 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/validation/validate.d.ts +38 -0
- package/dist/validation/validate.d.ts.map +1 -0
- package/dist/validation/validate.js +372 -0
- package/dist/validation/validate.js.map +1 -0
- package/docs/admin/error-mode.md +14 -0
- package/docs/admin/headers.md +63 -0
- package/docs/admin/index.md +111 -0
- package/docs/admin/overrides.md +23 -0
- package/docs/admin/rest-api.md +90 -0
- package/docs/admin/scenarios.md +28 -0
- package/docs/cli.md +76 -0
- package/docs/config/admin.md +62 -0
- package/docs/config/cors.md +72 -0
- package/docs/config/endpoints.md +162 -0
- package/docs/config/index.md +107 -0
- package/docs/config/log.md +43 -0
- package/docs/config/mock-mode.md +84 -0
- package/docs/config/openapi.md +8 -0
- package/docs/config/schema.md +34 -0
- package/docs/config/server.md +32 -0
- package/docs/config/validation.md +161 -0
- package/docs/design.md +517 -0
- package/docs/errors.md +192 -0
- package/docs/getting-started.md +146 -0
- package/docs/index.md +23 -0
- package/docs/openapi/auto-schema.md +146 -0
- package/docs/openapi/fallback.md +242 -0
- package/docs/openapi/keywords.md +78 -0
- package/docs/recipes.md +258 -0
- package/docs/stability.md +118 -0
- package/docs/troubleshooting.md +191 -0
- package/package.json +80 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { generateFromOasSchema } from './generate.js';
|
|
2
|
+
const SUPPORTED_METHODS = new Set([
|
|
3
|
+
'get',
|
|
4
|
+
'post',
|
|
5
|
+
'put',
|
|
6
|
+
'delete',
|
|
7
|
+
'patch',
|
|
8
|
+
]);
|
|
9
|
+
/**
|
|
10
|
+
* Convert OAS path syntax (`/users/{id}`) into @databehave/server pattern syntax
|
|
11
|
+
* (`/users/:id`). Other `{x}` placeholders not isolated to a single
|
|
12
|
+
* segment are left as-is — the matcher will then reject them at
|
|
13
|
+
* registration time.
|
|
14
|
+
*/
|
|
15
|
+
const oasPathToInternal = (oasPath) => oasPath
|
|
16
|
+
.split('/')
|
|
17
|
+
.map((seg) => (seg.startsWith('{') && seg.endsWith('}') ? `:${seg.slice(1, -1)}` : seg))
|
|
18
|
+
.join('/');
|
|
19
|
+
const stubBody = (method) => Object.freeze(method === 'get' ? {} : { success: true, message: null });
|
|
20
|
+
const buildHandler = (body) => {
|
|
21
|
+
// The OAS-only generator is structurally deterministic — same
|
|
22
|
+
// schema → same value — so the body is materialised once at boot
|
|
23
|
+
// and re-served per request.
|
|
24
|
+
return () => ({ json: body });
|
|
25
|
+
};
|
|
26
|
+
const buildStubHandler = (_routePath, method) => {
|
|
27
|
+
const body = stubBody(method);
|
|
28
|
+
return () => ({ json: body });
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* `true` when an OAS schema node is empty / permissive (`{}`) —
|
|
32
|
+
* i.e. carries no shape information (no `$ref`, `type`, `enum`,
|
|
33
|
+
* `const`, `properties`, `items`, `allOf`, `oneOf`, `anyOf`).
|
|
34
|
+
* Per JSON Schema this matches any JSON value.
|
|
35
|
+
*/
|
|
36
|
+
const isEmptyOasSchema = (node) => !node.$ref &&
|
|
37
|
+
!node.type &&
|
|
38
|
+
!node.enum &&
|
|
39
|
+
node.const === undefined &&
|
|
40
|
+
!node.properties &&
|
|
41
|
+
!node.items &&
|
|
42
|
+
!node.allOf &&
|
|
43
|
+
!node.oneOf &&
|
|
44
|
+
!node.anyOf;
|
|
45
|
+
/**
|
|
46
|
+
* Build a per-route per-status JSON body generator map from the OAS
|
|
47
|
+
* `responses` table. Used by mock-mode to synthesize an error body
|
|
48
|
+
* from the schema declared for that status code.
|
|
49
|
+
*
|
|
50
|
+
* Each generator is deterministic: same `(routeKey, status)` always
|
|
51
|
+
* yields the same body. Routes with `:param` segments are matched
|
|
52
|
+
* by exact path key only — runtime path-param substitution is the
|
|
53
|
+
* caller's responsibility.
|
|
54
|
+
*
|
|
55
|
+
* `onWalkError` mirrors `buildOpenApiRoutes` so spec gaps in error
|
|
56
|
+
* responses surface the same way as gaps in success responses.
|
|
57
|
+
*/
|
|
58
|
+
export const buildOpenApiResponseGenerators = (doc, options = {}) => {
|
|
59
|
+
const out = new Map();
|
|
60
|
+
const paths = doc.paths;
|
|
61
|
+
if (!paths)
|
|
62
|
+
return out;
|
|
63
|
+
for (const [oasPath, methods] of Object.entries(paths)) {
|
|
64
|
+
if (oasPath === '/')
|
|
65
|
+
continue;
|
|
66
|
+
const kitPath = oasPathToInternal(oasPath);
|
|
67
|
+
for (const [methodName, op] of Object.entries(methods)) {
|
|
68
|
+
const method = methodName.toLowerCase();
|
|
69
|
+
if (!SUPPORTED_METHODS.has(method))
|
|
70
|
+
continue;
|
|
71
|
+
const key = `${method.toUpperCase()} ${kitPath}`;
|
|
72
|
+
const responses = op.responses;
|
|
73
|
+
if (!responses)
|
|
74
|
+
continue;
|
|
75
|
+
const perStatus = new Map();
|
|
76
|
+
for (const [statusStr, resp] of Object.entries(responses)) {
|
|
77
|
+
const status = Number.parseInt(statusStr, 10);
|
|
78
|
+
if (!Number.isFinite(status))
|
|
79
|
+
continue;
|
|
80
|
+
const schemaNode = resp?.content?.['application/json']?.schema;
|
|
81
|
+
if (!schemaNode || typeof schemaNode !== 'object')
|
|
82
|
+
continue;
|
|
83
|
+
if (isEmptyOasSchema(schemaNode)) {
|
|
84
|
+
// Permissive — let mock-mode fall back to its envelope.
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
// Pre-generate at boot — the OAS-only generator is
|
|
89
|
+
// structurally deterministic so the body is constant per
|
|
90
|
+
// (route, status).
|
|
91
|
+
const body = generateFromOasSchema(schemaNode, doc);
|
|
92
|
+
perStatus.set(status, () => body);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
// Caller's mock-mode fallback envelope will be used; surface
|
|
96
|
+
// the spec gap so it can be fixed.
|
|
97
|
+
options.onWalkError?.(method, kitPath, err);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (perStatus.size > 0)
|
|
101
|
+
out.set(key, perStatus);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Produce a `Map<RouteKey, Handler>` covering every OAS
|
|
108
|
+
* `paths.<p>.<m>` not in `skip`. Failing schemas fall back to the
|
|
109
|
+
* stub body and report via `onWalkError` (default: ignore).
|
|
110
|
+
*/
|
|
111
|
+
export const buildOpenApiRoutes = (doc, options = {}) => {
|
|
112
|
+
const out = new Map();
|
|
113
|
+
const skip = options.skip ?? new Set();
|
|
114
|
+
const paths = doc.paths;
|
|
115
|
+
if (!paths)
|
|
116
|
+
return out;
|
|
117
|
+
for (const [oasPath, methods] of Object.entries(paths)) {
|
|
118
|
+
if (oasPath === '/')
|
|
119
|
+
continue;
|
|
120
|
+
const kitPath = oasPathToInternal(oasPath);
|
|
121
|
+
for (const [methodName, op] of Object.entries(methods)) {
|
|
122
|
+
const method = methodName.toLowerCase();
|
|
123
|
+
if (!SUPPORTED_METHODS.has(method))
|
|
124
|
+
continue;
|
|
125
|
+
const key = `${method.toUpperCase()} ${kitPath}`;
|
|
126
|
+
if (skip.has(key))
|
|
127
|
+
continue;
|
|
128
|
+
// Prefer 200, then the smallest declared 2xx response \u2014 OAS
|
|
129
|
+
// documents for mutating endpoints often only declare 201 / 202
|
|
130
|
+
// / 204, so falling back to those keeps the synthesised handler
|
|
131
|
+
// honest. 204 explicitly means \"no body\" and is handled below.
|
|
132
|
+
const responses = op.responses ?? {};
|
|
133
|
+
const twoXx = Object.keys(responses)
|
|
134
|
+
.map((s) => Number.parseInt(s, 10))
|
|
135
|
+
.filter((n) => Number.isFinite(n) && n >= 200 && n < 300)
|
|
136
|
+
.sort((a, b) => (a === 200 ? -1 : b === 200 ? 1 : a - b));
|
|
137
|
+
const pickedStatus = twoXx[0];
|
|
138
|
+
const pickedKey = pickedStatus !== undefined ? String(pickedStatus) : '200';
|
|
139
|
+
const responseSchema = responses[pickedKey]?.content?.['application/json']?.schema;
|
|
140
|
+
try {
|
|
141
|
+
if (pickedStatus === 204) {
|
|
142
|
+
out.set(key, buildStubHandler(kitPath, method));
|
|
143
|
+
}
|
|
144
|
+
else if (responseSchema && typeof responseSchema === 'object') {
|
|
145
|
+
if (isEmptyOasSchema(responseSchema)) {
|
|
146
|
+
options.onEmptySchema?.(method, kitPath, pickedStatus ?? 200);
|
|
147
|
+
out.set(key, buildStubHandler(kitPath, method));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const body = generateFromOasSchema(responseSchema, doc);
|
|
151
|
+
out.set(key, buildHandler(body));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
out.set(key, buildStubHandler(kitPath, method));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
options.onWalkError?.(method, kitPath, err);
|
|
160
|
+
out.set(key, buildStubHandler(kitPath, method));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/openapi/register.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper for building deterministic databehave seed strings.
|
|
3
|
+
*
|
|
4
|
+
* Seed format: `<endpoint>|<sortedKey>=<val>|...|date=<YYYY-MM-DD>|day=<n>`.
|
|
5
|
+
* Same input → same string → databehave yields byte-identical JSON. Re-exported
|
|
6
|
+
* from `@databehave/server` so consumers can derive their own seeds without
|
|
7
|
+
* reimplementing the format.
|
|
8
|
+
*/
|
|
9
|
+
export type SeedInput = {
|
|
10
|
+
endpoint: string;
|
|
11
|
+
from?: string;
|
|
12
|
+
dayOffset?: number;
|
|
13
|
+
extra?: Record<string, unknown>;
|
|
14
|
+
};
|
|
15
|
+
export declare const seedFor: (i: SeedInput) => string;
|
|
16
|
+
//# sourceMappingURL=seed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["../../src/openapi/seed.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC,CAAA;AAmCD,eAAO,MAAM,OAAO,GAAI,GAAG,SAAS,KAAG,MAWtC,CAAA"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper for building deterministic databehave seed strings.
|
|
3
|
+
*
|
|
4
|
+
* Seed format: `<endpoint>|<sortedKey>=<val>|...|date=<YYYY-MM-DD>|day=<n>`.
|
|
5
|
+
* Same input → same string → databehave yields byte-identical JSON. Re-exported
|
|
6
|
+
* from `@databehave/server` so consumers can derive their own seeds without
|
|
7
|
+
* reimplementing the format.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Recursively rebuild `v` with object keys sorted so two structurally
|
|
11
|
+
* identical objects with different insertion orders serialise to the
|
|
12
|
+
* same string. Arrays preserve order (semantic), plain objects are
|
|
13
|
+
* key-sorted, primitives are returned as-is.
|
|
14
|
+
*/
|
|
15
|
+
const sortKeys = (v) => {
|
|
16
|
+
if (v === null || typeof v !== 'object')
|
|
17
|
+
return v;
|
|
18
|
+
if (Array.isArray(v))
|
|
19
|
+
return v.map(sortKeys);
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const k of Object.keys(v).sort()) {
|
|
22
|
+
out[k] = sortKeys(v[k]);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
};
|
|
26
|
+
const stringifyExtra = (v) => {
|
|
27
|
+
if (v === null || v === undefined)
|
|
28
|
+
return String(v);
|
|
29
|
+
const t = typeof v;
|
|
30
|
+
if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint') {
|
|
31
|
+
return String(v);
|
|
32
|
+
}
|
|
33
|
+
// Objects / arrays — use JSON so distinct shapes produce distinct
|
|
34
|
+
// seeds. Keys are sorted recursively so insertion-order doesn't
|
|
35
|
+
// perturb the seed. Falls back to `String()` if stringify fails
|
|
36
|
+
// (cycles).
|
|
37
|
+
try {
|
|
38
|
+
return JSON.stringify(sortKeys(v));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return String(v);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
export const seedFor = (i) => {
|
|
45
|
+
const parts = [i.endpoint];
|
|
46
|
+
if (i.extra) {
|
|
47
|
+
const keys = Object.keys(i.extra).sort();
|
|
48
|
+
if (keys.length > 0) {
|
|
49
|
+
parts.push(keys.map((k) => `${k}=${stringifyExtra(i.extra[k])}`).join('|'));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (i.from !== undefined)
|
|
53
|
+
parts.push(`date=${i.from}`);
|
|
54
|
+
if (i.dayOffset !== undefined)
|
|
55
|
+
parts.push(`day=${i.dayOffset}`);
|
|
56
|
+
return parts.join('|');
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=seed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../../src/openapi/seed.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH;;;;;GAKG;AACH,MAAM,QAAQ,GAAG,CAAC,CAAU,EAAW,EAAE;IACvC,IAAI,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAA;IACjD,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAC5C,MAAM,GAAG,GAA4B,EAAE,CAAA;IACvC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAA4B,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QACjE,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAE,CAA6B,CAAC,CAAC,CAAC,CAAC,CAAA;IACtD,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC,CAAA;AAED,MAAM,cAAc,GAAG,CAAC,CAAU,EAAU,EAAE;IAC5C,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,CAAC,CAAC,CAAA;IACnD,MAAM,CAAC,GAAG,OAAO,CAAC,CAAA;IAClB,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC1E,OAAO,MAAM,CAAC,CAAC,CAAC,CAAA;IAClB,CAAC;IACD,kEAAkE;IAClE,gEAAgE;IAChE,gEAAgE;IAChE,YAAY;IACZ,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,CAAC,CAAC,CAAA;IAClB,CAAC;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAY,EAAU,EAAE;IAC9C,MAAM,KAAK,GAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAA;IACpC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;QACZ,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAA;QACxC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,cAAc,CAAC,CAAC,CAAC,KAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;QAC9E,CAAC;IACH,CAAC;IACD,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;IACtD,IAAI,CAAC,CAAC,SAAS,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAA;IAC/D,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACxB,CAAC,CAAA"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI-node → @databehave/schema IR translator.
|
|
3
|
+
*
|
|
4
|
+
* Phase 2C step 2C.a: pure translator + unit tests. NOT yet wired into
|
|
5
|
+
* the OAS dispatch — `register.ts` and `generate.ts` still drive the
|
|
6
|
+
* default OAS-only mock path; this module is staged for the upcoming
|
|
7
|
+
* auto-schema mode where, when the consumer opts in via the JSONC
|
|
8
|
+
* `schema:` field, the server translates each response schema to the
|
|
9
|
+
* databehave IR and hands it to `mock()` for richer fixtures.
|
|
10
|
+
*
|
|
11
|
+
* Genuine-separation invariant: this file MUST NOT statically value-
|
|
12
|
+
* import `@databehave/schema`. The runtime DSL constructors are only
|
|
13
|
+
* loaded via `await import('@databehave/schema')` inside the function
|
|
14
|
+
* body, so OAS-only consumers who never opt in do not pay for the
|
|
15
|
+
* schema package at all. Type-only imports (`import type`) are erased
|
|
16
|
+
* at compile time and are fine.
|
|
17
|
+
*
|
|
18
|
+
* Supported OAS keywords (mirror `validation/validate.ts`):
|
|
19
|
+
* type / enum / required / properties / items / nullable + ["X","null"] /
|
|
20
|
+
* minLength / maxLength / pattern / minimum / maximum /
|
|
21
|
+
* exclusiveMinimum / exclusiveMaximum / minItems / maxItems /
|
|
22
|
+
* format (date | date-time | email | uuid | uri) /
|
|
23
|
+
* $ref (intra-document only) / oneOf / anyOf / allOf (shallow merge) /
|
|
24
|
+
* discriminator (propertyName + mapping) / example / examples / const /
|
|
25
|
+
* additionalProperties (no-op for generation; nested schema is
|
|
26
|
+
* recursively validated so unsupported inner keywords still FAIL LOUD).
|
|
27
|
+
*
|
|
28
|
+
* UNSUPPORTED keywords (FAIL LOUD at translate time, mirroring the
|
|
29
|
+
* validator): if / then / else / dependentSchemas / dependentRequired /
|
|
30
|
+
* unevaluatedProperties / unevaluatedItems / propertyNames /
|
|
31
|
+
* patternProperties / contentEncoding / contentMediaType /
|
|
32
|
+
* remote `http(s)://` $ref.
|
|
33
|
+
*
|
|
34
|
+
* Security guarantees mirror the validator:
|
|
35
|
+
* - Pattern length capped at 1024 chars (ReDoS guard).
|
|
36
|
+
* - `pattern` requires an own-node `maxLength` (ReDoS footgun).
|
|
37
|
+
* - Recursion depth capped at 64 levels.
|
|
38
|
+
* - $ref cycle detection via a build-time visited-pointer set.
|
|
39
|
+
*
|
|
40
|
+
* Precedence (mirrors `generate.ts` ordering, expressed via IR):
|
|
41
|
+
* example > examples[0] > enum[0]/enum > const > $ref >
|
|
42
|
+
* allOf (shallow merge of object branches) > oneOf[0]/union >
|
|
43
|
+
* anyOf[0]/union > type-fallback.
|
|
44
|
+
*/
|
|
45
|
+
import type { Schema } from '@databehave/schema';
|
|
46
|
+
import type { OasDoc as OasDocument, OasNode as OasSchemaNode } from './walker.js';
|
|
47
|
+
/**
|
|
48
|
+
* Translate an OpenAPI schema node into a `@databehave/schema` IR
|
|
49
|
+
* Schema instance. Throws at translate time on unsupported keywords,
|
|
50
|
+
* malformed `$ref`s, `$ref` cycles, ReDoS-vulnerable patterns, and
|
|
51
|
+
* schemas that exceed the depth cap. See the file header for the
|
|
52
|
+
* full keyword matrix and security guarantees.
|
|
53
|
+
*/
|
|
54
|
+
export declare function translateOasToIR(node: OasSchemaNode, doc: OasDocument): Promise<Schema<unknown>>;
|
|
55
|
+
//# sourceMappingURL=translate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../src/openapi/translate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,KAAK,EAAE,MAAM,IAAI,WAAW,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,aAAa,CAAA;AAgVlF;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,aAAa,EACnB,GAAG,EAAE,WAAW,GACf,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAO1B"}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
const DEPTH_CAP = 64;
|
|
2
|
+
const PATTERN_MAX_LENGTH = 1024;
|
|
3
|
+
const UNSUPPORTED_KEYWORDS = new Set([
|
|
4
|
+
'if', 'then', 'else',
|
|
5
|
+
'dependentSchemas', 'dependentRequired',
|
|
6
|
+
'unevaluatedProperties', 'unevaluatedItems',
|
|
7
|
+
'contentEncoding', 'contentMediaType',
|
|
8
|
+
'propertyNames', 'patternProperties',
|
|
9
|
+
]);
|
|
10
|
+
const FORMAT_MAP = {
|
|
11
|
+
date: 'date',
|
|
12
|
+
'date-time': 'datetime',
|
|
13
|
+
email: 'email',
|
|
14
|
+
uuid: 'uuid',
|
|
15
|
+
uri: 'url',
|
|
16
|
+
};
|
|
17
|
+
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
18
|
+
const buildErr = (pointer, message) => new Error(`@databehave/server/openapi: ${message} (at ${pointer})`);
|
|
19
|
+
const resolveRef = (ref, doc, pointer) => {
|
|
20
|
+
if (ref.includes('://'))
|
|
21
|
+
throw buildErr(pointer, `unsupported $ref: ${ref}`);
|
|
22
|
+
const m = /^#\/components\/schemas\/(.+)$/.exec(ref);
|
|
23
|
+
if (!m)
|
|
24
|
+
throw buildErr(pointer, `unsupported $ref: ${ref}`);
|
|
25
|
+
const name = decodeURIComponent(m[1]);
|
|
26
|
+
const target = doc.components?.schemas?.[name];
|
|
27
|
+
if (!target)
|
|
28
|
+
throw buildErr(pointer, `$ref not found: ${ref}`);
|
|
29
|
+
return target;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Build a databehave Schema from an arbitrary JSON value, used when
|
|
33
|
+
* an OAS node carries a literal `example` / `examples[0]` / `const`
|
|
34
|
+
* payload. Recursively maps objects → `obj({...})` and arrays →
|
|
35
|
+
* `tuple(...)` so the entire value is preserved in the IR.
|
|
36
|
+
*/
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
const literalize = (value, m, pointer, depth) => {
|
|
39
|
+
if (depth > DEPTH_CAP) {
|
|
40
|
+
throw buildErr(pointer, `schema nesting exceeds depth cap of ${DEPTH_CAP}`);
|
|
41
|
+
}
|
|
42
|
+
if (value === null ||
|
|
43
|
+
typeof value === 'string' ||
|
|
44
|
+
typeof value === 'number' ||
|
|
45
|
+
typeof value === 'boolean') {
|
|
46
|
+
return m.literal(value);
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
if (value.length === 0)
|
|
50
|
+
return m.tuple();
|
|
51
|
+
return m.tuple(...value.map((v) => literalize(v, m, pointer, depth + 1)));
|
|
52
|
+
}
|
|
53
|
+
if (isObj(value)) {
|
|
54
|
+
const fields = {};
|
|
55
|
+
for (const [k, v] of Object.entries(value)) {
|
|
56
|
+
fields[k] = literalize(v, m, pointer, depth + 1);
|
|
57
|
+
}
|
|
58
|
+
return m.obj(fields);
|
|
59
|
+
}
|
|
60
|
+
throw buildErr(pointer, `cannot literalize value of type ${typeof value}`);
|
|
61
|
+
};
|
|
62
|
+
const enterPtr = (ctx, segment) => ({
|
|
63
|
+
...ctx,
|
|
64
|
+
pointer: `${ctx.pointer}/${segment}`,
|
|
65
|
+
depth: ctx.depth + 1,
|
|
66
|
+
});
|
|
67
|
+
const pickType = (node) => {
|
|
68
|
+
if (Array.isArray(node.type))
|
|
69
|
+
return node.type.find((t) => t !== 'null');
|
|
70
|
+
return node.type;
|
|
71
|
+
};
|
|
72
|
+
const isNullable = (node) => node.nullable === true || (Array.isArray(node.type) && node.type.includes('null'));
|
|
73
|
+
const buildString = (node, ctx) => {
|
|
74
|
+
if (typeof node.pattern === 'string') {
|
|
75
|
+
if (node.pattern.length > PATTERN_MAX_LENGTH) {
|
|
76
|
+
throw buildErr(ctx.pointer, `pattern length ${node.pattern.length} exceeds cap of ${PATTERN_MAX_LENGTH} (ReDoS guard)`);
|
|
77
|
+
}
|
|
78
|
+
if (typeof node.maxLength !== 'number') {
|
|
79
|
+
throw buildErr(ctx.pointer, 'pattern requires maxLength on the same schema (footgun: ReDoS)');
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
new RegExp(node.pattern);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
throw buildErr(ctx.pointer, `invalid pattern: ${err instanceof Error ? err.message : String(err)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const fmt = typeof node.format === 'string' ? FORMAT_MAP[node.format] : undefined;
|
|
89
|
+
const stringNode = { kind: 'string', format: fmt ?? 'plain' };
|
|
90
|
+
if (typeof node.minLength === 'number')
|
|
91
|
+
stringNode.min = node.minLength;
|
|
92
|
+
if (typeof node.maxLength === 'number')
|
|
93
|
+
stringNode.max = node.maxLength;
|
|
94
|
+
if (typeof node.pattern === 'string')
|
|
95
|
+
stringNode.pattern = node.pattern;
|
|
96
|
+
return new ctx.m.StringSchema(stringNode);
|
|
97
|
+
};
|
|
98
|
+
const buildNumber = (node, ctx, isInt) => {
|
|
99
|
+
// schema's IR has only min/max (inclusive). exclusiveMinimum/Maximum are
|
|
100
|
+
// approximated as inclusive for IR mock-generation purposes — the
|
|
101
|
+
// generated value drift is at most one ulp / one int and acceptable for
|
|
102
|
+
// placeholder fixtures. The validator (validate.ts) still enforces the
|
|
103
|
+
// exact exclusive semantics on real inbound requests.
|
|
104
|
+
const numNode = { kind: 'number', int: isInt };
|
|
105
|
+
if (typeof node.minimum === 'number')
|
|
106
|
+
numNode.min = node.minimum;
|
|
107
|
+
else if (typeof node.exclusiveMinimum === 'number') {
|
|
108
|
+
numNode.min = node.exclusiveMinimum;
|
|
109
|
+
}
|
|
110
|
+
if (typeof node.maximum === 'number')
|
|
111
|
+
numNode.max = node.maximum;
|
|
112
|
+
else if (typeof node.exclusiveMaximum === 'number') {
|
|
113
|
+
numNode.max = node.exclusiveMaximum;
|
|
114
|
+
}
|
|
115
|
+
return new ctx.m.NumberSchema(numNode);
|
|
116
|
+
};
|
|
117
|
+
const buildArray = (node, ctx) => {
|
|
118
|
+
const itemNode = node.items ?? {};
|
|
119
|
+
const itemSchema = translate(itemNode, enterPtr(ctx, 'items'));
|
|
120
|
+
let arr = ctx.m.arr(itemSchema);
|
|
121
|
+
if (typeof node.minItems === 'number')
|
|
122
|
+
arr = arr.min(node.minItems);
|
|
123
|
+
if (typeof node.maxItems === 'number')
|
|
124
|
+
arr = arr.max(node.maxItems);
|
|
125
|
+
return arr;
|
|
126
|
+
};
|
|
127
|
+
const buildObject = (node, ctx) => {
|
|
128
|
+
const required = new Set(Array.isArray(node.required) ? node.required : []);
|
|
129
|
+
const fields = {};
|
|
130
|
+
for (const [k, child] of Object.entries(node.properties ?? {})) {
|
|
131
|
+
const childSchema = translate(child, enterPtr(ctx, `properties/${k}`));
|
|
132
|
+
fields[k] = required.has(k) ? childSchema : childSchema.optional();
|
|
133
|
+
}
|
|
134
|
+
// `additionalProperties` is a no-op for *generation* — we only ever
|
|
135
|
+
// emit the declared `properties` above, so we never invent extra
|
|
136
|
+
// keys regardless of the keyword's value. We still acknowledge it
|
|
137
|
+
// explicitly so the translator's keyword surface mirrors
|
|
138
|
+
// `validation/validate.ts` (which DOES enforce it on inbound bodies).
|
|
139
|
+
const ap = node.additionalProperties;
|
|
140
|
+
if (ap === false) {
|
|
141
|
+
// Strict-extra-keys: aligned with validator behaviour. No-op here.
|
|
142
|
+
}
|
|
143
|
+
else if (ap === true || ap === undefined) {
|
|
144
|
+
// Open object: no-op — generator never invents undeclared keys.
|
|
145
|
+
}
|
|
146
|
+
else if (isObj(ap)) {
|
|
147
|
+
// Schema form: translate-and-discard. We don't use the result for
|
|
148
|
+
// generation (we never emit extra keys), but recursing now means
|
|
149
|
+
// an unsupported nested keyword (e.g. `if`) FAILS LOUD at build
|
|
150
|
+
// time instead of being silently accepted.
|
|
151
|
+
translate(ap, enterPtr(ctx, 'additionalProperties'));
|
|
152
|
+
}
|
|
153
|
+
return ctx.m.obj(fields);
|
|
154
|
+
};
|
|
155
|
+
const buildDiscriminated = (node, ctx, disc) => {
|
|
156
|
+
const branches = {};
|
|
157
|
+
for (const [tag, ref] of Object.entries(disc.mapping)) {
|
|
158
|
+
const childCtx = enterPtr(ctx, `discriminator/mapping/${tag}`);
|
|
159
|
+
if (childCtx.refStack.includes(ref)) {
|
|
160
|
+
throw buildErr(childCtx.pointer, `$ref cycle detected: ${ref}`);
|
|
161
|
+
}
|
|
162
|
+
const target = resolveRef(ref, ctx.doc, childCtx.pointer);
|
|
163
|
+
const branchSchema = translate(target, {
|
|
164
|
+
...childCtx,
|
|
165
|
+
refStack: [...childCtx.refStack, ref],
|
|
166
|
+
pointer: ref,
|
|
167
|
+
});
|
|
168
|
+
if (!(branchSchema instanceof ctx.m.ObjectSchema)) {
|
|
169
|
+
throw buildErr(childCtx.pointer, `discriminator branch ${JSON.stringify(tag)} must be an object schema`);
|
|
170
|
+
}
|
|
171
|
+
// Force the discriminator field to literal(tag) so `discriminated()`
|
|
172
|
+
// accepts the branch — OAS schemas typically declare it as
|
|
173
|
+
// `type: 'string'` rather than constraining to the tag value.
|
|
174
|
+
const objBranch = branchSchema;
|
|
175
|
+
const newFields = {
|
|
176
|
+
...objBranch._fields,
|
|
177
|
+
[disc.propertyName]: ctx.m.literal(tag),
|
|
178
|
+
};
|
|
179
|
+
branches[tag] = ctx.m.obj(newFields);
|
|
180
|
+
}
|
|
181
|
+
return ctx.m.discriminated(disc.propertyName, branches);
|
|
182
|
+
};
|
|
183
|
+
const buildAllOfMerge = (node, ctx) => {
|
|
184
|
+
// Mirror generate.ts: shallow-merge object-typed branches into a
|
|
185
|
+
// single obj schema. If any branch isn't object-typed once translated
|
|
186
|
+
// we bail and let the caller fall through to other precedence rules.
|
|
187
|
+
const merged = {};
|
|
188
|
+
let allObjects = true;
|
|
189
|
+
for (let i = 0; i < node.allOf.length; i++) {
|
|
190
|
+
const branch = translate(node.allOf[i], enterPtr(ctx, `allOf/${i}`));
|
|
191
|
+
if (!(branch instanceof ctx.m.ObjectSchema)) {
|
|
192
|
+
allObjects = false;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
const fields = branch._fields;
|
|
196
|
+
for (const [k, v] of Object.entries(fields))
|
|
197
|
+
merged[k] = v;
|
|
198
|
+
}
|
|
199
|
+
if (!allObjects)
|
|
200
|
+
return null;
|
|
201
|
+
return ctx.m.obj(merged);
|
|
202
|
+
};
|
|
203
|
+
const translate = (node, ctx) => {
|
|
204
|
+
if (ctx.depth > DEPTH_CAP) {
|
|
205
|
+
throw buildErr(ctx.pointer, `schema nesting exceeds depth cap of ${DEPTH_CAP}`);
|
|
206
|
+
}
|
|
207
|
+
if (!isObj(node))
|
|
208
|
+
throw buildErr(ctx.pointer, 'schema node must be an object');
|
|
209
|
+
for (const key of Object.keys(node)) {
|
|
210
|
+
if (UNSUPPORTED_KEYWORDS.has(key)) {
|
|
211
|
+
throw buildErr(ctx.pointer, `unsupported JSON Schema keyword: ${key}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Precedence (mirrors generate.ts):
|
|
215
|
+
// example > examples[0] > enum > const > $ref > allOf > oneOf > anyOf > type
|
|
216
|
+
if (node.example !== undefined) {
|
|
217
|
+
return literalize(node.example, ctx.m, ctx.pointer, ctx.depth);
|
|
218
|
+
}
|
|
219
|
+
if (Array.isArray(node.examples) && node.examples.length > 0) {
|
|
220
|
+
return literalize(node.examples[0], ctx.m, ctx.pointer, ctx.depth);
|
|
221
|
+
}
|
|
222
|
+
if (Array.isArray(node.enum) && node.enum.length > 0) {
|
|
223
|
+
const allStrNum = node.enum.every((v) => typeof v === 'string' || typeof v === 'number');
|
|
224
|
+
return allStrNum
|
|
225
|
+
? ctx.m.enum_(node.enum)
|
|
226
|
+
: ctx.m.union(...node.enum.map((v) => literalize(v, ctx.m, ctx.pointer, ctx.depth)));
|
|
227
|
+
}
|
|
228
|
+
if (node.const !== undefined) {
|
|
229
|
+
return literalize(node.const, ctx.m, ctx.pointer, ctx.depth);
|
|
230
|
+
}
|
|
231
|
+
if (typeof node.$ref === 'string') {
|
|
232
|
+
if (ctx.refStack.includes(node.$ref)) {
|
|
233
|
+
throw buildErr(ctx.pointer, `$ref cycle detected: ${[...ctx.refStack, node.$ref].join(' -> ')}`);
|
|
234
|
+
}
|
|
235
|
+
const target = resolveRef(node.$ref, ctx.doc, ctx.pointer);
|
|
236
|
+
const out = translate(target, {
|
|
237
|
+
...ctx,
|
|
238
|
+
refStack: [...ctx.refStack, node.$ref],
|
|
239
|
+
depth: ctx.depth + 1,
|
|
240
|
+
pointer: node.$ref,
|
|
241
|
+
});
|
|
242
|
+
return isNullable(node) ? out.nullable() : out;
|
|
243
|
+
}
|
|
244
|
+
if (Array.isArray(node.allOf) && node.allOf.length > 0) {
|
|
245
|
+
const merged = buildAllOfMerge(node, ctx);
|
|
246
|
+
if (merged !== null)
|
|
247
|
+
return isNullable(node) ? merged.nullable() : merged;
|
|
248
|
+
}
|
|
249
|
+
const disc = node.discriminator;
|
|
250
|
+
if (Array.isArray(node.oneOf) && node.oneOf.length > 0) {
|
|
251
|
+
if (disc && disc.mapping) {
|
|
252
|
+
const out = buildDiscriminated(node, ctx, { propertyName: disc.propertyName, mapping: disc.mapping });
|
|
253
|
+
return isNullable(node) ? out.nullable() : out;
|
|
254
|
+
}
|
|
255
|
+
const branches = node.oneOf.map((b, i) => translate(b, enterPtr(ctx, `oneOf/${i}`)));
|
|
256
|
+
const out = ctx.m.union(...branches);
|
|
257
|
+
return isNullable(node) ? out.nullable() : out;
|
|
258
|
+
}
|
|
259
|
+
if (Array.isArray(node.anyOf) && node.anyOf.length > 0) {
|
|
260
|
+
const branches = node.anyOf.map((b, i) => translate(b, enterPtr(ctx, `anyOf/${i}`)));
|
|
261
|
+
const out = ctx.m.union(...branches);
|
|
262
|
+
return isNullable(node) ? out.nullable() : out;
|
|
263
|
+
}
|
|
264
|
+
// type-fallback
|
|
265
|
+
let t = pickType(node);
|
|
266
|
+
if (!t && node.properties)
|
|
267
|
+
t = 'object';
|
|
268
|
+
if (!t && node.items !== undefined)
|
|
269
|
+
t = 'array';
|
|
270
|
+
if (!t) {
|
|
271
|
+
// Unknown / open node: best-effort — empty object schema, with
|
|
272
|
+
// nullable applied if requested.
|
|
273
|
+
const empty = ctx.m.obj({});
|
|
274
|
+
return isNullable(node) ? empty.nullable() : empty;
|
|
275
|
+
}
|
|
276
|
+
let built;
|
|
277
|
+
switch (t) {
|
|
278
|
+
case 'string':
|
|
279
|
+
built = buildString(node, ctx);
|
|
280
|
+
break;
|
|
281
|
+
case 'integer':
|
|
282
|
+
built = buildNumber(node, ctx, true);
|
|
283
|
+
break;
|
|
284
|
+
case 'number':
|
|
285
|
+
built = buildNumber(node, ctx, false);
|
|
286
|
+
break;
|
|
287
|
+
case 'boolean':
|
|
288
|
+
built = ctx.m.bool();
|
|
289
|
+
break;
|
|
290
|
+
case 'null':
|
|
291
|
+
built = ctx.m.null_();
|
|
292
|
+
break;
|
|
293
|
+
case 'array':
|
|
294
|
+
built = buildArray(node, ctx);
|
|
295
|
+
break;
|
|
296
|
+
case 'object':
|
|
297
|
+
built = buildObject(node, ctx);
|
|
298
|
+
break;
|
|
299
|
+
default:
|
|
300
|
+
throw buildErr(ctx.pointer, `unsupported OpenAPI type: ${t}`);
|
|
301
|
+
}
|
|
302
|
+
return isNullable(node) ? built.nullable() : built;
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* Translate an OpenAPI schema node into a `@databehave/schema` IR
|
|
306
|
+
* Schema instance. Throws at translate time on unsupported keywords,
|
|
307
|
+
* malformed `$ref`s, `$ref` cycles, ReDoS-vulnerable patterns, and
|
|
308
|
+
* schemas that exceed the depth cap. See the file header for the
|
|
309
|
+
* full keyword matrix and security guarantees.
|
|
310
|
+
*/
|
|
311
|
+
export async function translateOasToIR(node, doc) {
|
|
312
|
+
// Genuine-separation invariant: dynamic import only. Do NOT replace
|
|
313
|
+
// with a static `import { ... } from '@databehave/schema'` — that
|
|
314
|
+
// would re-introduce a runtime dependency on the schema package and
|
|
315
|
+
// break the OAS-only consumer's zero-dep posture.
|
|
316
|
+
const m = await import('@databehave/schema');
|
|
317
|
+
return translate(node, { doc, refStack: [], depth: 0, pointer: '#', m });
|
|
318
|
+
}
|
|
319
|
+
//# sourceMappingURL=translate.js.map
|