@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 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 { MockOptions, MockOverrides, GenContext, Override } from '@ayepi/mock'
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?) => Record<string, Handler>`
68
+ ### `mockHandlers(spec, opts?) => MockHandlerBag<S, Exclude<EndpointName<S>, X>>`
66
69
 
67
- The handler bag for `server(spec, [implement(spec).handlers(bag)])`. One generated handler
68
- per endpoint. Use this when you want to mix mock handlers with real ones, or add
69
- middleware/options yourself.
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
- ### `mockServer(spec, opts?) => Server`
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
- `mockHandlers` returns a plain handler bag, so merge it with your real handlers (real keys
221
- win) and wrap the merged bag in a single `implement(api).handlers(...)` builder:
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 bag = { ...mockHandlers(api), ...real } // mocks fill the gaps; real overrides
229
- const app = server(api, [implement(api).handlers(bag)])
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, suitable
418
- * for `server(spec, [implement(spec).handlers(bag)])`.
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 bag = mockHandlers(spec, opts);
443
- return (0, _ayepi_core.server)(spec, [(0, _ayepi_core.implement)(spec).handlers(bag)]);
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
- * # Public option & context types
8
- *
9
- * The configuration surface ({@link MockOptions}) and the per-generation context
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
- * Build the handler bag for a spec — one generated handler per endpoint, suitable
88
- * for `server(spec, [implement(spec).handlers(bag)])`.
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: AnySpec, opts?: MockOptions): Record<string, MockHandler>;
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: AnySpec, opts?: MockOptions): Server<AnySpec>;
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
- * # Public option & context types
8
- *
9
- * The configuration surface ({@link MockOptions}) and the per-generation context
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
- * Build the handler bag for a spec — one generated handler per endpoint, suitable
88
- * for `server(spec, [implement(spec).handlers(bag)])`.
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: AnySpec, opts?: MockOptions): Record<string, MockHandler>;
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: AnySpec, opts?: MockOptions): Server<AnySpec>;
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, suitable
417
- * for `server(spec, [implement(spec).handlers(bag)])`.
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 bag = mockHandlers(spec, opts);
442
- return server(spec, [implement(spec).handlers(bag)]);
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.4.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.4.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.4.3",
50
- "@ayepi/core": "0.4.0"
49
+ "zod": "^4.6.7",
50
+ "@ayepi/core": "0.5.0"
51
51
  },
52
52
  "keywords": [
53
53
  "ayepi",