@databehave/server 0.1.1 → 0.1.2
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/package.json +24 -22
- package/docs/admin/error-mode.md +0 -14
- package/docs/admin/headers.md +0 -63
- package/docs/admin/index.md +0 -111
- package/docs/admin/overrides.md +0 -23
- package/docs/admin/rest-api.md +0 -90
- package/docs/admin/scenarios.md +0 -28
- package/docs/cli.md +0 -76
- package/docs/config/admin.md +0 -62
- package/docs/config/cors.md +0 -72
- package/docs/config/endpoints.md +0 -162
- package/docs/config/index.md +0 -107
- package/docs/config/log.md +0 -43
- package/docs/config/mock-mode.md +0 -84
- package/docs/config/openapi.md +0 -8
- package/docs/config/schema.md +0 -34
- package/docs/config/server.md +0 -32
- package/docs/config/validation.md +0 -161
- package/docs/design.md +0 -517
- package/docs/errors.md +0 -192
- package/docs/getting-started.md +0 -146
- package/docs/index.md +0 -23
- package/docs/openapi/auto-schema.md +0 -146
- package/docs/openapi/fallback.md +0 -242
- package/docs/openapi/keywords.md +0 -78
- package/docs/recipes.md +0 -258
- package/docs/stability.md +0 -118
- package/docs/troubleshooting.md +0 -191
package/docs/openapi/fallback.md
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
# OpenAPI fallback
|
|
2
|
-
|
|
3
|
-
Paths declared in the OpenAPI document but absent from `endpoints` are
|
|
4
|
-
auto-served by the walker. **Hand-written routes always win** — the
|
|
5
|
-
walker silently skips any key already declared in `endpoints`. There
|
|
6
|
-
is no precedence flag; the OAS doc is the source of truth for *gaps*,
|
|
7
|
-
not for overrides. This rule is canonical here; other pages link back.
|
|
8
|
-
|
|
9
|
-
This is intentional. The OAS document is your spec; @databehave/server treats it
|
|
10
|
-
as the source of truth for *gaps*, not as an override.
|
|
11
|
-
|
|
12
|
-
## Input format
|
|
13
|
-
|
|
14
|
-
@databehave/server accepts **JSON** OpenAPI documents only — convert
|
|
15
|
-
YAML first via `yq -o=json openapi.yaml > openapi.json` and pass the
|
|
16
|
-
JSON path or string to the loader. YAML inputs are rejected at boot;
|
|
17
|
-
see [config/openapi.md](../config/openapi.md) for the canonical
|
|
18
|
-
error wording.
|
|
19
|
-
|
|
20
|
-
A leading UTF-8 byte-order mark (`U+FEFF`) is stripped before parsing,
|
|
21
|
-
so files exported from Windows tooling load without manual cleanup.
|
|
22
|
-
After BOM stripping, the input must parse as plain JSON or
|
|
23
|
-
`JSON.parse` raises (caught and re-thrown with the file path
|
|
24
|
-
prepended).
|
|
25
|
-
|
|
26
|
-
## Pipeline
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
openapi.json ─▶ parser ─▶ paths.<p>.<m> ─▶ walker ─▶ generateFromOasSchema ─▶ response
|
|
30
|
-
│
|
|
31
|
-
├─▶ onOpenApiWalkError (recoverable)
|
|
32
|
-
└─▶ onOpenApiEmptySchema (advisory)
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
The walker visits every `paths.*.<method>.responses['200'].content
|
|
36
|
-
['application/json'].schema` and feeds the OAS node to the in-server,
|
|
37
|
-
zero-dep generator (`src/openapi/generate.ts`), which produces a
|
|
38
|
-
deterministic placeholder JSON value. For non-200 responses (used by
|
|
39
|
-
`mockMode`), the same generator feeds the per-status body builders.
|
|
40
|
-
For richer mock data (distributions, datasets, FK-aware generation),
|
|
41
|
-
install `@databehave/schema` separately and pre-generate fixtures —
|
|
42
|
-
the server itself pulls no DSL dependency at runtime.
|
|
43
|
-
|
|
44
|
-
### What the walker visits
|
|
45
|
-
|
|
46
|
-
- `paths['/']` (root) is **skipped**. Use an explicit `endpoints`
|
|
47
|
-
entry if you need to serve `/`.
|
|
48
|
-
- HTTP methods walked are exactly `get`, `post`, `put`, `delete`,
|
|
49
|
-
`patch`. `head` and `options` are handled by the dispatcher
|
|
50
|
-
(HEAD auto-derives from GET; OPTIONS is owned by CORS / admin).
|
|
51
|
-
- Vendor extensions (`x-*` keys) on path items are ignored.
|
|
52
|
-
- The OAS-only generator (`src/openapi/generate.ts`) is **not**
|
|
53
|
-
depth-capped; cyclic `$ref`s are detected via a visited-pointer
|
|
54
|
-
set and collapse to `{}` rather than infinite-loop. The
|
|
55
|
-
auto-schema translator and the request validator both hard-cap
|
|
56
|
-
recursion at **64** levels of nested `properties` / `items` /
|
|
57
|
-
`allOf` / `oneOf` / `anyOf` and **fail loud** past that — see
|
|
58
|
-
[`keywords.md` → Auto-schema translator](./keywords.md#auto-schema-translator-schema-set)
|
|
59
|
-
and [`config/validation.md#security-guarantees`](../config/validation.md#security-guarantees).
|
|
60
|
-
Hit either in practice → flatten the schema or declare the
|
|
61
|
-
route in `endpoints` directly.
|
|
62
|
-
|
|
63
|
-
### Status code picked
|
|
64
|
-
|
|
65
|
-
For mock-mode passthrough (no override) the walker picks the
|
|
66
|
-
response body from `responses['200']`. For mock-mode injected
|
|
67
|
-
statuses, the body comes from the smallest `2xx` declared in the
|
|
68
|
-
spec when an exact match is missing, falling back to the OAS
|
|
69
|
-
`default` response, falling back to the envelope
|
|
70
|
-
`{ error: true, status: N }`. `204` always renders as an empty
|
|
71
|
-
body regardless of what the spec declares.
|
|
72
|
-
|
|
73
|
-
### Per-status body generators
|
|
74
|
-
|
|
75
|
-
`buildOpenApiResponseGenerators(doc)` exposes the per-status body
|
|
76
|
-
table the dispatcher uses internally — keyed by `"METHOD path"`,
|
|
77
|
-
each entry maps a status code to a memoised body generator. The
|
|
78
|
-
function is exported so consumers wiring mock-mode into a custom
|
|
79
|
-
dispatch can reuse the same body shapes the kit ships with. See
|
|
80
|
-
[../config/mock-mode.md#body-resolution](../config/mock-mode.md#body-resolution)
|
|
81
|
-
for how the dispatcher consumes the table.
|
|
82
|
-
|
|
83
|
-
## Deterministic seeds
|
|
84
|
-
|
|
85
|
-
For a route `/users/:id` matched against `/users/42?expand=profile`:
|
|
86
|
-
|
|
87
|
-
```text
|
|
88
|
-
seed = sha256(
|
|
89
|
-
'/users/:id' // @databehave/server pattern, not the live URL
|
|
90
|
-
+ '|param:id=42' // path params first (sorted)
|
|
91
|
-
+ '|expand=profile' // query params (sorted)
|
|
92
|
-
)
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Same URL → same JSON body, byte-for-byte, on every machine. This is
|
|
96
|
-
what makes snapshot tests stable across CI runs.
|
|
97
|
-
|
|
98
|
-
Per-status bodies use `endpoint|status` as the seed, so the 500 body
|
|
99
|
-
for `/users/:id` is stable but different from the 404 body.
|
|
100
|
-
|
|
101
|
-
## Empty / permissive schemas (`schema: {}`)
|
|
102
|
-
|
|
103
|
-
```jsonc
|
|
104
|
-
{
|
|
105
|
-
"/api/v1/anything": {
|
|
106
|
-
"get": {
|
|
107
|
-
"responses": {
|
|
108
|
-
"200": {
|
|
109
|
-
"content": {
|
|
110
|
-
"application/json": {
|
|
111
|
-
"schema": {} // ← "any JSON value" per JSON Schema
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Per JSON Schema spec this matches any value, so @databehave/server:
|
|
122
|
-
|
|
123
|
-
1. Does **not** raise this via `onOpenApiWalkError` — it is a valid
|
|
124
|
-
OAS construct.
|
|
125
|
-
2. Serves a small stub body (`{}` for GET, `{ success: true, message:
|
|
126
|
-
null }` for mutations).
|
|
127
|
-
3. Surfaces the gap via the dedicated `onOpenApiEmptySchema` callback
|
|
128
|
-
so you can fix the spec.
|
|
129
|
-
|
|
130
|
-
The default JSONC loader wires this to a friendly warning:
|
|
131
|
-
|
|
132
|
-
```text
|
|
133
|
-
[@databehave/server] OAS response schema is empty for GET /api/v1/anything (status 200)
|
|
134
|
-
— serving stub. Consider filling the schema in your OpenAPI document.
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
To handle it yourself programmatically:
|
|
138
|
-
|
|
139
|
-
```ts
|
|
140
|
-
createServer({
|
|
141
|
-
openapi: doc,
|
|
142
|
-
onOpenApiEmptySchema: (method, path, status) => {
|
|
143
|
-
metrics.increment('@databehave/server.empty_schema', { method, path, status })
|
|
144
|
-
},
|
|
145
|
-
})
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## Walker failures
|
|
149
|
-
|
|
150
|
-
When the walker hits an OAS construct it genuinely cannot translate
|
|
151
|
-
(unsupported keyword, unresolvable `$ref`, etc.), it falls back to a
|
|
152
|
-
stub and reports via `onOpenApiWalkError`:
|
|
153
|
-
|
|
154
|
-
```text
|
|
155
|
-
[@databehave/server] openapi walk failed for GET /api/v1/weird: $ref not found: #/components/schemas/Missing
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
The route stays usable (the stub body keeps clients happy) but the
|
|
159
|
-
gap is visible. The JSONC loader wires this to `console.warn` by
|
|
160
|
-
default; override with:
|
|
161
|
-
|
|
162
|
-
```ts
|
|
163
|
-
createServer({
|
|
164
|
-
openapi: doc,
|
|
165
|
-
onOpenApiWalkError: (method, path, err) => {
|
|
166
|
-
// Fail CI on any walker error:
|
|
167
|
-
throw err
|
|
168
|
-
},
|
|
169
|
-
})
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
## Skipping OAS-declared routes
|
|
173
|
-
|
|
174
|
-
Routes that exist in `endpoints` (hand-written or static) are
|
|
175
|
-
automatically skipped by the walker — no extra configuration needed.
|
|
176
|
-
This is the recommended way to override an OAS path: declare it in
|
|
177
|
-
`endpoints` and the walker leaves it alone.
|
|
178
|
-
|
|
179
|
-
If you need to skip an OAS path without declaring a replacement
|
|
180
|
-
(e.g. a deprecated endpoint), declare it as an `EndpointResponse`
|
|
181
|
-
returning the deprecation message:
|
|
182
|
-
|
|
183
|
-
```jsonc
|
|
184
|
-
"endpoints": {
|
|
185
|
-
"GET /api/v1/legacy": {
|
|
186
|
-
"response": {
|
|
187
|
-
"status": 410,
|
|
188
|
-
"json": { "error": "gone", "message": "Use /api/v2/replacement" }
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
## Trust model
|
|
195
|
-
|
|
196
|
-
@databehave/server assumes the OpenAPI document comes from a **trusted source**
|
|
197
|
-
(your own repo, your own build pipeline). The walker passes `pattern` fields
|
|
198
|
-
straight to `new RegExp(...)` and that compiled regex is consulted by
|
|
199
|
-
`parse()` on incoming request bodies. A hostile OAS author could therefore
|
|
200
|
-
supply a catastrophic-backtracking pattern (e.g. `(a+)+b`) that would slow
|
|
201
|
-
down request validation against attacker-controlled input.
|
|
202
|
-
|
|
203
|
-
Mock-body generation (`mock()`) does **not** evaluate the regex, so simply
|
|
204
|
-
*loading* a hostile OAS is safe — but if you also turn on request-body
|
|
205
|
-
validation in front of untrusted input, validate or sandbox patterns from
|
|
206
|
-
untrusted OAS docs before passing them in.
|
|
207
|
-
|
|
208
|
-
### `pattern` requires a `maxLength` sibling
|
|
209
|
-
|
|
210
|
-
To make this concrete, the generator refuses to mock a string node
|
|
211
|
-
that declares a `pattern` without a `maxLength`. The boot-time error
|
|
212
|
-
is:
|
|
213
|
-
|
|
214
|
-
```text
|
|
215
|
-
@databehave/server/openapi: pattern at <pointer> requires a sibling maxLength to bound generation (ReDoS guard)
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
This is a hard guard, not a warning — fixing the OAS to add a
|
|
219
|
-
sensible `maxLength` is required before the kit will boot. The same
|
|
220
|
-
guard fires on nested string nodes, e.g. `items.pattern` inside an
|
|
221
|
-
array.
|
|
222
|
-
|
|
223
|
-
The `pattern` source itself is also length-capped at **1024
|
|
224
|
-
characters** in both the auto-schema translator and the request
|
|
225
|
-
validator. A pattern longer than the cap is rejected at boot with:
|
|
226
|
-
|
|
227
|
-
```text
|
|
228
|
-
@databehave/server/openapi: pattern length <N> exceeds cap of 1024 (ReDoS guard) (at <pointer>)
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
The cap exists because regex engines have super-linear behaviour on
|
|
232
|
-
sufficiently long pathological patterns even with `maxLength`-bounded
|
|
233
|
-
input — capping the pattern source is the cheap belt-and-braces.
|
|
234
|
-
|
|
235
|
-
## Admin route picker
|
|
236
|
-
|
|
237
|
-
The OAS document is also consumed by the admin panel. When
|
|
238
|
-
`admin.enabled`, the kit serves `GET {admin.path}/openapi-routes`
|
|
239
|
-
which returns `{ routes: { method, path, summary }[], discoveredAt }`
|
|
240
|
-
derived from the same OAS doc the walker uses. The admin UI calls this
|
|
241
|
-
endpoint to populate its route picker — no separate spec is needed.
|
|
242
|
-
See [../admin/rest-api.md](../admin/rest-api.md).
|
package/docs/openapi/keywords.md
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# Supported OAS keywords
|
|
2
|
-
|
|
3
|
-
The kit ships **two** OAS readers and they support different
|
|
4
|
-
keyword sets. Pick the one that matches your config:
|
|
5
|
-
|
|
6
|
-
- The **OAS-only generator** runs by default — when `schema:` is
|
|
7
|
-
absent from `databehave.jsonc`. Zero-dep, conservative subset.
|
|
8
|
-
- The **auto-schema translator** runs when `schema:` is set. Lifts
|
|
9
|
-
the OAS into the `@databehave/schema` IR, so it accepts a wider
|
|
10
|
-
keyword set and can fail loudly on constructs it cannot model.
|
|
11
|
-
See [auto-schema.md](./auto-schema.md).
|
|
12
|
-
|
|
13
|
-
## OAS-only generator (default)
|
|
14
|
-
|
|
15
|
-
```text
|
|
16
|
-
$ref ……………… resolved against components.schemas
|
|
17
|
-
circular refs collapse to {} (recursion guard)
|
|
18
|
-
type: 'object' properties + required
|
|
19
|
-
type: 'array' items
|
|
20
|
-
type: 'string' + minLength / maxLength / pattern (with maxLength sibling — see fallback.md)
|
|
21
|
-
type: 'integer' + minimum / maximum
|
|
22
|
-
type: 'number' + minimum / maximum
|
|
23
|
-
type: 'boolean'
|
|
24
|
-
enum string enums sampled from the list, deterministically
|
|
25
|
-
const literal value emitted verbatim
|
|
26
|
-
allOf merged left-to-right
|
|
27
|
-
oneOf first branch picked deterministically
|
|
28
|
-
anyOf first branch picked deterministically
|
|
29
|
-
nullable: true string types may emit null
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
`format` **is** consumed for the five common cases:
|
|
33
|
-
|
|
34
|
-
| `format` | Emitted value |
|
|
35
|
-
| ------------ | --- |
|
|
36
|
-
| `date` | `"2024-01-01"` |
|
|
37
|
-
| `date-time` | `"2024-01-01T00:00:00Z"` |
|
|
38
|
-
| `email` | `"user@example.com"` |
|
|
39
|
-
| `uuid` | `"00000000-0000-4000-8000-000000000000"` |
|
|
40
|
-
| `uri` / `url`| `"https://example.com/"` |
|
|
41
|
-
|
|
42
|
-
Any other `format` falls through to the bare-type default
|
|
43
|
-
(`"string"`, `0`, `false`, …). File an issue if you need
|
|
44
|
-
format-aware sampling beyond this list.
|
|
45
|
-
|
|
46
|
-
## Auto-schema translator (`schema:` set)
|
|
47
|
-
|
|
48
|
-
The translator (`translateOasToIR` in `src/openapi/translate.ts`)
|
|
49
|
-
accepts everything the OAS-only generator does, plus:
|
|
50
|
-
|
|
51
|
-
- `format`: `date`, `date-time`, `email`, `uuid`, `uri`, `url`.
|
|
52
|
-
- `nullable: true` (lifts the IR node to `union(t, null)`).
|
|
53
|
-
- `minLength`, `maxLength`, `pattern` (passed through).
|
|
54
|
-
- `exclusiveMinimum`, `exclusiveMaximum` (boolean OAS-3.0 form
|
|
55
|
-
and numeric OAS-3.1 form both accepted).
|
|
56
|
-
- `minItems`, `maxItems`.
|
|
57
|
-
- `discriminator` on `oneOf` / `anyOf` (resolves to the right
|
|
58
|
-
branch when the input has the discriminator property).
|
|
59
|
-
- `additionalProperties` (boolean and schema-shaped both honoured).
|
|
60
|
-
- `example` / `examples` (first usable value picked when present).
|
|
61
|
-
|
|
62
|
-
### Unsupported — fail loud
|
|
63
|
-
|
|
64
|
-
The translator refuses these keywords at boot rather than silently
|
|
65
|
-
dropping them. The error wording is
|
|
66
|
-
`@databehave/server: translateOasToIR failed for <route> responses[<status>]: unsupported keyword <name>`:
|
|
67
|
-
|
|
68
|
-
- `if` / `then` / `else`
|
|
69
|
-
- `dependentSchemas`, `dependentRequired`
|
|
70
|
-
- `unevaluatedProperties`, `unevaluatedItems`
|
|
71
|
-
- `propertyNames`
|
|
72
|
-
- `patternProperties`
|
|
73
|
-
- `contentEncoding`, `contentMediaType`
|
|
74
|
-
- Remote `$ref` (anything not under `#/components/schemas/...`)
|
|
75
|
-
|
|
76
|
-
The fail-loud list is intentionally short — the goal is to expose
|
|
77
|
-
gaps in the IR, not silently drift away from the spec. File an
|
|
78
|
-
issue if you hit one and need it modelled.
|
package/docs/recipes.md
DELETED
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
# Recipes
|
|
2
|
-
|
|
3
|
-
Practical patterns for common mocking problems. Each example is
|
|
4
|
-
self-contained and runnable against the public surface.
|
|
5
|
-
|
|
6
|
-
## Static health endpoint
|
|
7
|
-
|
|
8
|
-
```jsonc
|
|
9
|
-
"endpoints": {
|
|
10
|
-
"GET /health": {
|
|
11
|
-
"response": {
|
|
12
|
-
"status": 200,
|
|
13
|
-
"json": { "ok": true }
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
No handler module to write, no OAS entry needed. Add the path to
|
|
20
|
-
`mockMode.healthPaths` so `mockMode` does not inject an error status
|
|
21
|
-
into your probes:
|
|
22
|
-
|
|
23
|
-
```jsonc
|
|
24
|
-
"mockMode": {
|
|
25
|
-
"enabled": true,
|
|
26
|
-
"healthPaths": ["/health"]
|
|
27
|
-
}
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Force a single endpoint to 500
|
|
31
|
-
|
|
32
|
-
```jsonc
|
|
33
|
-
"endpoints": {
|
|
34
|
-
"POST products/bulk-update": {
|
|
35
|
-
"handler": "./src/routes/bulk-update.js",
|
|
36
|
-
"status": 500
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
The handler still loads (and stays valid for tests that call it
|
|
42
|
-
directly), but live HTTP requests get a 500 with the OAS-derived
|
|
43
|
-
error body. To remove the override, delete the `status` field.
|
|
44
|
-
|
|
45
|
-
## Force every endpoint to 500 (chaos mode)
|
|
46
|
-
|
|
47
|
-
```jsonc
|
|
48
|
-
"mockMode": {
|
|
49
|
-
"enabled": true,
|
|
50
|
-
"defaultStatus": 500,
|
|
51
|
-
"healthPaths": ["/health"]
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
Health probes pass through, everything else returns 500 with the
|
|
56
|
-
OAS-derived error body. Useful for client-side error-path testing.
|
|
57
|
-
|
|
58
|
-
## In-process testing with `fetch()`
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
import { describe, it, expect } from 'vitest'
|
|
62
|
-
import { createServer } from '@databehave/server'
|
|
63
|
-
import { loadConfig } from '@databehave/server'
|
|
64
|
-
|
|
65
|
-
describe('mock server', () => {
|
|
66
|
-
it('serves the OAS-derived east inventory', async () => {
|
|
67
|
-
const { config } = await loadConfig('./databehave.jsonc')
|
|
68
|
-
const server = createServer(config)
|
|
69
|
-
|
|
70
|
-
const res = await server.fetch(
|
|
71
|
-
new Request(
|
|
72
|
-
'http://localhost/api/v1/products/list?date_from=2024-01-01',
|
|
73
|
-
),
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
expect(res.status).toBe(200)
|
|
77
|
-
expect(res.headers.get('x-mock-status')).toBe('200')
|
|
78
|
-
const body = await res.json()
|
|
79
|
-
expect(body).toHaveProperty('result')
|
|
80
|
-
})
|
|
81
|
-
})
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
No sockets, no `listen()`. The same `Request` URL always yields the
|
|
85
|
-
same JSON body — assert against it directly or use a snapshot.
|
|
86
|
-
|
|
87
|
-
## Auth short-circuit via `hooks.onRequest`
|
|
88
|
-
|
|
89
|
-
```ts
|
|
90
|
-
import { createServer } from '@databehave/server'
|
|
91
|
-
|
|
92
|
-
const server = createServer({
|
|
93
|
-
routes: { /* ... */ },
|
|
94
|
-
hooks: {
|
|
95
|
-
onRequest: (req) => {
|
|
96
|
-
if (req.headers.authorization !== 'Bearer test-token') {
|
|
97
|
-
return { status: 401, json: { error: 'unauthorized' } }
|
|
98
|
-
}
|
|
99
|
-
// returning void → continue to the route handler
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
})
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
The handler is never reached for requests without the token. Pair
|
|
106
|
-
with `mockMode.healthPaths` if you want probes to bypass auth.
|
|
107
|
-
|
|
108
|
-
## Inject a header on every response
|
|
109
|
-
|
|
110
|
-
```ts
|
|
111
|
-
createServer({
|
|
112
|
-
routes: { /* ... */ },
|
|
113
|
-
hooks: {
|
|
114
|
-
onResponse: (_req, res) => ({
|
|
115
|
-
...res,
|
|
116
|
-
headers: { ...(res.headers ?? {}), 'x-build-id': process.env.BUILD_ID ?? 'local' },
|
|
117
|
-
}),
|
|
118
|
-
},
|
|
119
|
-
})
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
`onResponse` runs after the handler but before the runtime adapts the
|
|
123
|
-
POJO into a web `Response`.
|
|
124
|
-
|
|
125
|
-
## Generate deterministic bodies with `databehave`
|
|
126
|
-
|
|
127
|
-
```ts
|
|
128
|
-
// src/routes/products/list.ts
|
|
129
|
-
import { mock, obj, str, decimal, arr, seedFor } from '@databehave/server'
|
|
130
|
-
import type { Handler } from '@databehave/server'
|
|
131
|
-
|
|
132
|
-
const Product = obj({
|
|
133
|
-
category: str().in(['books', 'movies']),
|
|
134
|
-
price: decimal(10, 2).min('0').max('1000').typically(400, 700),
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
const Body = obj({ result: arr(Product).length(48) })
|
|
138
|
-
|
|
139
|
-
const handler: Handler = (req) => {
|
|
140
|
-
const seed = seedFor({
|
|
141
|
-
endpoint: '/api/v1/products/list',
|
|
142
|
-
extra: Object.fromEntries(new URL(req.url).searchParams),
|
|
143
|
-
})
|
|
144
|
-
return { json: mock(Body, { seed }) }
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export default handler
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
Same URL → same body. `databehave` is re-exported from `@databehave/server` so a
|
|
151
|
-
single import covers schema authoring and server wiring.
|
|
152
|
-
|
|
153
|
-
## Per-variant sub-routes (one handler each — no shared helpers)
|
|
154
|
-
|
|
155
|
-
When granularity matters, write each endpoint as a separate handler
|
|
156
|
-
with its own seed namespace. Do **not** extract a helper that loops
|
|
157
|
-
over variants — you lose the ability to tune `min` / `max` / `typically`
|
|
158
|
-
per endpoint without disturbing the others.
|
|
159
|
-
|
|
160
|
-
```jsonc
|
|
161
|
-
"endpoints": {
|
|
162
|
-
"GET products/list": "./src/routes/products/list.js",
|
|
163
|
-
"GET products/featured": "./src/routes/products/featured.js",
|
|
164
|
-
"GET products/summary": "./src/routes/products/summary.js"
|
|
165
|
-
}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Each handler controls its own `min` / `max` / `typically` ranges
|
|
169
|
-
independently.
|
|
170
|
-
|
|
171
|
-
## Custom CORS origin reflector
|
|
172
|
-
|
|
173
|
-
The JSONC form mirrors the request `Origin` by default. To customise:
|
|
174
|
-
|
|
175
|
-
```ts
|
|
176
|
-
createServer({
|
|
177
|
-
routes: { /* ... */ },
|
|
178
|
-
cors: {
|
|
179
|
-
origin: (o) => (o.endsWith('.example.com') ? o : 'https://app.example.com'),
|
|
180
|
-
credentials: true,
|
|
181
|
-
exposeHeaders: ['x-mock-status'],
|
|
182
|
-
},
|
|
183
|
-
})
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
## Bind to a random port in tests
|
|
187
|
-
|
|
188
|
-
```ts
|
|
189
|
-
const server = createServer(config)
|
|
190
|
-
const { port, close } = await server.listen({ port: 0 })
|
|
191
|
-
|
|
192
|
-
// ... run tests against http://127.0.0.1:${port} ...
|
|
193
|
-
|
|
194
|
-
await close()
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
`port: 0` asks the OS for a free port; the actual port comes back in
|
|
198
|
-
the handle.
|
|
199
|
-
|
|
200
|
-
## Inject a 503 on every route via the admin REST API
|
|
201
|
-
|
|
202
|
-
Enable `admin` (see [admin.md](./admin.md)), then POST a global
|
|
203
|
-
sticky override. Every subsequent request returns 503 until cleared.
|
|
204
|
-
|
|
205
|
-
```sh
|
|
206
|
-
curl -sS -X POST http://127.0.0.1:8000/databehave/overrides \
|
|
207
|
-
-H 'content-type: application/json' \
|
|
208
|
-
-d '{"matcher":{"kind":"global"},"mode":{"kind":"http-status","status":503}}'
|
|
209
|
-
|
|
210
|
-
curl -sS -X DELETE http://127.0.0.1:8000/databehave/overrides # clear
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
## Replay a captured response as a custom-body override
|
|
214
|
-
|
|
215
|
-
Paste a real response payload as base64 JSON into a sticky `custom-body`
|
|
216
|
-
override; the mock returns it verbatim, with the status of your choice.
|
|
217
|
-
|
|
218
|
-
```sh
|
|
219
|
-
BODY_B64=$(printf '{"result":[{"id":1,"name":"replayed"}]}' | base64)
|
|
220
|
-
curl -sS -X POST http://127.0.0.1:8000/databehave/overrides \
|
|
221
|
-
-H 'content-type: application/json' \
|
|
222
|
-
-d "{\"matcher\":{\"kind\":\"exact\",\"method\":\"GET\",\"path\":\"/api/v1/products/list\"},
|
|
223
|
-
\"mode\":{\"kind\":\"custom-body\",\"status\":200,\"body\":$(echo "$BODY_B64" | base64 -d)}}"
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
## Simulate slow networks with a sticky delay
|
|
227
|
-
|
|
228
|
-
A `delay` override wraps another mode (or passthrough). 3 seconds is
|
|
229
|
-
enough to exercise loading indicators without a test timing out.
|
|
230
|
-
|
|
231
|
-
```sh
|
|
232
|
-
curl -sS -X POST http://127.0.0.1:8000/databehave/overrides \
|
|
233
|
-
-H 'content-type: application/json' \
|
|
234
|
-
-d '{"matcher":{"kind":"global"},"mode":{"kind":"delay","ms":3000}}'
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
## Save and load named scenarios
|
|
238
|
-
|
|
239
|
-
Snapshot the current sticky overrides under a name, then re-apply later
|
|
240
|
-
in one step.
|
|
241
|
-
|
|
242
|
-
```sh
|
|
243
|
-
curl -sS -X POST http://127.0.0.1:8000/databehave/scenarios \
|
|
244
|
-
-H 'content-type: application/json' -d '{"name":"all-503"}'
|
|
245
|
-
|
|
246
|
-
curl -sS -X POST http://127.0.0.1:8000/databehave/scenarios/all-503/load
|
|
247
|
-
# → {"loaded":1}
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
Scenarios live on disk under `admin.scenariosDir` (default
|
|
251
|
-
`${cwd}/mock-scenarios`). See [admin.md](./admin.md#scenarios).
|
|
252
|
-
|
|
253
|
-
## Enable per-request access logs
|
|
254
|
-
|
|
255
|
-
Set `"log": true` (shorthand for `{ "access": true }`) to emit one
|
|
256
|
-
stdout line per request, with override-tag suffixes such as
|
|
257
|
-
`[override:http-status]`. Full field reference, JSON format, and
|
|
258
|
-
admin-traffic toggle live in [config/log.md](./config/log.md).
|
package/docs/stability.md
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# Stability Contract
|
|
2
|
-
|
|
3
|
-
This document defines what `@databehave/server` guarantees across versions, in
|
|
4
|
-
addition to the contract inherited from
|
|
5
|
-
[`@databehave/schema`](../../schema/docs/stability.md).
|
|
6
|
-
|
|
7
|
-
## Public surface
|
|
8
|
-
|
|
9
|
-
The **public surface** is exactly the symbols re-exported from `src/index.ts`,
|
|
10
|
-
plus the runtime shape of `Config` and the JSONC config schema
|
|
11
|
-
accepted by `loadConfig`. Anything reachable only through
|
|
12
|
-
deeper paths (`@databehave/server/src/...`, `_internal`, identifiers prefixed with
|
|
13
|
-
`_`) is **internal** and may change in any release, including `PATCH`.
|
|
14
|
-
|
|
15
|
-
The set of public symbols is locked by `test/public-surface.test.ts`.
|
|
16
|
-
|
|
17
|
-
The `admin` admin REST endpoints (`/overrides`, `/scenarios`,
|
|
18
|
-
`/openapi-routes`, `/ui*`) and their request / response shapes are part
|
|
19
|
-
of the public surface; the versioning rules below apply to them too.
|
|
20
|
-
|
|
21
|
-
## Versioning rules
|
|
22
|
-
|
|
23
|
-
Same `PATCH` / `MINOR` / `MAJOR` rules as `@databehave/schema` apply, plus:
|
|
24
|
-
|
|
25
|
-
| Bump | Additional triggers for `@databehave/server` |
|
|
26
|
-
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
27
|
-
| **PATCH** | Wire-level behaviour of `fetch(Request) → Response` for handlers that previously returned identical bodies. Header casing fixes. |
|
|
28
|
-
| **MINOR** | New CORS options, new mock-mode hooks, new JSONC fields with backwards-compatible defaults. Additive `admin` admin REST endpoints, additive fields on admin request / response shapes, new `x-mock-*` headers. |
|
|
29
|
-
| **MAJOR** | Default CORS policy changes that may close previously-permitted requests. Removal or renaming of JSONC fields. Changes to OAS path dispatch order. Removal / renaming of admin REST endpoints, breaking admin request / response shape changes, removal / renaming of `x-mock-*` headers, changes to sticky-override resolution priority. |
|
|
30
|
-
|
|
31
|
-
## Determinism contract
|
|
32
|
-
|
|
33
|
-
For a fixed config (`Config`, JSONC, OAS document) and a fixed
|
|
34
|
-
request, the **response body** generated by @databehave/server is deterministic
|
|
35
|
-
across `PATCH` and `MINOR` releases. The same per-request seeding rules
|
|
36
|
-
documented in `docs/seeding.md` apply.
|
|
37
|
-
|
|
38
|
-
## Mock-mode dispatch order
|
|
39
|
-
|
|
40
|
-
Within a single config, mock-mode resolves the response status and body in
|
|
41
|
-
this order, in this priority (first match wins):
|
|
42
|
-
|
|
43
|
-
1. Explicit `pathOverrides` entry keyed by `'<METHOD> /path'` (concrete path).
|
|
44
|
-
2. Explicit `pathOverrides` entry keyed by `'/path'` (any method).
|
|
45
|
-
3. Per-endpoint `status` set in JSONC `endpoints`.
|
|
46
|
-
4. `mockMode.defaultStatus` (when set).
|
|
47
|
-
|
|
48
|
-
Body resolution, once a status is chosen, follows this priority:
|
|
49
|
-
|
|
50
|
-
1. Hand-written handler body (when registered for the route).
|
|
51
|
-
2. OAS `responses[N]` schema match by **specificity**:
|
|
52
|
-
1. Static path patterns (`/items/me`) before parametric (`/items/:id`).
|
|
53
|
-
2. Within the same kind, longer paths before shorter wildcards.
|
|
54
|
-
3. Lexical fallback for ties (deterministic, declaration-order
|
|
55
|
-
independent).
|
|
56
|
-
3. Generic envelope `{ error: true, status: N }`.
|
|
57
|
-
|
|
58
|
-
This order is part of the public contract; changes are `MAJOR`.
|
|
59
|
-
|
|
60
|
-
## CORS defaults
|
|
61
|
-
|
|
62
|
-
- `cors: undefined` → no CORS headers added (handlers fully control).
|
|
63
|
-
- `cors: {}` → permissive default for **mock development**:
|
|
64
|
-
`Access-Control-Allow-Origin` mirrors the request `Origin` (or `*` if absent).
|
|
65
|
-
This is intentionally lax because @databehave/server is a developer mock server.
|
|
66
|
-
- `cors: { origin: ... }` → strict; the resolver function is authoritative.
|
|
67
|
-
- `cors: { allowHeaders: [] }` → hard lockdown; preflight omits the header.
|
|
68
|
-
|
|
69
|
-
When a deployment must run on a network where third parties can reach the
|
|
70
|
-
mock server, set `cors: { origin: '...' }` explicitly. The README documents
|
|
71
|
-
this.
|
|
72
|
-
|
|
73
|
-
## Fail-loud policy
|
|
74
|
-
|
|
75
|
-
Configuration that is structurally valid but semantically nonsensical throws
|
|
76
|
-
at config-load time, not at request time. Examples already enforced:
|
|
77
|
-
|
|
78
|
-
- `pathOverrides` keys that do not start with `/` or `<METHOD> /`.
|
|
79
|
-
- `pathOverrides` values that are not numbers.
|
|
80
|
-
- OpenAPI documents that fail to parse as JSON (convert YAML first via `yq -o=json`).
|
|
81
|
-
|
|
82
|
-
## Logger injection
|
|
83
|
-
|
|
84
|
-
Every diagnostic the kit emits goes through `LoadConfigOptions.logger.warn`
|
|
85
|
-
(or the equivalent `MockModeConfig.logger`). The kit never calls
|
|
86
|
-
`console.warn` / `console.error` directly. Callers may pipe to their preferred
|
|
87
|
-
sink (pino, winston, structured JSON, /dev/null).
|
|
88
|
-
|
|
89
|
-
The opt-in `log` feature (top-level `log` config, see
|
|
90
|
-
[config.md](./config.md#log)) is **separate** from diagnostics. It writes
|
|
91
|
-
one line per request to **stdout** by design, is off by default
|
|
92
|
-
(`log: false`), and is intentionally not routed through the logger
|
|
93
|
-
injection hook — access logs and diagnostic warnings have different
|
|
94
|
-
audiences and different destinations. This split is intentional and is
|
|
95
|
-
not a contract violation.
|
|
96
|
-
|
|
97
|
-
## Admin mode contract
|
|
98
|
-
|
|
99
|
-
When `admin` is opt-in, the following defaults are part of the
|
|
100
|
-
public contract — changing any default is **MAJOR**:
|
|
101
|
-
|
|
102
|
-
- `enabled: false`
|
|
103
|
-
- `path: '/databehave'`
|
|
104
|
-
- `bind: 'loopback-only'`
|
|
105
|
-
- `allowDestroy: true`
|
|
106
|
-
- `cors: 'auto'`
|
|
107
|
-
|
|
108
|
-
Additional rules:
|
|
109
|
-
|
|
110
|
-
- **`x-mock-*` header semantics.** Adding a new header is **MINOR**.
|
|
111
|
-
Removing or renaming a header, or changing the ErrorMode kind a
|
|
112
|
-
header resolves to, is **MAJOR**.
|
|
113
|
-
- **Sticky-override resolution priority.** The order
|
|
114
|
-
`header > exact > path > global > existing mockMode > passthrough`
|
|
115
|
-
is part of the contract — changes are **MAJOR**.
|
|
116
|
-
- **Scenarios file format.** Adding new top-level fields to a saved
|
|
117
|
-
scenario JSON is **MINOR**. Changing the directory layout, the file
|
|
118
|
-
naming, or the meaning of an existing field is **MAJOR**.
|