@molecule/api-mock-server 1.0.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/LICENSE +115 -0
- package/README.md +853 -0
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.d.ts.map +1 -0
- package/dist/browser-guard.js +19 -0
- package/dist/browser-guard.js.map +1 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +152 -0
- package/dist/cli.js.map +1 -0
- package/dist/fixtures/app-fixtures.d.ts +54 -0
- package/dist/fixtures/app-fixtures.d.ts.map +1 -0
- package/dist/fixtures/app-fixtures.js +601 -0
- package/dist/fixtures/app-fixtures.js.map +1 -0
- package/dist/fixtures/index.d.ts +9 -0
- package/dist/fixtures/index.d.ts.map +1 -0
- package/dist/fixtures/index.js +9 -0
- package/dist/fixtures/index.js.map +1 -0
- package/dist/fixtures/seed.d.ts +74 -0
- package/dist/fixtures/seed.d.ts.map +1 -0
- package/dist/fixtures/seed.js +112 -0
- package/dist/fixtures/seed.js.map +1 -0
- package/dist/fixtures/semantic-generator.d.ts +20 -0
- package/dist/fixtures/semantic-generator.d.ts.map +1 -0
- package/dist/fixtures/semantic-generator.js +539 -0
- package/dist/fixtures/semantic-generator.js.map +1 -0
- package/dist/fixtures/zod-walker.d.ts +34 -0
- package/dist/fixtures/zod-walker.d.ts.map +1 -0
- package/dist/fixtures/zod-walker.js +183 -0
- package/dist/fixtures/zod-walker.js.map +1 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/dist/scanner/index.d.ts +6 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +6 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/scanner.d.ts +21 -0
- package/dist/scanner/scanner.d.ts.map +1 -0
- package/dist/scanner/scanner.js +463 -0
- package/dist/scanner/scanner.js.map +1 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +7 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/middleware.d.ts +51 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +124 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/server.d.ts +29 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +314 -0
- package/dist/server/server.js.map +1 -0
- package/dist/states/index.d.ts +6 -0
- package/dist/states/index.d.ts.map +1 -0
- package/dist/states/index.js +6 -0
- package/dist/states/index.js.map +1 -0
- package/dist/states/states.d.ts +57 -0
- package/dist/states/states.d.ts.map +1 -0
- package/dist/states/states.js +89 -0
- package/dist/states/states.js.map +1 -0
- package/dist/types.d.ts +214 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
3
|
+
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
|
+
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
|
+
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
+
Generated: 2026-08-04T00:36:59.622Z
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# @molecule/api-mock-server
|
|
10
|
+
|
|
11
|
+
> **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
|
|
12
|
+
> It is written to be read by coding agents as much as by people, and is generated from this
|
|
13
|
+
> package's source — edit `src/index.ts` JSDoc, not this file.
|
|
14
|
+
|
|
15
|
+
Mock API server with deterministic fixture data for testing, screenshots, and E2E.
|
|
16
|
+
|
|
17
|
+
Provides a lightweight Express server that serves realistic fixture responses
|
|
18
|
+
for any molecule app type. Supports per-request state control via query params
|
|
19
|
+
and headers for testing success, empty, error, and unauthorized states.
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { createMockServer } from '@molecule/api-mock-server'
|
|
25
|
+
|
|
26
|
+
const server = await createMockServer({
|
|
27
|
+
appType: 'personal-finance',
|
|
28
|
+
fixturesPath: './api/fixtures', // directory of *.json fixture files
|
|
29
|
+
port: 4000,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Control state programmatically ('GET /accounts' and 'GET /api/accounts'
|
|
33
|
+
// are equivalent keys)
|
|
34
|
+
server.setState('GET /accounts', { state: 'error', statusCode: 500 })
|
|
35
|
+
server.setState('GET /transactions', { state: 'empty' })
|
|
36
|
+
server.setDefaultState('empty') // flips every endpoint at once
|
|
37
|
+
|
|
38
|
+
// Undo an endpoint override so ?_state / the default control it again —
|
|
39
|
+
// setState(key, { state: 'success' }) is NOT the same thing (see @remarks)
|
|
40
|
+
server.clearState('GET /accounts')
|
|
41
|
+
|
|
42
|
+
// Teardown
|
|
43
|
+
await server.close()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Type
|
|
47
|
+
|
|
48
|
+
`utility`
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install @molecule/api-mock-server express zod
|
|
54
|
+
npm install -D @types/express
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
### Interfaces
|
|
60
|
+
|
|
61
|
+
#### `AppDataPool`
|
|
62
|
+
|
|
63
|
+
App-specific fixture data pools
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
interface AppDataPool {
|
|
67
|
+
/** App type name */
|
|
68
|
+
appType: string
|
|
69
|
+
/** Resource-keyed fixture data: resource name -> array of records */
|
|
70
|
+
resources: Record<string, FixtureRecord[]>
|
|
71
|
+
/** Report endpoints: endpoint path suffix -> response data */
|
|
72
|
+
reports?: Record<string, unknown>
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### `AppFixtureSet`
|
|
77
|
+
|
|
78
|
+
Fixture set for an entire app type
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
interface AppFixtureSet {
|
|
82
|
+
/** App type name */
|
|
83
|
+
appType: string
|
|
84
|
+
/** Endpoint key -> fixture data */
|
|
85
|
+
endpoints: Map<string, EndpointFixture>
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### `EndpointDefinition`
|
|
90
|
+
|
|
91
|
+
A single discovered endpoint from handler scanning
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
interface EndpointDefinition {
|
|
95
|
+
/** HTTP method (GET, POST, PUT, DELETE) */
|
|
96
|
+
method: HttpMethod
|
|
97
|
+
/** Full route path (e.g. '/accounts', '/accounts/:id', '/reports/net-worth') */
|
|
98
|
+
path: string
|
|
99
|
+
/** Zod schema for request body (if any) */
|
|
100
|
+
bodySchema?: ZodSchemaDefinition
|
|
101
|
+
/** Whether the endpoint requires authentication */
|
|
102
|
+
requiresAuth: boolean
|
|
103
|
+
/** Expected response shape hints */
|
|
104
|
+
responseHints: ResponseHint
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### `EndpointFixture`
|
|
109
|
+
|
|
110
|
+
Fixture data for a single endpoint
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
interface EndpointFixture {
|
|
114
|
+
/** The endpoint definition */
|
|
115
|
+
endpoint: EndpointDefinition
|
|
116
|
+
/** Success response body */
|
|
117
|
+
successResponse: unknown
|
|
118
|
+
/** Empty state response body */
|
|
119
|
+
emptyResponse: unknown
|
|
120
|
+
/** Error response */
|
|
121
|
+
errorResponse: { error: string }
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### `FixtureConfig`
|
|
126
|
+
|
|
127
|
+
Configuration for fixture generation
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
interface FixtureConfig {
|
|
131
|
+
/** Number of items to generate for list endpoints */
|
|
132
|
+
listCount?: number
|
|
133
|
+
/** Seed override (default: derived from app type + path) */
|
|
134
|
+
seed?: number
|
|
135
|
+
/** App type for domain-specific data pools */
|
|
136
|
+
appType?: string
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### `FixtureRecord`
|
|
141
|
+
|
|
142
|
+
Pre-built fixture record for a specific resource
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
interface FixtureRecord {
|
|
146
|
+
[key: string]: unknown
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### `HandlerScanResult`
|
|
151
|
+
|
|
152
|
+
Result of scanning all handlers for an app type
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
interface HandlerScanResult {
|
|
156
|
+
/** App type (e.g. 'personal-finance', 'online-store') */
|
|
157
|
+
appType: string
|
|
158
|
+
/** All discovered endpoints */
|
|
159
|
+
endpoints: EndpointDefinition[]
|
|
160
|
+
/** Resource names discovered (accounts, transactions, etc.) */
|
|
161
|
+
resources: string[]
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### `MockServer`
|
|
166
|
+
|
|
167
|
+
Running mock server instance with control methods
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
interface MockServer {
|
|
171
|
+
/** The port the server is listening on */
|
|
172
|
+
port: number
|
|
173
|
+
/** The app type being served */
|
|
174
|
+
appType: string
|
|
175
|
+
/**
|
|
176
|
+
* Set a persistent state override for one endpoint (key: `"METHOD /path"`,
|
|
177
|
+
* e.g. `'GET /accounts'` — the bare and `/api/`-prefixed forms are
|
|
178
|
+
* equivalent keys). This is the HIGHEST-priority source of state for that
|
|
179
|
+
* endpoint. Full per-request precedence (highest first):
|
|
180
|
+
*
|
|
181
|
+
* 1. `setState(endpointKey, ...)` (this method)
|
|
182
|
+
* 2. per-request `?_state` query param / `X-Mock-State` header
|
|
183
|
+
* 3. `setDefaultState(...)` / the server's configured `defaultState`
|
|
184
|
+
*
|
|
185
|
+
* So a forgotten `setState('GET /accounts', { state: 'error' })` left over
|
|
186
|
+
* from an earlier test SILENTLY beats every later `?_state=success` on
|
|
187
|
+
* that endpoint — the request looks like `?_state` is being ignored.
|
|
188
|
+
* **Calling `setState(key, { state: 'success' })` does NOT remove the
|
|
189
|
+
* override** — it replaces it with an override that happens to look like
|
|
190
|
+
* the default, but per-request `?_state`/`X-Mock-State` still can't reach
|
|
191
|
+
* that endpoint (the override still outranks them). Use
|
|
192
|
+
* {@link MockServer.clearState} to actually remove the override and hand
|
|
193
|
+
* control back to per-request/default state. `setDefaultState()` never
|
|
194
|
+
* clears endpoint overrides either — the default is only the fallback used
|
|
195
|
+
* when no endpoint override exists at all.
|
|
196
|
+
* @param endpointKey - `"METHOD /path"`, e.g. `'GET /accounts'`.
|
|
197
|
+
* @param state - The state this endpoint returns for every request until
|
|
198
|
+
* `setState` is called again, or `clearState`d, for the same key.
|
|
199
|
+
*/
|
|
200
|
+
setState: (endpointKey: string, state: ResponseState) => void
|
|
201
|
+
/**
|
|
202
|
+
* Remove a persistent per-endpoint override previously set with
|
|
203
|
+
* {@link MockServer.setState}, restoring per-request `?_state`/
|
|
204
|
+
* `X-Mock-State` (and ultimately `setDefaultState()`) control over that
|
|
205
|
+
* endpoint. Pass the EXACT same key string used in the matching
|
|
206
|
+
* `setState()` call — keys are matched as opaque strings, not normalized,
|
|
207
|
+
* so `setState('GET /accounts', ...)` and `setState('GET /api/accounts',
|
|
208
|
+
* ...)` are independent overrides and each needs its own `clearState()`.
|
|
209
|
+
* Clearing a key with no active override is a harmless no-op.
|
|
210
|
+
* @param endpointKey - The same `"METHOD /path"` key passed to `setState`.
|
|
211
|
+
*/
|
|
212
|
+
clearState: (endpointKey: string) => void
|
|
213
|
+
/**
|
|
214
|
+
* Set the fallback state used for any endpoint that has no per-endpoint
|
|
215
|
+
* `setState()` override — see {@link MockServer.setState} for the full
|
|
216
|
+
* precedence chain. A per-request `?_state`/`X-Mock-State` value still
|
|
217
|
+
* wins over this default; only an active `setState()` override outranks
|
|
218
|
+
* both.
|
|
219
|
+
* @param state - The fallback response state for endpoints with no override.
|
|
220
|
+
*/
|
|
221
|
+
setDefaultState: (state: 'success' | 'empty' | 'error' | 'unauthorized') => void
|
|
222
|
+
/** Get the fixture set being served */
|
|
223
|
+
getFixtures: () => AppFixtureSet
|
|
224
|
+
/** Close the server */
|
|
225
|
+
close: () => Promise<void>
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
#### `MockServerConfig`
|
|
230
|
+
|
|
231
|
+
Configuration for the mock HTTP server
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
interface MockServerConfig {
|
|
235
|
+
/** App type to serve fixtures for (used for display and handler resolution) */
|
|
236
|
+
appType: string
|
|
237
|
+
/** Path to a directory of JSON fixture files (takes priority over appType for data) */
|
|
238
|
+
fixturesPath?: string
|
|
239
|
+
/** Port to listen on (default 4000) */
|
|
240
|
+
port?: number
|
|
241
|
+
/** Default response delay in ms (default 0) */
|
|
242
|
+
defaultDelay?: number
|
|
243
|
+
/** Default state for all endpoints */
|
|
244
|
+
defaultState?: 'success' | 'empty' | 'error' | 'unauthorized'
|
|
245
|
+
/** Per-endpoint state overrides (key: "METHOD /path") */
|
|
246
|
+
endpointStates?: Record<string, ResponseState>
|
|
247
|
+
/** Path to handler templates directory (auto-detected from appType) */
|
|
248
|
+
handlersPath?: string
|
|
249
|
+
/** Custom fixture data to use instead of auto-generated */
|
|
250
|
+
customFixtures?: AppFixtureSet
|
|
251
|
+
/** Whether to log requests (default true) */
|
|
252
|
+
logging?: boolean
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
#### `ResponseHint`
|
|
257
|
+
|
|
258
|
+
Hints about the response shape extracted from handler analysis
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
interface ResponseHint {
|
|
262
|
+
/** Whether the response is an array (list endpoint) */
|
|
263
|
+
isList: boolean
|
|
264
|
+
/** Whether the response is paginated ({ data, total, page, limit }) */
|
|
265
|
+
isPaginated: boolean
|
|
266
|
+
/** Whether the response includes nested resources */
|
|
267
|
+
hasNestedResources: boolean
|
|
268
|
+
/** Resource name (e.g. 'accounts', 'transactions') */
|
|
269
|
+
resourceName: string
|
|
270
|
+
/**
|
|
271
|
+
* Whether the success response is a single bare object literal
|
|
272
|
+
* (`res.json({ a, b, c })`) rather than a list or `{ data: [] }`
|
|
273
|
+
* envelope — e.g. `/analytics/summary`, `/profile/me`. When true,
|
|
274
|
+
* `responseFields` holds its top-level keys.
|
|
275
|
+
*/
|
|
276
|
+
isSingleObject?: boolean
|
|
277
|
+
/** Top-level field names of the success `res.json({...})` object, if extracted */
|
|
278
|
+
responseFields?: string[]
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### `ResponseState`
|
|
283
|
+
|
|
284
|
+
Response state for controlling mock behavior
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
interface ResponseState {
|
|
288
|
+
/** The state of the response */
|
|
289
|
+
state: 'success' | 'empty' | 'error' | 'unauthorized'
|
|
290
|
+
/**
|
|
291
|
+
* Additional delay in ms before responding. Clamped to `MAX_MOCK_DELAY_MS`
|
|
292
|
+
* (60s, see {@link applyDelay}) — an oversized value is capped and logged
|
|
293
|
+
* rather than honored verbatim, so it cannot hang a request indefinitely.
|
|
294
|
+
*/
|
|
295
|
+
delay?: number
|
|
296
|
+
/** Custom status code override */
|
|
297
|
+
statusCode?: number
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
#### `SemanticRule`
|
|
302
|
+
|
|
303
|
+
A semantic rule for generating realistic values based on field names
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
interface SemanticRule {
|
|
307
|
+
/** Field name pattern (regex) */
|
|
308
|
+
pattern: RegExp
|
|
309
|
+
/** Generator function that produces a realistic value */
|
|
310
|
+
generate: (rng: () => number, index: number) => unknown
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
#### `ZodSchemaDefinition`
|
|
315
|
+
|
|
316
|
+
Serialized Zod schema definition for fixture generation
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
interface ZodSchemaDefinition {
|
|
320
|
+
/** Schema type name (e.g. 'ZodObject', 'ZodString') */
|
|
321
|
+
type: string
|
|
322
|
+
/** Schema shape for objects: field name -> nested schema def */
|
|
323
|
+
shape?: Record<string, ZodSchemaDefinition>
|
|
324
|
+
/** Enum values if applicable */
|
|
325
|
+
enumValues?: string[]
|
|
326
|
+
/** Default value if specified */
|
|
327
|
+
defaultValue?: unknown
|
|
328
|
+
/** Inner type for optional/nullable wrappers */
|
|
329
|
+
innerType?: ZodSchemaDefinition
|
|
330
|
+
/** Element type for arrays */
|
|
331
|
+
elementType?: ZodSchemaDefinition
|
|
332
|
+
/** Constraints (min, max, positive, int, etc.) */
|
|
333
|
+
constraints?: {
|
|
334
|
+
min?: number
|
|
335
|
+
max?: number
|
|
336
|
+
positive?: boolean
|
|
337
|
+
int?: boolean
|
|
338
|
+
minLength?: number
|
|
339
|
+
maxLength?: number
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Types
|
|
345
|
+
|
|
346
|
+
#### `HttpMethod`
|
|
347
|
+
|
|
348
|
+
HTTP methods supported by the mock server
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Functions
|
|
355
|
+
|
|
356
|
+
#### `applyDelay(state)`
|
|
357
|
+
|
|
358
|
+
Apply a delay if specified in the response state. The requested delay is
|
|
359
|
+
capped at {@link MAX_MOCK_DELAY_MS} — a value above the cap is clamped and
|
|
360
|
+
a warning is logged (via `console.warn`, immediately, before waiting —
|
|
361
|
+
not after — so the clamp is visible in server logs right when the
|
|
362
|
+
oversized delay is requested rather than a minute later).
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
function applyDelay(state: ResponseState): Promise<void>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
- `state` — The response state that may contain a delay
|
|
369
|
+
|
|
370
|
+
**Returns:** A promise that resolves after the (possibly clamped) delay, or immediately if no delay was requested
|
|
371
|
+
|
|
372
|
+
#### `applySemanticRules(fieldName, rng, index, rules)`
|
|
373
|
+
|
|
374
|
+
Apply semantic rules to generate a value for a given field name.
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
function applySemanticRules(
|
|
378
|
+
fieldName: string,
|
|
379
|
+
rng: () => number,
|
|
380
|
+
index: number,
|
|
381
|
+
rules?: SemanticRule[],
|
|
382
|
+
): unknown
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
- `fieldName` — The name of the field to generate a value for
|
|
386
|
+
- `rng` — The seeded random function
|
|
387
|
+
- `index` — The item index (for cycling through pools)
|
|
388
|
+
- `rules` — Custom rules to apply (defaults to defaultSemanticRules)
|
|
389
|
+
|
|
390
|
+
**Returns:** The generated value, or undefined if no rule matched
|
|
391
|
+
|
|
392
|
+
#### `buildFixtureSet(appType, endpoints, fixturesDir)`
|
|
393
|
+
|
|
394
|
+
Build a complete fixture set for an app type by combining
|
|
395
|
+
data pool records with endpoint definitions.
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
function buildFixtureSet(
|
|
399
|
+
appType: string,
|
|
400
|
+
endpoints: EndpointDefinition[],
|
|
401
|
+
fixturesDir?: string,
|
|
402
|
+
): AppFixtureSet | undefined
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
- `appType` — The app type
|
|
406
|
+
- `endpoints` — The discovered endpoints from scanning
|
|
407
|
+
- `fixturesDir` — Path to the fixtures directory
|
|
408
|
+
|
|
409
|
+
**Returns:** A complete fixture set, or undefined if no data available
|
|
410
|
+
|
|
411
|
+
#### `corsMiddleware()`
|
|
412
|
+
|
|
413
|
+
CORS middleware that sets permissive CORS headers for development.
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
function corsMiddleware(): (req: Request, res: Response, next: NextFunction) => void
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Returns:** Express middleware function
|
|
420
|
+
|
|
421
|
+
#### `createMockServer(config)`
|
|
422
|
+
|
|
423
|
+
Create and start a mock API server for the given app type.
|
|
424
|
+
The server discovers endpoints by scanning handler templates and
|
|
425
|
+
serves deterministic fixture data for each discovered route.
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
function createMockServer(config: MockServerConfig): Promise<MockServer>
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
- `config` — Server configuration
|
|
432
|
+
|
|
433
|
+
**Returns:** A running MockServer instance with control methods
|
|
434
|
+
|
|
435
|
+
#### `createSeededRandom(seed)`
|
|
436
|
+
|
|
437
|
+
Create a seeded random number generator using Mulberry32.
|
|
438
|
+
Returns a function that produces numbers in [0, 1).
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
function createSeededRandom(seed: number): () => number
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
- `seed` — The seed value
|
|
445
|
+
|
|
446
|
+
**Returns:** A function that returns the next pseudo-random number
|
|
447
|
+
|
|
448
|
+
#### `futureDate(rng, daysAhead)`
|
|
449
|
+
|
|
450
|
+
Generate a "future" ISO date string within the next N days of the fixed
|
|
451
|
+
`FIXTURE_NOW` anchor (NOT the wall clock — see `FIXTURE_NOW`).
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
function futureDate(rng: () => number, daysAhead?: number): string
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
- `rng` — The seeded random function
|
|
458
|
+
- `daysAhead` — Maximum days in the future (default 365)
|
|
459
|
+
|
|
460
|
+
**Returns:** An ISO date string
|
|
461
|
+
|
|
462
|
+
#### `generateFixtures(fixturesDir, appType)`
|
|
463
|
+
|
|
464
|
+
Generate fixtures from a directory of JSON files without scanning handlers.
|
|
465
|
+
Builds standard CRUD endpoints for each resource file and custom endpoints
|
|
466
|
+
for each sub-endpoint group.
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
function generateFixtures(fixturesDir: string, appType?: string): AppFixtureSet | undefined
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
- `fixturesDir` — Absolute path to the fixtures directory
|
|
473
|
+
- `appType` — App type label (default: derived from directory name)
|
|
474
|
+
|
|
475
|
+
**Returns:** A fixture set with standard CRUD endpoints, or undefined if no data
|
|
476
|
+
|
|
477
|
+
#### `generateRecord(schema, rng, index)`
|
|
478
|
+
|
|
479
|
+
Generate a single conformant record from a schema, enriched with
|
|
480
|
+
standard fields (id, created_at, updated_at).
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
function generateRecord(
|
|
484
|
+
schema: ZodSchemaDefinition | undefined,
|
|
485
|
+
rng: () => number,
|
|
486
|
+
index: number,
|
|
487
|
+
): Record<string, unknown>
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
- `schema` — The serialized Zod schema definition
|
|
491
|
+
- `rng` — The seeded random function
|
|
492
|
+
- `index` — The item index
|
|
493
|
+
|
|
494
|
+
**Returns:** A record with all schema fields plus standard fields
|
|
495
|
+
|
|
496
|
+
#### `generateRecords(schema, count, appType, path, config)`
|
|
497
|
+
|
|
498
|
+
Generate multiple records from a schema definition.
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
function generateRecords(
|
|
502
|
+
schema: ZodSchemaDefinition | undefined,
|
|
503
|
+
count: number,
|
|
504
|
+
appType: string,
|
|
505
|
+
path: string,
|
|
506
|
+
config?: FixtureConfig,
|
|
507
|
+
): Record<string, unknown>[]
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
- `schema` — The serialized Zod schema definition
|
|
511
|
+
- `count` — Number of records to generate
|
|
512
|
+
- `appType` — App type for seed derivation
|
|
513
|
+
- `path` — Endpoint path for seed derivation
|
|
514
|
+
- `config` — Optional fixture configuration
|
|
515
|
+
|
|
516
|
+
**Returns:** An array of conformant records
|
|
517
|
+
|
|
518
|
+
#### `getAppDataPool(fixturesDir, appType)`
|
|
519
|
+
|
|
520
|
+
Get an AppDataPool for a fixtures directory, with fingerprint-validated
|
|
521
|
+
caching: editing/adding/removing a `*.json` fixture file (mtime or size
|
|
522
|
+
change) invalidates the cached pool, so a mock server re-created in the
|
|
523
|
+
same process serves the fresh data instead of the first load.
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
function getAppDataPool(fixturesDir: string, appType: string): AppDataPool | undefined
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
- `fixturesDir` — Absolute path to the fixtures directory
|
|
530
|
+
- `appType` — App type label
|
|
531
|
+
|
|
532
|
+
**Returns:** The loaded pool, or undefined if directory missing/empty
|
|
533
|
+
|
|
534
|
+
#### `getResponseBody(state, method, fixture)`
|
|
535
|
+
|
|
536
|
+
Get the response body for a given state, using the endpoint fixture data.
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
function getResponseBody(
|
|
540
|
+
state: ResponseState,
|
|
541
|
+
method: HttpMethod,
|
|
542
|
+
fixture: { successResponse: unknown; emptyResponse: unknown; errorResponse: { error: string } },
|
|
543
|
+
): unknown
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
- `state` — The response state
|
|
547
|
+
- `method` — The HTTP method
|
|
548
|
+
- `fixture` — The fixture data containing success, empty, and error responses
|
|
549
|
+
|
|
550
|
+
**Returns:** The response body, or null for 204 responses
|
|
551
|
+
|
|
552
|
+
#### `getStatusCode(state, method)`
|
|
553
|
+
|
|
554
|
+
Get the HTTP status code for a given state and method.
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
function getStatusCode(state: ResponseState, method: HttpMethod): number
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
- `state` — The response state
|
|
561
|
+
- `method` — The HTTP method
|
|
562
|
+
|
|
563
|
+
**Returns:** The appropriate HTTP status code
|
|
564
|
+
|
|
565
|
+
#### `loadFixturesFromDirectory(fixturesDir, appType)`
|
|
566
|
+
|
|
567
|
+
Load an AppDataPool from a directory of JSON fixture files.
|
|
568
|
+
|
|
569
|
+
File naming conventions:
|
|
570
|
+
|
|
571
|
+
- Array files (e.g. `products.json` containing `[...]`) become CRUD resources
|
|
572
|
+
- Object files (e.g. `reports.json` containing `{key: ...}`) become sub-endpoint groups
|
|
573
|
+
where each key maps to a GET endpoint
|
|
574
|
+
|
|
575
|
+
Special filenames are treated as report/sub-endpoint groups (object shape expected):
|
|
576
|
+
`reports.json`, `storefront.json`, `admin.json`
|
|
577
|
+
|
|
578
|
+
All other files are treated as array resources by default.
|
|
579
|
+
|
|
580
|
+
```typescript
|
|
581
|
+
function loadFixturesFromDirectory(fixturesDir: string, appType: string): AppDataPool | undefined
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
- `fixturesDir` — Absolute path to the fixtures directory
|
|
585
|
+
- `appType` — App type label (for the returned pool metadata)
|
|
586
|
+
|
|
587
|
+
**Returns:** An AppDataPool, or undefined if the directory doesn't exist or is empty
|
|
588
|
+
|
|
589
|
+
#### `loggingMiddleware()`
|
|
590
|
+
|
|
591
|
+
Request logging middleware for the mock server.
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
function loggingMiddleware(): (req: Request, res: Response, next: NextFunction) => void
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
**Returns:** Express middleware function
|
|
598
|
+
|
|
599
|
+
#### `parseState(stateStr)`
|
|
600
|
+
|
|
601
|
+
Parse a state string into a ResponseState object.
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
function parseState(stateStr: string): ResponseState
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
- `stateStr` — The state string (e.g. 'success', 'error', 'empty', 'unauthorized')
|
|
608
|
+
|
|
609
|
+
**Returns:** The parsed ResponseState. An unrecognized string falls back to `DEFAULT_STATES.success` — the same forgiving behavior as the per-request `?_state` middleware (which additionally labels the response with an `X-Mock-Invalid-State` header so typos are detectable).
|
|
610
|
+
|
|
611
|
+
#### `pick(rng, arr)`
|
|
612
|
+
|
|
613
|
+
Pick a random element from an array using the RNG.
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
function pick(rng: () => number, arr: readonly T[]): T
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
- `rng` — The seeded random function
|
|
620
|
+
- `arr` — The array to pick from
|
|
621
|
+
|
|
622
|
+
**Returns:** A random element from the array
|
|
623
|
+
|
|
624
|
+
#### `randomDollars(rng, min, max)`
|
|
625
|
+
|
|
626
|
+
Generate a random dollar amount rounded to cents.
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
function randomDollars(rng: () => number, min: number, max: number): number
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
- `rng` — The seeded random function
|
|
633
|
+
- `min` — Minimum value
|
|
634
|
+
- `max` — Maximum value
|
|
635
|
+
|
|
636
|
+
**Returns:** A number rounded to 2 decimal places
|
|
637
|
+
|
|
638
|
+
#### `randomInt(rng, min, max)`
|
|
639
|
+
|
|
640
|
+
Pick a random integer in [min, max] inclusive.
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
function randomInt(rng: () => number, min: number, max: number): number
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
- `rng` — The seeded random function
|
|
647
|
+
- `min` — Minimum value (inclusive)
|
|
648
|
+
- `max` — Maximum value (inclusive)
|
|
649
|
+
|
|
650
|
+
**Returns:** A random integer in the given range
|
|
651
|
+
|
|
652
|
+
#### `recentDate(rng, daysBack)`
|
|
653
|
+
|
|
654
|
+
Generate a "recent" ISO date string within the last N days of the fixed
|
|
655
|
+
`FIXTURE_NOW` anchor (NOT the wall clock — see `FIXTURE_NOW`).
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
function recentDate(rng: () => number, daysBack?: number): string
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
- `rng` — The seeded random function
|
|
662
|
+
- `daysBack` — Maximum days in the past (default 90)
|
|
663
|
+
|
|
664
|
+
**Returns:** An ISO date string
|
|
665
|
+
|
|
666
|
+
#### `resolveHandlersPath(appType, workspaceRoot)`
|
|
667
|
+
|
|
668
|
+
Resolve the handlers path for a given app type.
|
|
669
|
+
Searches standard locations in the mlcl templates directory.
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
function resolveHandlersPath(appType: string, workspaceRoot?: string): string | undefined
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
- `appType` — The app type name
|
|
676
|
+
- `workspaceRoot` — The workspace root directory
|
|
677
|
+
|
|
678
|
+
**Returns:** The resolved handlers path, or undefined if not found
|
|
679
|
+
|
|
680
|
+
#### `scanHandlers(handlersPath, appType)`
|
|
681
|
+
|
|
682
|
+
Scan all handler files for a given app type and produce endpoint definitions.
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
function scanHandlers(handlersPath: string, appType: string): HandlerScanResult
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
- `handlersPath` — Path to the handlers directory
|
|
689
|
+
- `appType` — The app type name
|
|
690
|
+
|
|
691
|
+
**Returns:** The scan result with all discovered endpoints
|
|
692
|
+
|
|
693
|
+
#### `seededUUID(rng)`
|
|
694
|
+
|
|
695
|
+
Generate a UUID-like string from the RNG (not cryptographically secure).
|
|
696
|
+
|
|
697
|
+
```typescript
|
|
698
|
+
function seededUUID(rng: () => number): string
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
- `rng` — The seeded random function
|
|
702
|
+
|
|
703
|
+
**Returns:** A UUID v4-like string
|
|
704
|
+
|
|
705
|
+
#### `seedFromPath(appType, path)`
|
|
706
|
+
|
|
707
|
+
Derive a deterministic seed from an app type and endpoint path.
|
|
708
|
+
The same app+path always produces the same seed, ensuring stable fixture output.
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
function seedFromPath(appType: string, path: string): number
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
- `appType` — The application type (e.g. 'personal-finance')
|
|
715
|
+
- `path` — The endpoint path (e.g. '/accounts')
|
|
716
|
+
|
|
717
|
+
**Returns:** A numeric seed value
|
|
718
|
+
|
|
719
|
+
#### `stateControlMiddleware(defaultState)`
|
|
720
|
+
|
|
721
|
+
Express middleware that extracts state control signals from the request
|
|
722
|
+
and attaches them to res.locals for the route handler to use.
|
|
723
|
+
|
|
724
|
+
```typescript
|
|
725
|
+
function stateControlMiddleware(
|
|
726
|
+
defaultState?: ResponseState | (() => ResponseState),
|
|
727
|
+
): (req: Request, res: Response, next: NextFunction) => void
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
- `defaultState` — The default state to use when no override is provided. Pass a function to have the default resolved per-request (a live getter) — required for `MockServer.setDefaultState()` to take effect after startup, since a plain object is captured once at middleware-creation time.
|
|
731
|
+
|
|
732
|
+
**Returns:** Express middleware function
|
|
733
|
+
|
|
734
|
+
#### `walkSchema(schema, fieldName, rng, index)`
|
|
735
|
+
|
|
736
|
+
Walk a serialized Zod schema definition and produce conformant data.
|
|
737
|
+
|
|
738
|
+
```typescript
|
|
739
|
+
function walkSchema(
|
|
740
|
+
schema: ZodSchemaDefinition,
|
|
741
|
+
fieldName: string,
|
|
742
|
+
rng: () => number,
|
|
743
|
+
index: number,
|
|
744
|
+
): unknown
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
- `schema` — The serialized schema definition
|
|
748
|
+
- `fieldName` — The field name (for semantic heuristics)
|
|
749
|
+
- `rng` — The seeded random function
|
|
750
|
+
- `index` — The item index for list generation
|
|
751
|
+
|
|
752
|
+
**Returns:** A conformant value matching the schema
|
|
753
|
+
|
|
754
|
+
### Constants
|
|
755
|
+
|
|
756
|
+
#### `DEFAULT_STATES`
|
|
757
|
+
|
|
758
|
+
Default response states for each scenario
|
|
759
|
+
|
|
760
|
+
```typescript
|
|
761
|
+
const DEFAULT_STATES: {
|
|
762
|
+
readonly success: { readonly state: 'success' }
|
|
763
|
+
readonly empty: { readonly state: 'empty' }
|
|
764
|
+
readonly error: { readonly state: 'error'; readonly statusCode: 500 }
|
|
765
|
+
readonly unauthorized: { readonly state: 'unauthorized'; readonly statusCode: 401 }
|
|
766
|
+
}
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
#### `defaultSemanticRules`
|
|
770
|
+
|
|
771
|
+
Default semantic rules that match field names to realistic generators.
|
|
772
|
+
Rules are checked in order; the first match wins.
|
|
773
|
+
|
|
774
|
+
```typescript
|
|
775
|
+
const defaultSemanticRules: SemanticRule[]
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
#### `FIXTURE_NOW`
|
|
779
|
+
|
|
780
|
+
Fixed "now" anchor (2026-04-01T12:00:00Z) for all generated dates.
|
|
781
|
+
|
|
782
|
+
Dates are deliberately anchored to a constant instead of the wall clock so
|
|
783
|
+
fixture output is byte-stable across runs (reliable screenshot diffs).
|
|
784
|
+
Consequence: "recent"/"future" are relative to this anchor, not the real
|
|
785
|
+
current time — never assert generated dates against `Date.now()`.
|
|
786
|
+
|
|
787
|
+
```typescript
|
|
788
|
+
const FIXTURE_NOW: Date
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
#### `MAX_MOCK_DELAY_MS`
|
|
792
|
+
|
|
793
|
+
Maximum delay, in ms, that {@link applyDelay} will actually wait — a
|
|
794
|
+
requested delay above this is clamped (and logged) rather than honored
|
|
795
|
+
verbatim. Guards against a stray oversized `?_delay` / `X-Mock-Delay` /
|
|
796
|
+
`defaultDelay` / `setState({ delay })` value (e.g. a units mistake
|
|
797
|
+
applying `*1000` twice) hanging a request until the CLIENT gives up —
|
|
798
|
+
which in an E2E harness presents as an inexplicable page timeout rather
|
|
799
|
+
than an obvious mock misconfiguration.
|
|
800
|
+
|
|
801
|
+
```typescript
|
|
802
|
+
const MAX_MOCK_DELAY_MS: 60000
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
## Injection Notes
|
|
806
|
+
|
|
807
|
+
### Requirements
|
|
808
|
+
|
|
809
|
+
Peer dependencies:
|
|
810
|
+
|
|
811
|
+
- `zod` >=4.0.0
|
|
812
|
+
|
|
813
|
+
### Runtime Dependencies
|
|
814
|
+
|
|
815
|
+
- `express`
|
|
816
|
+
- `zod`
|
|
817
|
+
|
|
818
|
+
The server uses deterministic seeded PRNG for stable fixture data, making
|
|
819
|
+
screenshot comparisons reliable. Fixture data comes from the JSON files in
|
|
820
|
+
`fixturesPath` (array files become CRUD resources; `reports`/`storefront`/
|
|
821
|
+
`admin` object files become sub-endpoint groups).
|
|
822
|
+
|
|
823
|
+
Omitting `fixturesPath` makes the server resolve
|
|
824
|
+
`mlcl/templates/apps/<appType>/api/fixtures/` by walking up from `process.cwd()`
|
|
825
|
+
— that only works inside the molecule workspace. In a scaffolded project,
|
|
826
|
+
always pass `fixturesPath`.
|
|
827
|
+
|
|
828
|
+
Requests to `/api/*` paths with no matching fixture endpoint return an empty
|
|
829
|
+
success (`200 []` for GET) so pages still render — the response carries an
|
|
830
|
+
`X-Mock-Unmatched: true` header so a typo'd endpoint can be told apart from
|
|
831
|
+
an endpoint that legitimately returned empty data. Similarly, an invalid
|
|
832
|
+
`?_state`/`X-Mock-State` value is ignored (the default state is served) but
|
|
833
|
+
labeled with an `X-Mock-Invalid-State` response header, so a typo'd state
|
|
834
|
+
control is detectable instead of silently looking like "state applied".
|
|
835
|
+
|
|
836
|
+
State precedence, per request, highest first: (1) an endpoint-level
|
|
837
|
+
`server.setState(key, state)` override, (2) a per-request `?_state` query
|
|
838
|
+
param / `X-Mock-State` header, (3) `server.setDefaultState(...)` / the
|
|
839
|
+
configured `defaultState`. A `setState()` override is PERSISTENT — a
|
|
840
|
+
forgotten override from an earlier test silently beats every later
|
|
841
|
+
`?_state` on that same endpoint. Calling `setState(key, { state: 'success'
|
|
842
|
+
})` again does NOT remove the override (it replaces it with one that looks
|
|
843
|
+
like the default, still outranking `?_state`); call `server.clearState(key)`
|
|
844
|
+
to actually remove it and hand control back to per-request/default state.
|
|
845
|
+
`setDefaultState()` only changes the fallback and never clears endpoint
|
|
846
|
+
overrides.
|
|
847
|
+
|
|
848
|
+
Response delay (`defaultDelay`, `?_delay`/`X-Mock-Delay`, or a `delay` in
|
|
849
|
+
`setState`/`setDefaultState`) is capped at `MAX_MOCK_DELAY_MS` (60s) — an
|
|
850
|
+
oversized value (e.g. a units mistake applying `*1000` twice) is clamped
|
|
851
|
+
and logged with `console.warn` instead of hanging the request until the
|
|
852
|
+
client gives up, which in an E2E harness reads as an inexplicable page
|
|
853
|
+
timeout rather than a mock misconfiguration.
|