@ayepi/mock 0.4.0 → 0.5.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/README.md +2 -1
- package/ayepi-mock.md +40 -12
- package/dist/index.cjs +71 -7
- package/dist/index.d.cts +54 -22
- package/dist/index.d.ts +54 -22
- package/dist/index.js +71 -8
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -32,8 +32,9 @@ const res = await app.fetch(new Request('http://x/listUsers?limit=10', { method:
|
|
|
32
32
|
## Exports
|
|
33
33
|
|
|
34
34
|
- `generate(schema, opts?, ctx?)` — one fake value from a zod schema.
|
|
35
|
-
- `mockHandlers(spec, opts?)` — a handler bag for `server(spec, [implement(spec).handlers(bag)])
|
|
35
|
+
- `mockHandlers(spec, opts?)` — a handler bag typed per endpoint for `server(spec, [implement(spec).handlers(bag)])`; `except` leaves out the endpoints you implement for real.
|
|
36
36
|
- `mockServer(spec, opts?)` — a real ayepi `Server` with generated responses.
|
|
37
|
+
- `mockEvents(app, opts?)` — generated, schema-valid events through a server's `emit`, on demand or on a timer.
|
|
37
38
|
|
|
38
39
|
See [`ayepi-mock.md`](./ayepi-mock.md) for the full reference (options, seeding,
|
|
39
40
|
determinism, pagination, overrides, supported zod types, and gotchas).
|
package/ayepi-mock.md
CHANGED
|
@@ -40,8 +40,11 @@ const body = await res.json() // schema-valid, deterministic, 10 items
|
|
|
40
40
|
## Public API
|
|
41
41
|
|
|
42
42
|
```ts
|
|
43
|
-
import { generate, mockServer, mockHandlers } from '@ayepi/mock'
|
|
44
|
-
import type {
|
|
43
|
+
import { generate, mockServer, mockHandlers, mockEvents } from '@ayepi/mock'
|
|
44
|
+
import type {
|
|
45
|
+
MockOptions, MockOverrides, GenContext, Override,
|
|
46
|
+
MockHandlersOptions, MockHandlerBag, MockEventsOptions, MockEvents, EndpointName, EventName,
|
|
47
|
+
} from '@ayepi/mock'
|
|
45
48
|
```
|
|
46
49
|
|
|
47
50
|
### `generate(schema, opts?, ctx?) => unknown`
|
|
@@ -62,18 +65,36 @@ const user = generate(z.object({ id: z.uuid(), name: z.string(), age: z.int().mi
|
|
|
62
65
|
- `path` — starting path for override lookup (rarely needed).
|
|
63
66
|
- `rng` — supply your own `() => number` to fully control randomness (tests).
|
|
64
67
|
|
|
65
|
-
### `mockHandlers(spec, opts?) =>
|
|
68
|
+
### `mockHandlers(spec, opts?) => MockHandlerBag<S, Exclude<EndpointName<S>, X>>`
|
|
66
69
|
|
|
67
|
-
The handler bag for `server(spec, [implement(spec).handlers(bag)])
|
|
68
|
-
per endpoint
|
|
69
|
-
|
|
70
|
+
The handler bag for `server(spec, [implement(spec).handlers(bag)])`: one generated handler
|
|
71
|
+
per endpoint, **typed per endpoint** (`HandlerFor<S, S['endpoints'][K]>`), so it needs no
|
|
72
|
+
cast. `opts` is `MockOptions` plus `except?: readonly X[]` — endpoint names to leave out
|
|
73
|
+
(you implement them for real). The bag's type omits them, so `server()`'s missing-handler
|
|
74
|
+
compile check sees exactly which endpoints the mock covers and which still need a handler.
|
|
70
75
|
|
|
71
76
|
```ts
|
|
72
77
|
import { server, implement } from '@ayepi/core'
|
|
73
78
|
const app = server(api, [implement(api).handlers(mockHandlers(api, { seed: 1 }))])
|
|
74
79
|
```
|
|
75
80
|
|
|
76
|
-
### `
|
|
81
|
+
### `mockEvents(app, opts?) => MockEvents<S>`
|
|
82
|
+
|
|
83
|
+
Generated, schema-valid event traffic through a server's typed `emit` (any `Pick<Server<S>,
|
|
84
|
+
'spec' | 'emit'>`, real or mock). Each emission generates the event's channel params (when it
|
|
85
|
+
has a `params` schema) and its `data`. The n-th emission of an event is seeded from
|
|
86
|
+
`seed + 'event' + name + n`, so a run replays identically. `opts` is `MockOptions` plus
|
|
87
|
+
`only?: readonly EventName<S>[]` — the events `tick`/`start` pick from (default: all).
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const events = mockEvents(app, { seed: 1, only: ['userChanged'] })
|
|
91
|
+
events.emit('userChanged') // one generated event now (unknown names fail to compile; throw at runtime)
|
|
92
|
+
events.tick() // the next event from the chosen set, in order
|
|
93
|
+
events.start(1_000) // tick every second (restarts if already running)
|
|
94
|
+
events.stop()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `mockServer(spec, opts?) => Server<S>`
|
|
77
98
|
|
|
78
99
|
The convenience path: builds the handler bag and wires it into
|
|
79
100
|
`server(spec, [implement(spec).handlers(bag)])`, returning a real ayepi `Server`. Routing, **input validation**, **output validation**, and
|
|
@@ -217,16 +238,23 @@ word — pin those with a `fields`/`formats` override.
|
|
|
217
238
|
## Recipes
|
|
218
239
|
|
|
219
240
|
**Mix mock and real handlers** — implement the endpoints you have, mock the rest.
|
|
220
|
-
|
|
221
|
-
|
|
241
|
+
Name the real endpoints in `except`; the typed bag covers the rest, and `server()` checks at
|
|
242
|
+
compile time that the two builders together handle every endpoint exactly once:
|
|
222
243
|
|
|
223
244
|
```ts
|
|
224
245
|
import { implement, server } from '@ayepi/core'
|
|
225
246
|
import { mockHandlers } from '@ayepi/mock'
|
|
226
247
|
|
|
227
|
-
const real = { getUser: ({ data }) => loadUser(data.id) }
|
|
228
|
-
const
|
|
229
|
-
const app = server(api, [
|
|
248
|
+
const real = implement(api).handlers({ getUser: ({ data }) => loadUser(data.id) })
|
|
249
|
+
const mocked = implement(api).handlers(mockHandlers(api, { except: ['getUser'] }))
|
|
250
|
+
const app = server(api, [real, mocked])
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Realtime UI on a mock** — emit generated events on a timer:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
const app = mockServer(api, { seed: 1 })
|
|
257
|
+
mockEvents(app, { seed: 1 }).start(2_000)
|
|
230
258
|
```
|
|
231
259
|
|
|
232
260
|
**Stable demo data** — fix the seed and clock so screenshots never drift:
|
package/dist/index.cjs
CHANGED
|
@@ -363,14 +363,17 @@ function generate(schema, opts, ctx) {
|
|
|
363
363
|
//#region src/server.ts
|
|
364
364
|
/** Build a {@link GenContext} from a handler payload + endpoint name. */
|
|
365
365
|
function contextFor(name, payload, opts) {
|
|
366
|
-
const data = payload.data ?? {};
|
|
367
366
|
return {
|
|
368
367
|
path: "",
|
|
369
368
|
rng: makeRng(opts, name, JSON.stringify(payload.data ?? null)),
|
|
370
369
|
request: payload.data,
|
|
371
|
-
query: data
|
|
370
|
+
query: isRecord(payload.data) ? payload.data : {}
|
|
372
371
|
};
|
|
373
372
|
}
|
|
373
|
+
/** Whether `value` is a plain keyed object (the merged params/query/body record a handler receives). */
|
|
374
|
+
function isRecord(value) {
|
|
375
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
376
|
+
}
|
|
374
377
|
/** Read an endpoint config's schemas off its erased definition. */
|
|
375
378
|
function cfgOf(def) {
|
|
376
379
|
return def.cfg;
|
|
@@ -414,17 +417,23 @@ function handlerFor(name, def, opts) {
|
|
|
414
417
|
return () => void 0;
|
|
415
418
|
}
|
|
416
419
|
/**
|
|
417
|
-
* Build the handler bag for a spec — one generated handler per endpoint
|
|
418
|
-
*
|
|
420
|
+
* Build the handler bag for a spec — one generated handler per endpoint (minus any
|
|
421
|
+
* named in `except`), typed per endpoint, so it drops straight into
|
|
422
|
+
* `implement(spec).handlers(bag)` and `server()`'s missing-handler check sees exactly
|
|
423
|
+
* which endpoints it covers.
|
|
419
424
|
*
|
|
420
425
|
* @example
|
|
421
426
|
* ```ts
|
|
422
427
|
* const app = server(api, [implement(api).handlers(mockHandlers(api, { seed: 1 }))])
|
|
428
|
+
* // real handlers for some endpoints, generated data for the rest:
|
|
429
|
+
* const real = implement(api).handlers({ getUser: loadUser })
|
|
430
|
+
* const app2 = server(api, [real, implement(api).handlers(mockHandlers(api, { except: ['getUser'] }))])
|
|
423
431
|
* ```
|
|
424
432
|
*/
|
|
425
433
|
function mockHandlers(spec, opts) {
|
|
434
|
+
const except = new Set(opts?.except ?? []);
|
|
426
435
|
const bag = {};
|
|
427
|
-
for (const [name, def] of Object.entries(spec.endpoints)) bag[name] = handlerFor(name, def, opts);
|
|
436
|
+
for (const [name, def] of Object.entries(spec.endpoints)) if (!except.has(name)) bag[name] = handlerFor(name, def, opts);
|
|
428
437
|
return bag;
|
|
429
438
|
}
|
|
430
439
|
/**
|
|
@@ -439,10 +448,65 @@ function mockHandlers(spec, opts) {
|
|
|
439
448
|
* ```
|
|
440
449
|
*/
|
|
441
450
|
function mockServer(spec, opts) {
|
|
442
|
-
const
|
|
443
|
-
return (0, _ayepi_core.server)(
|
|
451
|
+
const erased = spec;
|
|
452
|
+
return (0, _ayepi_core.server)(erased, [(0, _ayepi_core.implement)(erased).handlers(mockHandlers(erased, opts))]);
|
|
453
|
+
}
|
|
454
|
+
//#endregion
|
|
455
|
+
//#region src/events.ts
|
|
456
|
+
/**
|
|
457
|
+
* Build a generated event emitter over `app` (any server for the spec, real or mock).
|
|
458
|
+
*
|
|
459
|
+
* @example
|
|
460
|
+
* ```ts
|
|
461
|
+
* const app = mockServer(api, { seed: 1 })
|
|
462
|
+
* const events = mockEvents(app, { seed: 1, only: ['userChanged'] })
|
|
463
|
+
* events.start(1_000) // a generated userChanged every second
|
|
464
|
+
* ```
|
|
465
|
+
*/
|
|
466
|
+
function mockEvents(app, opts) {
|
|
467
|
+
const gcfg = resolveConfig(opts);
|
|
468
|
+
const defs = app.spec.events ?? {};
|
|
469
|
+
const send = app.emit;
|
|
470
|
+
const names = opts?.only ?? Object.keys(defs);
|
|
471
|
+
const counts = /* @__PURE__ */ new Map();
|
|
472
|
+
let next = 0;
|
|
473
|
+
let timer;
|
|
474
|
+
const emit = (name) => {
|
|
475
|
+
const def = defs[name];
|
|
476
|
+
if (def === void 0) throw new Error(`mockEvents: the spec has no event "${name}"`);
|
|
477
|
+
const n = counts.get(name) ?? 0;
|
|
478
|
+
counts.set(name, n + 1);
|
|
479
|
+
const ctx = {
|
|
480
|
+
path: "",
|
|
481
|
+
rng: makeRng(opts, "event", name, String(n)),
|
|
482
|
+
request: void 0,
|
|
483
|
+
query: {}
|
|
484
|
+
};
|
|
485
|
+
const data = genValue(def.data, ctx, gcfg);
|
|
486
|
+
if (def.params) send(name, genValue(def.params, ctx, gcfg), data);
|
|
487
|
+
else send(name, data);
|
|
488
|
+
};
|
|
489
|
+
const stop = () => {
|
|
490
|
+
if (timer !== void 0) clearInterval(timer);
|
|
491
|
+
timer = void 0;
|
|
492
|
+
};
|
|
493
|
+
const tick = () => {
|
|
494
|
+
const name = names[next++ % Math.max(names.length, 1)];
|
|
495
|
+
if (name !== void 0) emit(name);
|
|
496
|
+
};
|
|
497
|
+
const start = (every) => {
|
|
498
|
+
stop();
|
|
499
|
+
timer = setInterval(tick, every);
|
|
500
|
+
};
|
|
501
|
+
return {
|
|
502
|
+
emit,
|
|
503
|
+
tick,
|
|
504
|
+
start,
|
|
505
|
+
stop
|
|
506
|
+
};
|
|
444
507
|
}
|
|
445
508
|
//#endregion
|
|
446
509
|
exports.generate = generate;
|
|
510
|
+
exports.mockEvents = mockEvents;
|
|
447
511
|
exports.mockHandlers = mockHandlers;
|
|
448
512
|
exports.mockServer = mockServer;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
+
import { AnySpec, EventsOf, HandlerFor, Server } from "@ayepi/core";
|
|
1
2
|
import { z } from "zod";
|
|
2
|
-
import { AnySpec, Server } from "@ayepi/core";
|
|
3
3
|
|
|
4
4
|
//#region src/types.d.ts
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* ({@link GenContext}) passed to override callbacks. Kept in their own module so
|
|
11
|
-
* both the generator and the server entry points can import them without a cycle.
|
|
12
|
-
*
|
|
13
|
-
* @module
|
|
14
|
-
*/
|
|
6
|
+
/** A spec's endpoint names. */
|
|
7
|
+
type EndpointName<S extends AnySpec> = keyof S['endpoints'] & string;
|
|
8
|
+
/** A spec's event names. */
|
|
9
|
+
type EventName<S extends AnySpec> = keyof EventsOf<S> & string;
|
|
15
10
|
/**
|
|
16
11
|
* Context handed to override callbacks (and threaded through the generator). It
|
|
17
12
|
* exposes where in the schema we are ({@link GenContext.path | path}), the seeded
|
|
@@ -58,9 +53,29 @@ interface MockOptions {
|
|
|
58
53
|
/** Clock used for date generation (injectable for tests). Default `Date.now`. */
|
|
59
54
|
readonly now?: () => number;
|
|
60
55
|
}
|
|
56
|
+
/** Options for {@link mockHandlers}: generation options plus the endpoints to leave out. */
|
|
57
|
+
interface MockHandlersOptions<X extends string = never> extends MockOptions {
|
|
58
|
+
/** Endpoints to skip (you implement them for real); the returned bag's type omits them. */
|
|
59
|
+
readonly except?: readonly X[];
|
|
60
|
+
}
|
|
61
|
+
/** Options for {@link mockEvents}: generation options plus which events to emit. */
|
|
62
|
+
interface MockEventsOptions<S extends AnySpec> extends MockOptions {
|
|
63
|
+
/** The events `tick`/`start` pick from. Default: every event in the spec. */
|
|
64
|
+
readonly only?: readonly EventName<S>[];
|
|
65
|
+
}
|
|
66
|
+
/** A generated event emitter over a server (see {@link mockEvents}). */
|
|
67
|
+
interface MockEvents<S extends AnySpec> {
|
|
68
|
+
/** Emit one generated event (params and data both generated) now. */
|
|
69
|
+
emit(name: EventName<S>): void;
|
|
70
|
+
/** Emit the next event from the chosen set, in order. Does nothing for a spec with no events. */
|
|
71
|
+
tick(): void;
|
|
72
|
+
/** Call {@link MockEvents.tick | tick} every `every` ms until {@link MockEvents.stop | stop}. Restarts if already running. */
|
|
73
|
+
start(every: number): void;
|
|
74
|
+
/** Stop the timer `start` began. Safe to call when not running. */
|
|
75
|
+
stop(): void;
|
|
76
|
+
}
|
|
61
77
|
//#endregion
|
|
62
78
|
//#region src/generate.d.ts
|
|
63
|
-
|
|
64
79
|
/**
|
|
65
80
|
* Generate one schema-valid fake value from a zod schema.
|
|
66
81
|
*
|
|
@@ -77,22 +92,26 @@ interface MockOptions {
|
|
|
77
92
|
declare function generate(schema: z.ZodType, opts?: MockOptions, ctx?: Partial<GenContext>): unknown;
|
|
78
93
|
//#endregion
|
|
79
94
|
//#region src/server.d.ts
|
|
80
|
-
/** A handler payload, narrowed to the parts the mock reads. */
|
|
81
|
-
interface MockPayload {
|
|
82
|
-
readonly data?: unknown;
|
|
83
|
-
}
|
|
84
|
-
/** A single generated mock handler (erased payload/return types). */
|
|
85
|
-
type MockHandler = (payload: MockPayload) => unknown;
|
|
86
95
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
96
|
+
* The handler bag {@link mockHandlers} returns, typed per endpoint: one handler for each
|
|
97
|
+
* endpoint name in `K`, each exactly what `implement(spec).handlers(...)` expects.
|
|
98
|
+
*/
|
|
99
|
+
type MockHandlerBag<S extends AnySpec, K extends EndpointName<S> = EndpointName<S>> = { readonly [P in K]: HandlerFor<S, S['endpoints'][P]> };
|
|
100
|
+
/**
|
|
101
|
+
* Build the handler bag for a spec — one generated handler per endpoint (minus any
|
|
102
|
+
* named in `except`), typed per endpoint, so it drops straight into
|
|
103
|
+
* `implement(spec).handlers(bag)` and `server()`'s missing-handler check sees exactly
|
|
104
|
+
* which endpoints it covers.
|
|
89
105
|
*
|
|
90
106
|
* @example
|
|
91
107
|
* ```ts
|
|
92
108
|
* const app = server(api, [implement(api).handlers(mockHandlers(api, { seed: 1 }))])
|
|
109
|
+
* // real handlers for some endpoints, generated data for the rest:
|
|
110
|
+
* const real = implement(api).handlers({ getUser: loadUser })
|
|
111
|
+
* const app2 = server(api, [real, implement(api).handlers(mockHandlers(api, { except: ['getUser'] }))])
|
|
93
112
|
* ```
|
|
94
113
|
*/
|
|
95
|
-
declare function mockHandlers(spec:
|
|
114
|
+
declare function mockHandlers<S extends AnySpec, const X extends EndpointName<S> = never>(spec: S, opts?: MockHandlersOptions<X>): MockHandlerBag<S, Exclude<EndpointName<S>, X>>;
|
|
96
115
|
/**
|
|
97
116
|
* Build a real ayepi {@link Server} for a spec where every endpoint returns
|
|
98
117
|
* generated, schema-valid data. Routing, input validation, output validation, and
|
|
@@ -104,6 +123,19 @@ declare function mockHandlers(spec: AnySpec, opts?: MockOptions): Record<string,
|
|
|
104
123
|
* const res = await app.fetch(new Request('http://x/listUsers?limit=10', { method: 'POST' }))
|
|
105
124
|
* ```
|
|
106
125
|
*/
|
|
107
|
-
declare function mockServer(spec:
|
|
126
|
+
declare function mockServer<S extends AnySpec>(spec: S, opts?: MockOptions): Server<S>;
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/events.d.ts
|
|
129
|
+
/**
|
|
130
|
+
* Build a generated event emitter over `app` (any server for the spec, real or mock).
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* const app = mockServer(api, { seed: 1 })
|
|
135
|
+
* const events = mockEvents(app, { seed: 1, only: ['userChanged'] })
|
|
136
|
+
* events.start(1_000) // a generated userChanged every second
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
declare function mockEvents<S extends AnySpec>(app: Pick<Server<S>, 'spec' | 'emit'>, opts?: MockEventsOptions<S>): MockEvents<S>;
|
|
108
140
|
//#endregion
|
|
109
|
-
export { type GenContext, type MockOptions, type MockOverrides, type Override, generate, mockHandlers, mockServer };
|
|
141
|
+
export { type EndpointName, type EventName, type GenContext, type MockEvents, type MockEventsOptions, type MockHandlerBag, type MockHandlersOptions, type MockOptions, type MockOverrides, type Override, generate, mockEvents, mockHandlers, mockServer };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
import { AnySpec, Server } from "@ayepi/core";
|
|
1
|
+
import { AnySpec, EventsOf, HandlerFor, Server } from "@ayepi/core";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
//#region src/types.d.ts
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* ({@link GenContext}) passed to override callbacks. Kept in their own module so
|
|
11
|
-
* both the generator and the server entry points can import them without a cycle.
|
|
12
|
-
*
|
|
13
|
-
* @module
|
|
14
|
-
*/
|
|
6
|
+
/** A spec's endpoint names. */
|
|
7
|
+
type EndpointName<S extends AnySpec> = keyof S['endpoints'] & string;
|
|
8
|
+
/** A spec's event names. */
|
|
9
|
+
type EventName<S extends AnySpec> = keyof EventsOf<S> & string;
|
|
15
10
|
/**
|
|
16
11
|
* Context handed to override callbacks (and threaded through the generator). It
|
|
17
12
|
* exposes where in the schema we are ({@link GenContext.path | path}), the seeded
|
|
@@ -58,9 +53,29 @@ interface MockOptions {
|
|
|
58
53
|
/** Clock used for date generation (injectable for tests). Default `Date.now`. */
|
|
59
54
|
readonly now?: () => number;
|
|
60
55
|
}
|
|
56
|
+
/** Options for {@link mockHandlers}: generation options plus the endpoints to leave out. */
|
|
57
|
+
interface MockHandlersOptions<X extends string = never> extends MockOptions {
|
|
58
|
+
/** Endpoints to skip (you implement them for real); the returned bag's type omits them. */
|
|
59
|
+
readonly except?: readonly X[];
|
|
60
|
+
}
|
|
61
|
+
/** Options for {@link mockEvents}: generation options plus which events to emit. */
|
|
62
|
+
interface MockEventsOptions<S extends AnySpec> extends MockOptions {
|
|
63
|
+
/** The events `tick`/`start` pick from. Default: every event in the spec. */
|
|
64
|
+
readonly only?: readonly EventName<S>[];
|
|
65
|
+
}
|
|
66
|
+
/** A generated event emitter over a server (see {@link mockEvents}). */
|
|
67
|
+
interface MockEvents<S extends AnySpec> {
|
|
68
|
+
/** Emit one generated event (params and data both generated) now. */
|
|
69
|
+
emit(name: EventName<S>): void;
|
|
70
|
+
/** Emit the next event from the chosen set, in order. Does nothing for a spec with no events. */
|
|
71
|
+
tick(): void;
|
|
72
|
+
/** Call {@link MockEvents.tick | tick} every `every` ms until {@link MockEvents.stop | stop}. Restarts if already running. */
|
|
73
|
+
start(every: number): void;
|
|
74
|
+
/** Stop the timer `start` began. Safe to call when not running. */
|
|
75
|
+
stop(): void;
|
|
76
|
+
}
|
|
61
77
|
//#endregion
|
|
62
78
|
//#region src/generate.d.ts
|
|
63
|
-
|
|
64
79
|
/**
|
|
65
80
|
* Generate one schema-valid fake value from a zod schema.
|
|
66
81
|
*
|
|
@@ -77,22 +92,26 @@ interface MockOptions {
|
|
|
77
92
|
declare function generate(schema: z.ZodType, opts?: MockOptions, ctx?: Partial<GenContext>): unknown;
|
|
78
93
|
//#endregion
|
|
79
94
|
//#region src/server.d.ts
|
|
80
|
-
/** A handler payload, narrowed to the parts the mock reads. */
|
|
81
|
-
interface MockPayload {
|
|
82
|
-
readonly data?: unknown;
|
|
83
|
-
}
|
|
84
|
-
/** A single generated mock handler (erased payload/return types). */
|
|
85
|
-
type MockHandler = (payload: MockPayload) => unknown;
|
|
86
95
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
96
|
+
* The handler bag {@link mockHandlers} returns, typed per endpoint: one handler for each
|
|
97
|
+
* endpoint name in `K`, each exactly what `implement(spec).handlers(...)` expects.
|
|
98
|
+
*/
|
|
99
|
+
type MockHandlerBag<S extends AnySpec, K extends EndpointName<S> = EndpointName<S>> = { readonly [P in K]: HandlerFor<S, S['endpoints'][P]> };
|
|
100
|
+
/**
|
|
101
|
+
* Build the handler bag for a spec — one generated handler per endpoint (minus any
|
|
102
|
+
* named in `except`), typed per endpoint, so it drops straight into
|
|
103
|
+
* `implement(spec).handlers(bag)` and `server()`'s missing-handler check sees exactly
|
|
104
|
+
* which endpoints it covers.
|
|
89
105
|
*
|
|
90
106
|
* @example
|
|
91
107
|
* ```ts
|
|
92
108
|
* const app = server(api, [implement(api).handlers(mockHandlers(api, { seed: 1 }))])
|
|
109
|
+
* // real handlers for some endpoints, generated data for the rest:
|
|
110
|
+
* const real = implement(api).handlers({ getUser: loadUser })
|
|
111
|
+
* const app2 = server(api, [real, implement(api).handlers(mockHandlers(api, { except: ['getUser'] }))])
|
|
93
112
|
* ```
|
|
94
113
|
*/
|
|
95
|
-
declare function mockHandlers(spec:
|
|
114
|
+
declare function mockHandlers<S extends AnySpec, const X extends EndpointName<S> = never>(spec: S, opts?: MockHandlersOptions<X>): MockHandlerBag<S, Exclude<EndpointName<S>, X>>;
|
|
96
115
|
/**
|
|
97
116
|
* Build a real ayepi {@link Server} for a spec where every endpoint returns
|
|
98
117
|
* generated, schema-valid data. Routing, input validation, output validation, and
|
|
@@ -104,6 +123,19 @@ declare function mockHandlers(spec: AnySpec, opts?: MockOptions): Record<string,
|
|
|
104
123
|
* const res = await app.fetch(new Request('http://x/listUsers?limit=10', { method: 'POST' }))
|
|
105
124
|
* ```
|
|
106
125
|
*/
|
|
107
|
-
declare function mockServer(spec:
|
|
126
|
+
declare function mockServer<S extends AnySpec>(spec: S, opts?: MockOptions): Server<S>;
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/events.d.ts
|
|
129
|
+
/**
|
|
130
|
+
* Build a generated event emitter over `app` (any server for the spec, real or mock).
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* const app = mockServer(api, { seed: 1 })
|
|
135
|
+
* const events = mockEvents(app, { seed: 1, only: ['userChanged'] })
|
|
136
|
+
* events.start(1_000) // a generated userChanged every second
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
declare function mockEvents<S extends AnySpec>(app: Pick<Server<S>, 'spec' | 'emit'>, opts?: MockEventsOptions<S>): MockEvents<S>;
|
|
108
140
|
//#endregion
|
|
109
|
-
export { type GenContext, type MockOptions, type MockOverrides, type Override, generate, mockHandlers, mockServer };
|
|
141
|
+
export { type EndpointName, type EventName, type GenContext, type MockEvents, type MockEventsOptions, type MockHandlerBag, type MockHandlersOptions, type MockOptions, type MockOverrides, type Override, generate, mockEvents, mockHandlers, mockServer };
|
package/dist/index.js
CHANGED
|
@@ -362,14 +362,17 @@ function generate(schema, opts, ctx) {
|
|
|
362
362
|
//#region src/server.ts
|
|
363
363
|
/** Build a {@link GenContext} from a handler payload + endpoint name. */
|
|
364
364
|
function contextFor(name, payload, opts) {
|
|
365
|
-
const data = payload.data ?? {};
|
|
366
365
|
return {
|
|
367
366
|
path: "",
|
|
368
367
|
rng: makeRng(opts, name, JSON.stringify(payload.data ?? null)),
|
|
369
368
|
request: payload.data,
|
|
370
|
-
query: data
|
|
369
|
+
query: isRecord(payload.data) ? payload.data : {}
|
|
371
370
|
};
|
|
372
371
|
}
|
|
372
|
+
/** Whether `value` is a plain keyed object (the merged params/query/body record a handler receives). */
|
|
373
|
+
function isRecord(value) {
|
|
374
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
375
|
+
}
|
|
373
376
|
/** Read an endpoint config's schemas off its erased definition. */
|
|
374
377
|
function cfgOf(def) {
|
|
375
378
|
return def.cfg;
|
|
@@ -413,17 +416,23 @@ function handlerFor(name, def, opts) {
|
|
|
413
416
|
return () => void 0;
|
|
414
417
|
}
|
|
415
418
|
/**
|
|
416
|
-
* Build the handler bag for a spec — one generated handler per endpoint
|
|
417
|
-
*
|
|
419
|
+
* Build the handler bag for a spec — one generated handler per endpoint (minus any
|
|
420
|
+
* named in `except`), typed per endpoint, so it drops straight into
|
|
421
|
+
* `implement(spec).handlers(bag)` and `server()`'s missing-handler check sees exactly
|
|
422
|
+
* which endpoints it covers.
|
|
418
423
|
*
|
|
419
424
|
* @example
|
|
420
425
|
* ```ts
|
|
421
426
|
* const app = server(api, [implement(api).handlers(mockHandlers(api, { seed: 1 }))])
|
|
427
|
+
* // real handlers for some endpoints, generated data for the rest:
|
|
428
|
+
* const real = implement(api).handlers({ getUser: loadUser })
|
|
429
|
+
* const app2 = server(api, [real, implement(api).handlers(mockHandlers(api, { except: ['getUser'] }))])
|
|
422
430
|
* ```
|
|
423
431
|
*/
|
|
424
432
|
function mockHandlers(spec, opts) {
|
|
433
|
+
const except = new Set(opts?.except ?? []);
|
|
425
434
|
const bag = {};
|
|
426
|
-
for (const [name, def] of Object.entries(spec.endpoints)) bag[name] = handlerFor(name, def, opts);
|
|
435
|
+
for (const [name, def] of Object.entries(spec.endpoints)) if (!except.has(name)) bag[name] = handlerFor(name, def, opts);
|
|
427
436
|
return bag;
|
|
428
437
|
}
|
|
429
438
|
/**
|
|
@@ -438,8 +447,62 @@ function mockHandlers(spec, opts) {
|
|
|
438
447
|
* ```
|
|
439
448
|
*/
|
|
440
449
|
function mockServer(spec, opts) {
|
|
441
|
-
const
|
|
442
|
-
return server(
|
|
450
|
+
const erased = spec;
|
|
451
|
+
return server(erased, [implement(erased).handlers(mockHandlers(erased, opts))]);
|
|
452
|
+
}
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/events.ts
|
|
455
|
+
/**
|
|
456
|
+
* Build a generated event emitter over `app` (any server for the spec, real or mock).
|
|
457
|
+
*
|
|
458
|
+
* @example
|
|
459
|
+
* ```ts
|
|
460
|
+
* const app = mockServer(api, { seed: 1 })
|
|
461
|
+
* const events = mockEvents(app, { seed: 1, only: ['userChanged'] })
|
|
462
|
+
* events.start(1_000) // a generated userChanged every second
|
|
463
|
+
* ```
|
|
464
|
+
*/
|
|
465
|
+
function mockEvents(app, opts) {
|
|
466
|
+
const gcfg = resolveConfig(opts);
|
|
467
|
+
const defs = app.spec.events ?? {};
|
|
468
|
+
const send = app.emit;
|
|
469
|
+
const names = opts?.only ?? Object.keys(defs);
|
|
470
|
+
const counts = /* @__PURE__ */ new Map();
|
|
471
|
+
let next = 0;
|
|
472
|
+
let timer;
|
|
473
|
+
const emit = (name) => {
|
|
474
|
+
const def = defs[name];
|
|
475
|
+
if (def === void 0) throw new Error(`mockEvents: the spec has no event "${name}"`);
|
|
476
|
+
const n = counts.get(name) ?? 0;
|
|
477
|
+
counts.set(name, n + 1);
|
|
478
|
+
const ctx = {
|
|
479
|
+
path: "",
|
|
480
|
+
rng: makeRng(opts, "event", name, String(n)),
|
|
481
|
+
request: void 0,
|
|
482
|
+
query: {}
|
|
483
|
+
};
|
|
484
|
+
const data = genValue(def.data, ctx, gcfg);
|
|
485
|
+
if (def.params) send(name, genValue(def.params, ctx, gcfg), data);
|
|
486
|
+
else send(name, data);
|
|
487
|
+
};
|
|
488
|
+
const stop = () => {
|
|
489
|
+
if (timer !== void 0) clearInterval(timer);
|
|
490
|
+
timer = void 0;
|
|
491
|
+
};
|
|
492
|
+
const tick = () => {
|
|
493
|
+
const name = names[next++ % Math.max(names.length, 1)];
|
|
494
|
+
if (name !== void 0) emit(name);
|
|
495
|
+
};
|
|
496
|
+
const start = (every) => {
|
|
497
|
+
stop();
|
|
498
|
+
timer = setInterval(tick, every);
|
|
499
|
+
};
|
|
500
|
+
return {
|
|
501
|
+
emit,
|
|
502
|
+
tick,
|
|
503
|
+
start,
|
|
504
|
+
stop
|
|
505
|
+
};
|
|
443
506
|
}
|
|
444
507
|
//#endregion
|
|
445
|
-
export { generate, mockHandlers, mockServer };
|
|
508
|
+
export { generate, mockEvents, mockHandlers, mockServer };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ayepi/mock",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Mock server for @ayepi/core — generate schema-valid fake data from a spec, with deterministic seeding and automatic pagination",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -38,16 +38,16 @@
|
|
|
38
38
|
"node": ">=18"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"zod": "^4",
|
|
42
|
-
"@ayepi/core": "^0.
|
|
41
|
+
"zod": "^4.6.7",
|
|
42
|
+
"@ayepi/core": "^0.5.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@vitest/coverage-v8": "^2.1.8",
|
|
46
46
|
"publint": "^0.3.0",
|
|
47
47
|
"tsdown": "^0.12.0",
|
|
48
48
|
"vitest": "^2.1.8",
|
|
49
|
-
"zod": "^4.
|
|
50
|
-
"@ayepi/core": "0.
|
|
49
|
+
"zod": "^4.6.7",
|
|
50
|
+
"@ayepi/core": "0.5.0"
|
|
51
51
|
},
|
|
52
52
|
"keywords": [
|
|
53
53
|
"ayepi",
|