@schmock/core 2.1.0 → 2.1.1

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/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /// <reference path="./schmock.d.ts" />
1
2
  /**
2
3
  * Create a new Schmock mock instance with callable API.
3
4
  *
@@ -0,0 +1,692 @@
1
+ /**
2
+ * Schmock - Schema-driven mock API generator with callable API
3
+ * @packageDocumentation
4
+ */
5
+
6
+ declare namespace Schmock {
7
+ type JSONSchema7 = import("json-schema").JSONSchema7;
8
+ /**
9
+ * HTTP methods supported by Schmock
10
+ */
11
+ type HttpMethod =
12
+ | "GET"
13
+ | "POST"
14
+ | "PUT"
15
+ | "DELETE"
16
+ | "PATCH"
17
+ | "HEAD"
18
+ | "OPTIONS";
19
+
20
+ /**
21
+ * Route key format: 'METHOD /path'
22
+ *
23
+ * @example
24
+ * 'GET /users'
25
+ * 'POST /users/:id'
26
+ * 'DELETE /api/posts/:postId/comments/:commentId'
27
+ */
28
+ type RouteKey = `${HttpMethod} ${string}`;
29
+
30
+ /**
31
+ * Plugin interface for extending Schmock functionality
32
+ */
33
+ interface Plugin {
34
+ /** Unique plugin identifier */
35
+ name: string;
36
+ /** Plugin version (semver) */
37
+ version?: string;
38
+
39
+ /**
40
+ * Called once when the plugin is added via .pipe()
41
+ * Use this to register routes or configure the instance at setup time
42
+ * @param instance - The callable mock instance
43
+ */
44
+ install?(instance: CallableMockInstance): void;
45
+
46
+ /**
47
+ * Process the request through this plugin
48
+ * First plugin to set response becomes the generator, others transform
49
+ * @param context - Plugin context with request details
50
+ * @param response - Response from previous plugin (if any)
51
+ * @returns Updated context and response
52
+ */
53
+ process(context: PluginContext, response?: unknown): PluginResult | Promise<PluginResult>;
54
+
55
+ /**
56
+ * Called when an error occurs
57
+ * Can handle, transform, or suppress errors
58
+ * @param error - The error that occurred
59
+ * @param context - Plugin context
60
+ * @returns Modified error, response data, or void to continue error propagation
61
+ */
62
+ onError?(error: Error, context: PluginContext): Error | ResponseResult | void | Promise<Error | ResponseResult | void>;
63
+ }
64
+
65
+ /**
66
+ * Alias for response body type
67
+ */
68
+ type ResponseBody = unknown;
69
+
70
+ /**
71
+ * Result returned by plugin process method
72
+ */
73
+ interface PluginResult {
74
+ /** Updated context */
75
+ context: PluginContext;
76
+ /** Response data (if generated/modified) */
77
+ response?: unknown;
78
+ }
79
+
80
+ /**
81
+ * Context passed through plugin pipeline
82
+ */
83
+ interface PluginContext {
84
+ /** Request path */
85
+ path: string;
86
+ /** Matched route configuration */
87
+ route: RouteConfig;
88
+ /** HTTP method */
89
+ method: HttpMethod;
90
+ /** Route parameters */
91
+ params: Record<string, string>;
92
+ /** Query parameters */
93
+ query: Record<string, string>;
94
+ /** Request headers */
95
+ headers: Record<string, string>;
96
+ /** Request body */
97
+ body?: unknown;
98
+ /** Shared state between plugins for this request */
99
+ state: Map<string, unknown>;
100
+ /** Route-specific state */
101
+ routeState?: Record<string, unknown>;
102
+ }
103
+
104
+ // ===== Callable API Types =====
105
+
106
+ /**
107
+ * Global configuration options for the mock instance
108
+ */
109
+ interface GlobalConfig {
110
+ /** Base path prefix for all routes */
111
+ namespace?: string;
112
+ /** Response delay in ms, or [min, max] for random delay */
113
+ delay?: number | [number, number];
114
+ /** Enable debug mode for detailed logging */
115
+ debug?: boolean;
116
+ /** Initial shared state object */
117
+ state?: Record<string, unknown>;
118
+ /**
119
+ * Maximum number of requests retained in history (FIFO eviction).
120
+ * Defaults to unbounded; set this to cap memory growth in long-running servers.
121
+ */
122
+ maxHistorySize?: number;
123
+ }
124
+
125
+ /**
126
+ * Route-specific configuration options
127
+ */
128
+ interface RouteConfig {
129
+ /** MIME type for content type validation (auto-detected if not provided) */
130
+ contentType?: string;
131
+ /** Per-route response delay in ms, or [min, max] for random delay (overrides global) */
132
+ delay?: number | [number, number];
133
+ /** Additional route-specific options */
134
+ [key: string]: unknown;
135
+ }
136
+
137
+ /**
138
+ * Generator types that can be passed to route definitions
139
+ */
140
+ type Generator =
141
+ | GeneratorFunction
142
+ | StaticData
143
+ | JSONSchema7;
144
+
145
+ /**
146
+ * Function that generates responses
147
+ */
148
+ type GeneratorFunction = (context: RequestContext) => ResponseResult | Promise<ResponseResult>;
149
+
150
+ /**
151
+ * Static data (non-function) that gets returned as-is
152
+ */
153
+ type StaticData = string | number | boolean | null | undefined | Record<string, unknown> | unknown[];
154
+
155
+ /**
156
+ * Context passed to generator functions
157
+ */
158
+ interface RequestContext {
159
+ /** HTTP method */
160
+ method: HttpMethod;
161
+ /** Request path */
162
+ path: string;
163
+ /** Route parameters (e.g., :id) */
164
+ params: Record<string, string>;
165
+ /** Query string parameters */
166
+ query: Record<string, string>;
167
+ /** Request headers */
168
+ headers: Record<string, string>;
169
+ /** Request body (for POST, PUT, PATCH) */
170
+ body?: unknown;
171
+ /** Shared mutable state */
172
+ state: Record<string, unknown>;
173
+ }
174
+
175
+ /**
176
+ * Response result types:
177
+ * - Any value: returns as 200 OK
178
+ * - [status, body]: custom status with body
179
+ * - [status, body, headers]: custom status, body, and headers
180
+ */
181
+ type ResponseResult =
182
+ | ResponseBody
183
+ | [number, unknown]
184
+ | [number, unknown, Record<string, string>];
185
+
186
+ /**
187
+ * Response object returned by handle method
188
+ */
189
+ interface Response {
190
+ status: number;
191
+ body: unknown;
192
+ headers: Record<string, string>;
193
+ }
194
+
195
+ /**
196
+ * Options for handle method
197
+ */
198
+ interface RequestOptions {
199
+ headers?: Record<string, string>;
200
+ body?: unknown;
201
+ query?: Record<string, string>;
202
+ }
203
+
204
+ /**
205
+ * Record of a single request handled by the mock
206
+ */
207
+ interface RequestRecord {
208
+ /** HTTP method */
209
+ method: HttpMethod;
210
+ /** Request path (without namespace) */
211
+ path: string;
212
+ /** Extracted route parameters */
213
+ params: Record<string, string>;
214
+ /** Query parameters */
215
+ query: Record<string, string>;
216
+ /** Request headers */
217
+ headers: Record<string, string>;
218
+ /** Request body */
219
+ body: unknown;
220
+ /** Unix timestamp (ms) when request was handled */
221
+ timestamp: number;
222
+ /** Response returned for this request */
223
+ response: { status: number; body: unknown };
224
+ }
225
+
226
+ /**
227
+ * Main callable mock instance interface
228
+ */
229
+ interface CallableMockInstance {
230
+ /**
231
+ * Define a route by calling the instance directly
232
+ *
233
+ * @param route - Route pattern in format 'METHOD /path'
234
+ * @param generator - Response generator (function, static data, or schema)
235
+ * @param config - Route-specific configuration
236
+ * @returns The same instance for method chaining
237
+ *
238
+ * @example
239
+ * ```typescript
240
+ * const mock = schmock()
241
+ * mock('GET /users', () => [...users], { contentType: 'application/json' })
242
+ * mock('POST /users', userData, { contentType: 'application/json' })
243
+ * ```
244
+ */
245
+ (route: RouteKey, generator: Generator, config?: RouteConfig): CallableMockInstance;
246
+
247
+ /**
248
+ * Add a plugin to the pipeline
249
+ *
250
+ * @param plugin - Plugin to add to the pipeline
251
+ * @returns The same instance for method chaining
252
+ *
253
+ * @example
254
+ * ```typescript
255
+ * mock('GET /users', generator, config)
256
+ * .pipe(authPlugin())
257
+ * .pipe(corsPlugin())
258
+ * ```
259
+ */
260
+ pipe(plugin: Plugin): CallableMockInstance;
261
+
262
+ /**
263
+ * Handle a request and return a response
264
+ *
265
+ * @param method - HTTP method
266
+ * @param path - Request path
267
+ * @param options - Request options (headers, body, query)
268
+ * @returns Promise resolving to response object
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * const response = await mock.handle('GET', '/users', {
273
+ * headers: { 'Authorization': 'Bearer token' }
274
+ * })
275
+ * ```
276
+ */
277
+ handle(method: HttpMethod, path: string, options?: RequestOptions): Promise<Response>;
278
+
279
+ // ===== Request Spy / History API =====
280
+
281
+ /**
282
+ * Get all recorded requests, optionally filtered by method and path
283
+ *
284
+ * @param method - Filter by HTTP method
285
+ * @param path - Filter by request path
286
+ * @returns Array of request records
287
+ */
288
+ history(): RequestRecord[];
289
+ history(method: HttpMethod, path: string): RequestRecord[];
290
+
291
+ /**
292
+ * Check if any request was made, optionally for a specific route
293
+ *
294
+ * @param method - Filter by HTTP method
295
+ * @param path - Filter by request path
296
+ * @returns true if at least one matching request was recorded
297
+ */
298
+ called(): boolean;
299
+ called(method: HttpMethod, path: string): boolean;
300
+
301
+ /**
302
+ * Get the number of recorded requests, optionally for a specific route
303
+ *
304
+ * @param method - Filter by HTTP method
305
+ * @param path - Filter by request path
306
+ * @returns Number of matching requests
307
+ */
308
+ callCount(): number;
309
+ callCount(method: HttpMethod, path: string): number;
310
+
311
+ /**
312
+ * Get the most recent request, optionally for a specific route
313
+ *
314
+ * @param method - Filter by HTTP method
315
+ * @param path - Filter by request path
316
+ * @returns Most recent matching request record, or undefined
317
+ */
318
+ lastRequest(): RequestRecord | undefined;
319
+ lastRequest(method: HttpMethod, path: string): RequestRecord | undefined;
320
+
321
+ // ===== Reset / Lifecycle =====
322
+
323
+ /**
324
+ * Clear all routes, state, plugins, and history
325
+ */
326
+ reset(): void;
327
+
328
+ /**
329
+ * Clear only request history, keep routes and state
330
+ */
331
+ resetHistory(): void;
332
+
333
+ /**
334
+ * Clear only state, keep routes and history
335
+ */
336
+ resetState(): void;
337
+
338
+ // ===== Lifecycle Events =====
339
+
340
+ /**
341
+ * Register an event listener
342
+ */
343
+ on<E extends SchmockEvent>(event: E, listener: (data: SchmockEventMap[E]) => void): CallableMockInstance;
344
+
345
+ /**
346
+ * Remove an event listener
347
+ */
348
+ off<E extends SchmockEvent>(event: E, listener: (data: SchmockEventMap[E]) => void): CallableMockInstance;
349
+
350
+ // ===== Introspection =====
351
+
352
+ /**
353
+ * Get all registered routes as an array of route info objects
354
+ */
355
+ getRoutes(): RouteInfo[];
356
+
357
+ /**
358
+ * Get the current shared state object
359
+ */
360
+ getState(): Record<string, unknown>;
361
+
362
+ // ===== Standalone Server =====
363
+
364
+ /**
365
+ * Start a standalone HTTP server
366
+ *
367
+ * @param port - Port to listen on (0 for random)
368
+ * @param hostname - Hostname to bind to (default: "127.0.0.1")
369
+ * @returns Promise resolving to server info with actual port and hostname
370
+ * @throws If the server is already running
371
+ */
372
+ listen(port?: number, hostname?: string): Promise<ServerInfo>;
373
+
374
+ /**
375
+ * Stop the standalone server (idempotent, no-op if not running)
376
+ */
377
+ close(): void;
378
+
379
+ // ===== Fetch Interceptor =====
380
+
381
+ /**
382
+ * Intercept globalThis.fetch and route requests through this mock.
383
+ * Client-side equivalent of listen().
384
+ *
385
+ * @param options - Intercept configuration
386
+ * @returns Handle with restore() to stop intercepting
387
+ * @throws If already intercepting (call restore() first)
388
+ */
389
+ intercept(options?: InterceptOptions): InterceptHandle;
390
+ }
391
+
392
+ /**
393
+ * Information about a running standalone server
394
+ */
395
+ interface ServerInfo {
396
+ /** Port the server is listening on */
397
+ port: number;
398
+ /** Hostname the server is bound to */
399
+ hostname: string;
400
+ }
401
+
402
+ // ===== Response Helpers =====
403
+
404
+ interface PaginateOptions {
405
+ page?: number;
406
+ pageSize?: number;
407
+ }
408
+
409
+ interface PaginatedResponse<T> {
410
+ data: T[];
411
+ page: number;
412
+ pageSize: number;
413
+ total: number;
414
+ totalPages: number;
415
+ }
416
+
417
+ // ===== Adapter Types =====
418
+
419
+ interface AdapterRequest {
420
+ method: string;
421
+ path: string;
422
+ headers: Record<string, string>;
423
+ body?: unknown;
424
+ query: Record<string, string>;
425
+ }
426
+
427
+ interface AdapterResponse {
428
+ status: number;
429
+ body: unknown;
430
+ headers: Record<string, string>;
431
+ }
432
+
433
+ interface InterceptOptions {
434
+ /**
435
+ * Only intercept URLs matching this base.
436
+ *
437
+ * Two modes:
438
+ * - Path form ("/api"): match request pathnames whose prefix is the
439
+ * base path (with a segment-boundary check, so "/api" never
440
+ * matches "/apiv2").
441
+ * - Origin form ("https://api.example.com" or
442
+ * "https://api.example.com/v1"): require the request origin to
443
+ * match the base origin AND, if a base path is present, the
444
+ * request pathname to start with it. Relative-URL fetches
445
+ * (no origin) won't match an origin-form base.
446
+ */
447
+ baseUrl?: string;
448
+ /** Pass unmatched routes to real fetch (default: true) */
449
+ passthrough?: boolean;
450
+ /** Modify request before Schmock handles it */
451
+ beforeRequest?: (
452
+ request: AdapterRequest,
453
+ ) => AdapterRequest | void | Promise<AdapterRequest | void>;
454
+ /** Modify response before returning to caller */
455
+ beforeResponse?: (
456
+ response: AdapterResponse,
457
+ request: AdapterRequest,
458
+ ) => AdapterResponse | void | Promise<AdapterResponse | void>;
459
+ /** Format errors into custom response bodies */
460
+ errorFormatter?: (error: Error) => unknown;
461
+ }
462
+
463
+ interface InterceptHandle {
464
+ /** Stop intercepting and restore original fetch */
465
+ restore(): void;
466
+ /** Whether this interceptor is currently active */
467
+ readonly active: boolean;
468
+ }
469
+
470
+ // ===== Lifecycle Events =====
471
+
472
+ interface RequestStartEvent {
473
+ method: HttpMethod;
474
+ path: string;
475
+ headers: Record<string, string>;
476
+ }
477
+
478
+ interface RequestMatchEvent {
479
+ method: HttpMethod;
480
+ path: string;
481
+ routePath: string;
482
+ params: Record<string, string>;
483
+ }
484
+
485
+ interface RequestNotFoundEvent {
486
+ method: HttpMethod;
487
+ path: string;
488
+ }
489
+
490
+ interface RequestEndEvent {
491
+ method: HttpMethod;
492
+ path: string;
493
+ status: number;
494
+ duration: number;
495
+ }
496
+
497
+ type SchmockEventMap = {
498
+ "request:start": RequestStartEvent;
499
+ "request:match": RequestMatchEvent;
500
+ "request:notfound": RequestNotFoundEvent;
501
+ "request:end": RequestEndEvent;
502
+ };
503
+
504
+ type SchmockEvent = keyof SchmockEventMap;
505
+
506
+ // ===== Introspection Types =====
507
+
508
+ interface RouteInfo {
509
+ method: HttpMethod;
510
+ path: string;
511
+ hasParams: boolean;
512
+ }
513
+
514
+ // ===== OpenAPI Plugin Types =====
515
+
516
+ /**
517
+ * Per-resource configuration override for OpenAPI plugin
518
+ */
519
+ interface ResourceOverride {
520
+ /** Override: which property in list response holds the items array (e.g. "data") */
521
+ listWrapProperty?: string;
522
+ /** Override: force flat array for list (ignores any wrapper in the spec) */
523
+ listFlat?: boolean;
524
+ /** Override: JSON Schema for error responses (404, etc.) */
525
+ errorSchema?: JSONSchema7;
526
+ }
527
+
528
+ /**
529
+ * Response header definition from an OpenAPI spec
530
+ */
531
+ interface ResponseHeaderDef {
532
+ schema?: JSONSchema7;
533
+ description: string;
534
+ }
535
+
536
+ /**
537
+ * Per-operation metadata auto-detected from spec or set via overrides
538
+ */
539
+ interface CrudOperationMeta {
540
+ /** Full success response schema (wrapper + items) */
541
+ responseSchema?: JSONSchema7;
542
+ /** Response headers from spec */
543
+ responseHeaders?: Record<string, ResponseHeaderDef>;
544
+ /** Error response schemas keyed by status code */
545
+ errorSchemas?: Map<number, JSONSchema7>;
546
+ }
547
+
548
+ // ===== Faker Plugin Types =====
549
+
550
+ /**
551
+ * Context for schema-based data generation
552
+ */
553
+ interface SchemaGenerationContext {
554
+ schema: JSONSchema7;
555
+ count?: number;
556
+ overrides?: Record<string, unknown>;
557
+ params?: Record<string, string>;
558
+ state?: Record<string, unknown>;
559
+ query?: Record<string, string>;
560
+ seed?: number;
561
+ }
562
+
563
+ /**
564
+ * Options for the faker plugin
565
+ */
566
+ interface FakerPluginOptions {
567
+ schema: JSONSchema7;
568
+ count?: number;
569
+ overrides?: Record<string, unknown>;
570
+ seed?: number;
571
+ }
572
+
573
+ // ===== Express Adapter Types =====
574
+
575
+ /**
576
+ * Override parts of a request before Schmock handles it
577
+ */
578
+ interface AdapterRequestOverride {
579
+ method?: string;
580
+ path?: string;
581
+ headers?: Record<string, string>;
582
+ body?: unknown;
583
+ query?: Record<string, string>;
584
+ }
585
+
586
+ /**
587
+ * Configuration options for Express adapter
588
+ */
589
+ interface ExpressAdapterOptions {
590
+ errorFormatter?: (error: Error, req: unknown) => unknown;
591
+ passErrorsToNext?: boolean;
592
+ transformHeaders?: (
593
+ headers: Record<string, string | string[] | undefined>,
594
+ ) => Record<string, string>;
595
+ transformQuery?: (query: Record<string, unknown>) => Record<string, string>;
596
+ beforeRequest?: (
597
+ req: unknown,
598
+ res: unknown,
599
+ ) =>
600
+ | AdapterRequestOverride
601
+ | undefined
602
+ | Promise<AdapterRequestOverride | undefined>;
603
+ beforeResponse?: (
604
+ response: Response,
605
+ req: unknown,
606
+ res: unknown,
607
+ ) => Response | undefined | Promise<Response | undefined>;
608
+ }
609
+
610
+ // ===== Angular Adapter Types =====
611
+
612
+ /**
613
+ * Configuration options for Angular adapter
614
+ */
615
+ interface AngularAdapterOptions {
616
+ baseUrl?: string;
617
+ passthrough?: boolean;
618
+ errorFormatter?: (error: Error, request: unknown) => unknown;
619
+ transformRequest?: (request: unknown) => AdapterRequestOverride;
620
+ transformResponse?: (
621
+ response: Response,
622
+ request: unknown,
623
+ ) => Response;
624
+ }
625
+
626
+ // ===== OpenAPI Plugin Options =====
627
+
628
+ /**
629
+ * Options for the OpenAPI plugin
630
+ */
631
+ interface OpenApiOptions {
632
+ spec: string | object;
633
+ seed?: SeedConfig;
634
+ validateRequests?: boolean;
635
+ validateResponses?: boolean;
636
+ queryFeatures?: {
637
+ pagination?: boolean;
638
+ sorting?: boolean;
639
+ filtering?: boolean;
640
+ };
641
+ resources?: Record<string, ResourceOverride>;
642
+ debug?: boolean;
643
+ fakerSeed?: number;
644
+ security?: boolean;
645
+ /** Replace response schemas for specific routes. Key format: "METHOD /path" or "METHOD /path STATUS" */
646
+ schemas?: Record<string, import("json-schema").JSONSchema7>;
647
+ /** Called before generating a response body. Return a schema to replace the original, or void to keep it. */
648
+ onSchema?: (
649
+ schema: import("json-schema").JSONSchema7,
650
+ context: { method: string; path: string; params: Record<string, string>; query: Record<string, string>; headers: Record<string, string> },
651
+ ) => import("json-schema").JSONSchema7 | undefined;
652
+ }
653
+
654
+ /**
655
+ * Seed data source: inline array, file path, or auto-generate count
656
+ */
657
+ type SeedSource = unknown[] | string | { count: number };
658
+
659
+ /**
660
+ * Seed configuration mapping resource names to seed sources
661
+ */
662
+ type SeedConfig = Record<string, SeedSource>;
663
+
664
+ // ===== CLI Types =====
665
+
666
+ /**
667
+ * Options for the CLI server
668
+ */
669
+ interface CliOptions {
670
+ spec: string;
671
+ port?: number;
672
+ hostname?: string;
673
+ seed?: string;
674
+ cors?: boolean;
675
+ debug?: boolean;
676
+ fakerSeed?: number;
677
+ errors?: boolean;
678
+ watch?: boolean;
679
+ admin?: boolean;
680
+ }
681
+
682
+ /**
683
+ * Running CLI server instance
684
+ */
685
+ interface CliServer {
686
+ server: import("node:http").Server;
687
+ port: number;
688
+ hostname: string;
689
+ close(): void;
690
+ }
691
+
692
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@schmock/core",
3
3
  "description": "Core functionality for Schmock",
4
- "version": "2.1.0",
4
+ "version": "2.1.1",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "build": "bun build:lib && bun build:types",
22
22
  "build:lib": "bun build --minify --target node --outdir=dist src/index.ts",
23
- "build:types": "rm -f tsconfig.tsbuildinfo && tsc --build",
23
+ "build:types": "rm -f tsconfig.tsbuildinfo && tsc --build && node scripts/embed-ambient-types.js",
24
24
  "pretest": "rm -f src/*.js src/*.d.ts || true",
25
25
  "test": "vitest",
26
26
  "test:watch": "vitest --watch",