@schmock/core 1.9.0 → 1.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/builder.d.ts CHANGED
@@ -39,45 +39,5 @@ export declare class CallableMockInstance {
39
39
  * @private
40
40
  */
41
41
  private applyDelay;
42
- /**
43
- * Parse and normalize response result into Response object
44
- * Handles tuple format [status, body, headers], direct values, and response objects
45
- * @param result - Raw result from generator or plugin
46
- * @param routeConfig - Route configuration for content-type defaults
47
- * @returns Normalized Response object with status, body, and headers
48
- * @private
49
- */
50
- private parseResponse;
51
- /**
52
- * Run all registered plugins in sequence
53
- * First plugin to set response becomes generator, subsequent plugins transform
54
- * Handles plugin errors via onError hooks
55
- * @param context - Plugin context with request details
56
- * @param initialResponse - Initial response from route generator
57
- * @param _routeConfig - Route config (unused but kept for signature)
58
- * @param _requestId - Request ID (unused but kept for signature)
59
- * @returns Updated context and final response after all plugins
60
- * @private
61
- */
62
- private runPluginPipeline;
63
- /**
64
- * Find a route that matches the given method and path
65
- * Uses two-pass matching: static routes first, then parameterized routes
66
- * Matches routes in registration order (first registered wins)
67
- * @param method - HTTP method to match
68
- * @param path - Request path to match
69
- * @returns Matched compiled route or undefined if no match
70
- * @private
71
- */
72
- private findRoute;
73
- /**
74
- * Extract parameter values from path based on route pattern
75
- * Maps capture groups from regex match to parameter names
76
- * @param route - Compiled route with pattern and param names
77
- * @param path - Request path to extract values from
78
- * @returns Object mapping parameter names to extracted values
79
- * @private
80
- */
81
- private extractParams;
82
42
  }
83
43
  //# sourceMappingURL=builder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAkFA;;;;GAIG;AACH,qBAAa,oBAAoB;IAYnB,OAAO,CAAC,YAAY;IAXhC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,WAAW,CAA2C;IAC9D,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,UAAU,CAAiC;IAEnD,OAAO,CAAC,SAAS,CAAoC;gBAEjC,YAAY,GAAE,OAAO,CAAC,YAAiB;IAa3D,WAAW,CACT,KAAK,EAAE,OAAO,CAAC,QAAQ,EACvB,SAAS,EAAE,OAAO,CAAC,SAAS,EAC5B,MAAM,EAAE,OAAO,CAAC,WAAW,GAC1B,IAAI;IAqFP,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,oBAAoB,GAAG,IAAI;IAIvD,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI;IAoBlC,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE;IAS5E,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO;IAS3D,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM;IAS7D,WAAW,CACT,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAC3B,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,GAAG,SAAS;IAYpC,SAAS,IAAI,OAAO,CAAC,SAAS,EAAE;IAQhC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAMnC,EAAE,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,EAC/B,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GACnD,IAAI;IAUP,GAAG,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,EAChC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GACnD,IAAI;IAKP,OAAO,CAAC,IAAI;IAWZ,KAAK,IAAI,IAAI;IAeb,YAAY,IAAI,IAAI;IAKpB,UAAU,IAAI,IAAI;IAWlB,MAAM,CAAC,IAAI,SAAI,EAAE,QAAQ,SAAc,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC;IAuCrE,KAAK,IAAI,IAAI;IAUP,MAAM,CACV,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,GAC/B,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;IAgN5B;;;;OAIG;YACW,UAAU;IAgBxB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IAwDrB;;;;;;;;;;OAUG;YACW,iBAAiB;IAqG/B;;;;;;;;OAQG;IACH,OAAO,CAAC,SAAS;IA0BjB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;CActB"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAuDA;;;;GAIG;AACH,qBAAa,oBAAoB;IAYnB,OAAO,CAAC,YAAY;IAXhC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,WAAW,CAA2C;IAC9D,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,UAAU,CAAiC;IAEnD,OAAO,CAAC,SAAS,CAAoC;gBAEjC,YAAY,GAAE,OAAO,CAAC,YAAiB;IAa3D,WAAW,CACT,KAAK,EAAE,OAAO,CAAC,QAAQ,EACvB,SAAS,EAAE,OAAO,CAAC,SAAS,EAC5B,MAAM,EAAE,OAAO,CAAC,WAAW,GAC1B,IAAI;IAiFP,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,oBAAoB,GAAG,IAAI;IAIvD,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI;IAoBlC,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE;IAS5E,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO;IAS3D,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM;IAS7D,WAAW,CACT,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAC3B,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,GAAG,SAAS;IAYpC,SAAS,IAAI,OAAO,CAAC,SAAS,EAAE;IAQhC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAMnC,EAAE,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,EAC/B,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GACnD,IAAI;IAUP,GAAG,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,EAChC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GACnD,IAAI;IAKP,OAAO,CAAC,IAAI;IAWZ,KAAK,IAAI,IAAI;IAeb,YAAY,IAAI,IAAI;IAKpB,UAAU,IAAI,IAAI;IAWlB,MAAM,CAAC,IAAI,SAAI,EAAE,QAAQ,SAAc,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC;IAqDrE,KAAK,IAAI,IAAI;IAUP,MAAM,CACV,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,GAC/B,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;IAoO5B;;;;OAIG;YACW,UAAU;CAezB"}
package/dist/builder.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { createServer } from "node:http";
2
- import { isStatusTuple, toHttpMethod } from "./constants.js";
3
- import { PluginError, RouteDefinitionError, RouteNotFoundError, SchmockError, } from "./errors.js";
2
+ import { normalizePath, toHttpMethod } from "./constants.js";
3
+ import { errorMessage, RouteDefinitionError, RouteNotFoundError, SchmockError, } from "./errors.js";
4
4
  import { collectBody, parseNodeHeaders, parseNodeQuery, writeSchmockResponse, } from "./http-helpers.js";
5
5
  import { parseRouteKey } from "./parser.js";
6
- function errorMessage(error) {
7
- return error instanceof Error ? error.message : "Unknown error";
8
- }
6
+ import { runPluginPipeline } from "./plugin-pipeline.js";
7
+ import { parseResponse } from "./response-parser.js";
8
+ import { extractParams, findRoute, isGeneratorFunction, } from "./route-matcher.js";
9
9
  /**
10
10
  * Debug logger that respects debug mode configuration
11
11
  */
@@ -37,15 +37,6 @@ class DebugLogger {
37
37
  console.timeEnd(`[SCHMOCK] ${label}`);
38
38
  }
39
39
  }
40
- function isGeneratorFunction(gen) {
41
- return typeof gen === "function";
42
- }
43
- function isResponseObject(value) {
44
- return (typeof value === "object" &&
45
- value !== null &&
46
- "status" in value &&
47
- "body" in value);
48
- }
49
40
  /**
50
41
  * Callable mock instance that implements the new API.
51
42
  *
@@ -128,10 +119,7 @@ export class CallableMockInstance {
128
119
  // Store static routes (no params) in Map for O(1) lookup
129
120
  // Only store the first registration — "first registration wins" semantics
130
121
  if (parsed.params.length === 0) {
131
- const normalizedPath = parsed.path.endsWith("/") && parsed.path !== "/"
132
- ? parsed.path.slice(0, -1)
133
- : parsed.path;
134
- const key = `${parsed.method} ${normalizedPath}`;
122
+ const key = `${parsed.method} ${normalizePath(parsed.path)}`;
135
123
  if (!this.staticRoutes.has(key)) {
136
124
  this.staticRoutes.set(key, compiledRoute);
137
125
  }
@@ -161,26 +149,26 @@ export class CallableMockInstance {
161
149
  }
162
150
  // ===== Request Spy / History API =====
163
151
  history(method, path) {
164
- if (method && path) {
165
- return this.requestHistory.filter((r) => r.method === method && r.path === path);
152
+ if (method || path) {
153
+ return this.requestHistory.filter((r) => (!method || r.method === method) && (!path || r.path === path));
166
154
  }
167
155
  return [...this.requestHistory];
168
156
  }
169
157
  called(method, path) {
170
- if (method && path) {
171
- return this.requestHistory.some((r) => r.method === method && r.path === path);
158
+ if (method || path) {
159
+ return this.requestHistory.some((r) => (!method || r.method === method) && (!path || r.path === path));
172
160
  }
173
161
  return this.requestHistory.length > 0;
174
162
  }
175
163
  callCount(method, path) {
176
- if (method && path) {
177
- return this.requestHistory.filter((r) => r.method === method && r.path === path).length;
164
+ if (method || path) {
165
+ return this.requestHistory.filter((r) => (!method || r.method === method) && (!path || r.path === path)).length;
178
166
  }
179
167
  return this.requestHistory.length;
180
168
  }
181
169
  lastRequest(method, path) {
182
- if (method && path) {
183
- const filtered = this.requestHistory.filter((r) => r.method === method && r.path === path);
170
+ if (method || path) {
171
+ const filtered = this.requestHistory.filter((r) => (!method || r.method === method) && (!path || r.path === path));
184
172
  return filtered[filtered.length - 1];
185
173
  }
186
174
  return this.requestHistory[this.requestHistory.length - 1];
@@ -194,7 +182,7 @@ export class CallableMockInstance {
194
182
  }));
195
183
  }
196
184
  getState() {
197
- return this.globalConfig.state || {};
185
+ return { ...(this.globalConfig.state || {}) };
198
186
  }
199
187
  // ===== Lifecycle Events =====
200
188
  on(event, listener) {
@@ -251,14 +239,29 @@ export class CallableMockInstance {
251
239
  throw new SchmockError("Server is already running", "SERVER_ALREADY_RUNNING");
252
240
  }
253
241
  const httpServer = createServer((req, res) => {
254
- const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
255
- const method = toHttpMethod(req.method ?? "GET");
256
- const path = url.pathname;
257
- const headers = parseNodeHeaders(req);
258
- const query = parseNodeQuery(url);
259
- void collectBody(req, headers).then((body) => this.handle(method, path, { headers, body, query }).then((schmockResponse) => {
242
+ const handleRequest = async () => {
243
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
244
+ const method = toHttpMethod(req.method ?? "GET");
245
+ const path = url.pathname;
246
+ const headers = parseNodeHeaders(req);
247
+ const query = parseNodeQuery(url);
248
+ const body = await collectBody(req, headers);
249
+ const schmockResponse = await this.handle(method, path, {
250
+ headers,
251
+ body,
252
+ query,
253
+ });
260
254
  writeSchmockResponse(res, schmockResponse);
261
- }));
255
+ };
256
+ handleRequest().catch((error) => {
257
+ if (!res.headersSent) {
258
+ res.writeHead(500, { "content-type": "application/json" });
259
+ }
260
+ res.end(JSON.stringify({
261
+ error: error instanceof Error ? error.message : "Internal Server Error",
262
+ code: "SERVER_ERROR",
263
+ }));
264
+ });
262
265
  });
263
266
  this.server = httpServer;
264
267
  return new Promise((resolve, reject) => {
@@ -282,18 +285,20 @@ export class CallableMockInstance {
282
285
  this.logger.log("server", "Server stopped");
283
286
  }
284
287
  async handle(method, path, options) {
285
- const requestId = crypto.randomUUID();
286
288
  const handleStart = performance.now();
289
+ const requestId = this.globalConfig.debug ? crypto.randomUUID() : "";
290
+ const reqQuery = options?.query || {};
291
+ const reqHeaders = options?.headers || {};
287
292
  this.logger.log("request", `[${requestId}] ${method} ${path}`, {
288
- headers: options?.headers,
289
- query: options?.query,
293
+ headers: reqHeaders,
294
+ query: reqQuery,
290
295
  bodyType: options?.body ? typeof options.body : "none",
291
296
  });
292
297
  this.logger.time(`request-${requestId}`);
293
298
  this.emit("request:start", {
294
299
  method,
295
300
  path,
296
- headers: options?.headers || {},
301
+ headers: reqHeaders,
297
302
  });
298
303
  try {
299
304
  // Apply namespace if configured
@@ -316,6 +321,12 @@ export class CallableMockInstance {
316
321
  body: { error: error.message, code: error.code },
317
322
  headers: {},
318
323
  };
324
+ this.emit("request:end", {
325
+ method,
326
+ path,
327
+ status: 404,
328
+ duration: performance.now() - handleStart,
329
+ });
319
330
  this.logger.timeEnd(`request-${requestId}`);
320
331
  return response;
321
332
  }
@@ -324,7 +335,7 @@ export class CallableMockInstance {
324
335
  requestPath = stripped.startsWith("/") ? stripped : `/${stripped}`;
325
336
  }
326
337
  // Find matching route
327
- const matchedRoute = this.findRoute(method, requestPath);
338
+ const matchedRoute = findRoute(method, requestPath, this.staticRoutes, this.routes);
328
339
  if (!matchedRoute) {
329
340
  this.logger.log("route", `[${requestId}] No route found for ${method} ${requestPath}`);
330
341
  this.emit("request:notfound", { method, path: requestPath });
@@ -345,7 +356,7 @@ export class CallableMockInstance {
345
356
  }
346
357
  this.logger.log("route", `[${requestId}] Matched route: ${method} ${matchedRoute.path}`);
347
358
  // Extract parameters from the matched route
348
- const params = this.extractParams(matchedRoute, requestPath);
359
+ const params = extractParams(matchedRoute, requestPath);
349
360
  this.emit("request:match", {
350
361
  method,
351
362
  path: requestPath,
@@ -357,8 +368,8 @@ export class CallableMockInstance {
357
368
  method,
358
369
  path: requestPath,
359
370
  params,
360
- query: options?.query || {},
361
- headers: options?.headers || {},
371
+ query: reqQuery,
372
+ headers: reqHeaders,
362
373
  body: options?.body,
363
374
  state: this.globalConfig.state || {},
364
375
  };
@@ -375,15 +386,15 @@ export class CallableMockInstance {
375
386
  route: matchedRoute.config,
376
387
  method,
377
388
  params,
378
- query: options?.query || {},
379
- headers: options?.headers || {},
389
+ query: reqQuery,
390
+ headers: reqHeaders,
380
391
  body: options?.body,
381
392
  state: new Map(),
382
393
  routeState: this.globalConfig.state || {},
383
394
  };
384
395
  // Run plugin pipeline to transform the response
385
396
  try {
386
- const pipelineResult = await this.runPluginPipeline(pluginContext, result, matchedRoute.config, requestId);
397
+ const pipelineResult = await runPluginPipeline(this.plugins, pluginContext, result, this.logger);
387
398
  pluginContext = pipelineResult.context;
388
399
  result = pipelineResult.response;
389
400
  }
@@ -392,7 +403,7 @@ export class CallableMockInstance {
392
403
  throw error;
393
404
  }
394
405
  // Parse and prepare response
395
- const response = this.parseResponse(result, matchedRoute.config);
406
+ const response = parseResponse(result, matchedRoute.config);
396
407
  // Apply delay (route-level overrides global)
397
408
  await this.applyDelay(matchedRoute.config.delay);
398
409
  // Record request in history
@@ -400,8 +411,8 @@ export class CallableMockInstance {
400
411
  method,
401
412
  path: requestPath,
402
413
  params,
403
- query: options?.query || {},
404
- headers: options?.headers || {},
414
+ query: reqQuery,
415
+ headers: reqHeaders,
405
416
  body: options?.body,
406
417
  timestamp: Date.now(),
407
418
  response: { status: response.status, body: response.body },
@@ -434,6 +445,12 @@ export class CallableMockInstance {
434
445
  };
435
446
  // Apply delay even for error responses
436
447
  await this.applyDelay();
448
+ this.emit("request:end", {
449
+ method,
450
+ path,
451
+ status: 500,
452
+ duration: performance.now() - handleStart,
453
+ });
437
454
  this.logger.log("error", `[${requestId}] Returning error response 500`);
438
455
  this.logger.timeEnd(`request-${requestId}`);
439
456
  return errorResponse;
@@ -455,173 +472,4 @@ export class CallableMockInstance {
455
472
  : effectiveDelay;
456
473
  await new Promise((resolve) => setTimeout(resolve, ms));
457
474
  }
458
- /**
459
- * Parse and normalize response result into Response object
460
- * Handles tuple format [status, body, headers], direct values, and response objects
461
- * @param result - Raw result from generator or plugin
462
- * @param routeConfig - Route configuration for content-type defaults
463
- * @returns Normalized Response object with status, body, and headers
464
- * @private
465
- */
466
- parseResponse(result, routeConfig) {
467
- let status = 200;
468
- let body = result;
469
- let headers = {};
470
- let tupleFormat = false;
471
- // Handle already-formed response objects (from plugin error recovery)
472
- if (isResponseObject(result)) {
473
- return {
474
- status: result.status,
475
- body: result.body,
476
- headers: result.headers || {},
477
- };
478
- }
479
- // Handle tuple response format [status, body, headers?]
480
- if (isStatusTuple(result)) {
481
- [status, body, headers = {}] = result;
482
- tupleFormat = true;
483
- }
484
- // Handle null/undefined responses with 204 No Content
485
- // But don't auto-convert if tuple format was used (status was explicitly provided)
486
- if (body === null || body === undefined) {
487
- if (!tupleFormat) {
488
- status = status === 200 ? 204 : status; // Only change to 204 if status wasn't explicitly set via tuple
489
- }
490
- body = undefined; // Ensure body is undefined for null responses
491
- }
492
- // Add content-type header from route config if it exists and headers don't already have it
493
- // But only if this isn't a tuple response (where headers are explicitly controlled)
494
- if (!headers["content-type"] && routeConfig.contentType && !tupleFormat) {
495
- headers["content-type"] = routeConfig.contentType;
496
- // Handle special conversion cases when contentType is explicitly set
497
- if (routeConfig.contentType === "text/plain" && body !== undefined) {
498
- if (typeof body === "object" && !Buffer.isBuffer(body)) {
499
- body = JSON.stringify(body);
500
- }
501
- else if (typeof body !== "string") {
502
- body = String(body);
503
- }
504
- }
505
- }
506
- return {
507
- status,
508
- body,
509
- headers,
510
- };
511
- }
512
- /**
513
- * Run all registered plugins in sequence
514
- * First plugin to set response becomes generator, subsequent plugins transform
515
- * Handles plugin errors via onError hooks
516
- * @param context - Plugin context with request details
517
- * @param initialResponse - Initial response from route generator
518
- * @param _routeConfig - Route config (unused but kept for signature)
519
- * @param _requestId - Request ID (unused but kept for signature)
520
- * @returns Updated context and final response after all plugins
521
- * @private
522
- */
523
- async runPluginPipeline(context, initialResponse, _routeConfig, _requestId) {
524
- let currentContext = context;
525
- let response = initialResponse;
526
- this.logger.log("pipeline", `Running plugin pipeline for ${this.plugins.length} plugins`);
527
- for (const plugin of this.plugins) {
528
- this.logger.log("pipeline", `Processing plugin: ${plugin.name}`);
529
- try {
530
- const result = await plugin.process(currentContext, response);
531
- if (!result || !result.context) {
532
- throw new Error(`Plugin ${plugin.name} didn't return valid result`);
533
- }
534
- currentContext = result.context;
535
- // First plugin to set response becomes the generator
536
- if (result.response !== undefined &&
537
- (response === undefined || response === null)) {
538
- this.logger.log("pipeline", `Plugin ${plugin.name} generated response`);
539
- response = result.response;
540
- }
541
- else if (result.response !== undefined && response !== undefined) {
542
- this.logger.log("pipeline", `Plugin ${plugin.name} transformed response`);
543
- response = result.response;
544
- }
545
- }
546
- catch (error) {
547
- this.logger.log("pipeline", `Plugin ${plugin.name} failed: ${errorMessage(error)}`);
548
- // Try error handling if plugin has onError hook
549
- if (plugin.onError) {
550
- try {
551
- const pluginError = error instanceof Error ? error : new Error(errorMessage(error));
552
- const errorResult = await plugin.onError(pluginError, currentContext);
553
- if (errorResult) {
554
- this.logger.log("pipeline", `Plugin ${plugin.name} handled error`);
555
- // Error return → transform the thrown error
556
- if (errorResult instanceof Error) {
557
- throw new PluginError(plugin.name, errorResult);
558
- }
559
- // ResponseResult return → recover, stop pipeline
560
- if (typeof errorResult === "object" &&
561
- errorResult !== null &&
562
- "status" in errorResult) {
563
- response = errorResult;
564
- break;
565
- }
566
- }
567
- // void/falsy return → propagate original error below
568
- }
569
- catch (hookError) {
570
- // If the hook itself threw (including our PluginError above), re-throw it
571
- if (hookError instanceof PluginError) {
572
- throw hookError;
573
- }
574
- this.logger.log("pipeline", `Plugin ${plugin.name} error handler failed: ${errorMessage(hookError)}`);
575
- }
576
- }
577
- const cause = error instanceof Error ? error : new Error(errorMessage(error));
578
- throw new PluginError(plugin.name, cause);
579
- }
580
- }
581
- return { context: currentContext, response };
582
- }
583
- /**
584
- * Find a route that matches the given method and path
585
- * Uses two-pass matching: static routes first, then parameterized routes
586
- * Matches routes in registration order (first registered wins)
587
- * @param method - HTTP method to match
588
- * @param path - Request path to match
589
- * @returns Matched compiled route or undefined if no match
590
- * @private
591
- */
592
- findRoute(method, path) {
593
- // O(1) lookup for static routes
594
- const normalizedPath = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
595
- const staticMatch = this.staticRoutes.get(`${method} ${normalizedPath}`);
596
- if (staticMatch) {
597
- return staticMatch;
598
- }
599
- // Fall through to parameterized route scan
600
- for (const route of this.routes) {
601
- if (route.method === method &&
602
- route.params.length > 0 &&
603
- route.pattern.test(path)) {
604
- return route;
605
- }
606
- }
607
- return undefined;
608
- }
609
- /**
610
- * Extract parameter values from path based on route pattern
611
- * Maps capture groups from regex match to parameter names
612
- * @param route - Compiled route with pattern and param names
613
- * @param path - Request path to extract values from
614
- * @returns Object mapping parameter names to extracted values
615
- * @private
616
- */
617
- extractParams(route, path) {
618
- const match = path.match(route.pattern);
619
- if (!match)
620
- return {};
621
- const params = {};
622
- route.params.forEach((param, index) => {
623
- params[param] = match[index + 1];
624
- });
625
- return params;
626
- }
627
475
  }
@@ -3,6 +3,8 @@ export declare const ROUTE_NOT_FOUND_CODE: "ROUTE_NOT_FOUND";
3
3
  export declare const HTTP_METHODS: readonly HttpMethod[];
4
4
  export declare function isHttpMethod(method: string): method is HttpMethod;
5
5
  export declare function toHttpMethod(method: string): HttpMethod;
6
+ export declare function normalizePath(path: string): string;
7
+ export declare function toRouteKey(method: HttpMethod, path: string): Schmock.RouteKey;
6
8
  /**
7
9
  * Check if a value is a status tuple: [status, body] or [status, body, headers]
8
10
  * Guards against misinterpreting numeric arrays like [1, 2, 3] as tuples.
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAG,iBAA0B,CAAC;AAE/D,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAQpC,CAAC;AAEX,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,UAAU,CAEjE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAMvD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAQxE"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAG,iBAA0B,CAAC;AAE/D,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAQpC,CAAC;AAEX,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,UAAU,CAEjE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAMvD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAG7E;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAQxE"}
package/dist/constants.js CHANGED
@@ -18,6 +18,13 @@ export function toHttpMethod(method) {
18
18
  }
19
19
  return upper;
20
20
  }
21
+ export function normalizePath(path) {
22
+ return path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
23
+ }
24
+ export function toRouteKey(method, path) {
25
+ const key = `${method} ${path}`;
26
+ return key;
27
+ }
21
28
  /**
22
29
  * Check if a value is a status tuple: [status, body] or [status, body, headers]
23
30
  * Guards against misinterpreting numeric arrays like [1, 2, 3] as tuples.
package/dist/errors.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export declare function errorMessage(error: unknown): string;
1
2
  /**
2
3
  * Base error class for all Schmock errors
3
4
  */
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,YAAa,SAAQ,KAAK;aAGnB,IAAI,EAAE,MAAM;aACZ,OAAO,CAAC,EAAE,OAAO;gBAFjC,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,YAAA;CAQpC;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAOzC;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,YAAY;gBACnC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,YAAY;gBAC3C,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAQxC;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,YAAY;gBAC/B,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAO7C;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;gBACxC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAQnE;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,OAAO;CAQ1D;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAQ7D"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEnD;AAED;;GAEG;AACH,qBAAa,YAAa,SAAQ,KAAK;aAGnB,IAAI,EAAE,MAAM;aACZ,OAAO,CAAC,EAAE,OAAO;gBAFjC,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,YAAA;CAQpC;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAOzC;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,YAAY;gBACnC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,YAAY;gBAC3C,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAQxC;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,YAAY;gBAC/B,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAO7C;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;gBACxC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAQnE;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,OAAO;CAQ1D;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAQ7D"}
package/dist/errors.js CHANGED
@@ -1,3 +1,6 @@
1
+ export function errorMessage(error) {
2
+ return error instanceof Error ? error.message : "Unknown error";
3
+ }
1
4
  /**
2
5
  * Base error class for all Schmock errors
3
6
  */
@@ -12,8 +12,11 @@ export declare function parseNodeQuery(url: URL): Record<string, string>;
12
12
  * Collect and parse the request body from a Node.js IncomingMessage.
13
13
  * Returns parsed JSON if content-type includes "json", otherwise the raw string.
14
14
  * Returns undefined for empty bodies.
15
+ * @param req - Node.js IncomingMessage
16
+ * @param headers - Parsed request headers
17
+ * @param maxBodySize - Maximum body size in bytes (default: 10 MB)
15
18
  */
16
- export declare function collectBody(req: IncomingMessage, headers: Record<string, string>): Promise<unknown>;
19
+ export declare function collectBody(req: IncomingMessage, headers: Record<string, string>, maxBodySize?: number): Promise<unknown>;
17
20
  /**
18
21
  * Write a Schmock Response to a Node.js ServerResponse.
19
22
  * Serializes non-string bodies as JSON and sets content-type when missing.
@@ -1 +1 @@
1
- {"version":3,"file":"http-helpers.d.ts","sourceRoot":"","sources":["../src/http-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ7E;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAM/D;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,OAAO,CAAC,OAAO,CAAC,CAsBlB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAC1B,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,IAAI,CAuBN"}
1
+ {"version":3,"file":"http-helpers.d.ts","sourceRoot":"","sources":["../src/http-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ7E;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAM/D;AAKD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,WAAW,SAAwB,GAClC,OAAO,CAAC,OAAO,CAAC,CAqClB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAC1B,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,IAAI,CAuBN"}
@@ -21,15 +21,30 @@ export function parseNodeQuery(url) {
21
21
  });
22
22
  return query;
23
23
  }
24
+ /** Default body size limit: 10 MB */
25
+ const DEFAULT_MAX_BODY_SIZE = 10 * 1024 * 1024;
24
26
  /**
25
27
  * Collect and parse the request body from a Node.js IncomingMessage.
26
28
  * Returns parsed JSON if content-type includes "json", otherwise the raw string.
27
29
  * Returns undefined for empty bodies.
30
+ * @param req - Node.js IncomingMessage
31
+ * @param headers - Parsed request headers
32
+ * @param maxBodySize - Maximum body size in bytes (default: 10 MB)
28
33
  */
29
- export function collectBody(req, headers) {
30
- return new Promise((resolve) => {
34
+ export function collectBody(req, headers, maxBodySize = DEFAULT_MAX_BODY_SIZE) {
35
+ return new Promise((resolve, reject) => {
31
36
  const chunks = [];
32
- req.on("data", (chunk) => chunks.push(chunk));
37
+ let totalSize = 0;
38
+ req.on("error", reject);
39
+ req.on("data", (chunk) => {
40
+ totalSize += chunk.length;
41
+ if (totalSize > maxBodySize) {
42
+ req.destroy();
43
+ reject(Object.assign(new Error("Request body too large"), { status: 413 }));
44
+ return;
45
+ }
46
+ chunks.push(chunk);
47
+ });
33
48
  req.on("end", () => {
34
49
  const raw = Buffer.concat(chunks).toString();
35
50
  if (!raw) {
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  * @returns A callable mock instance
23
23
  */
24
24
  export declare function schmock(config?: Schmock.GlobalConfig): Schmock.CallableMockInstance;
25
- export { HTTP_METHODS, isHttpMethod, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, } from "./constants.js";
25
+ export { HTTP_METHODS, isHttpMethod, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, toRouteKey, } from "./constants.js";
26
26
  export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors.js";
27
27
  export { collectBody, parseNodeHeaders, parseNodeQuery, writeSchmockResponse, } from "./http-helpers.js";
28
28
  export type { CallableMockInstance, Generator, GeneratorFunction, GlobalConfig, HttpMethod, Plugin, PluginContext, PluginResult, RequestContext, RequestOptions, RequestRecord, Response, ResponseBody, ResponseResult, RouteConfig, RouteInfo, RouteKey, ServerInfo, StaticData, } from "./types.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,OAAO,CACrB,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,GAC5B,OAAO,CAAC,oBAAoB,CAmD9B;AAGD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,YAAY,GACb,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,GACb,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,oBAAoB,EACpB,SAAS,EACT,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,MAAM,EACN,aAAa,EACb,YAAY,EACZ,cAAc,EACd,cAAc,EACd,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,WAAW,EACX,SAAS,EACT,QAAQ,EACR,UAAU,EACV,UAAU,GACX,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,OAAO,CACrB,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,GAC5B,OAAO,CAAC,oBAAoB,CAmD9B;AAGD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,YAAY,EACZ,UAAU,GACX,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,GACb,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,oBAAoB,EACpB,SAAS,EACT,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,MAAM,EACN,aAAa,EACb,YAAY,EACZ,cAAc,EACd,cAAc,EACd,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,WAAW,EACX,SAAS,EACT,QAAQ,EACR,UAAU,EACV,UAAU,GACX,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -59,7 +59,7 @@ export function schmock(config) {
59
59
  return callableInstance;
60
60
  }
61
61
  // Re-export constants and utilities
62
- export { HTTP_METHODS, isHttpMethod, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, } from "./constants.js";
62
+ export { HTTP_METHODS, isHttpMethod, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, toRouteKey, } from "./constants.js";
63
63
  // Re-export errors
64
64
  export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors.js";
65
65
  // Re-export HTTP server helpers
@@ -0,0 +1,15 @@
1
+ /** Structural typing — DebugLogger satisfies this without an import */
2
+ interface PipelineLogger {
3
+ log(category: string, message: string, data?: unknown): void;
4
+ }
5
+ /**
6
+ * Run all registered plugins in sequence
7
+ * First plugin to set response becomes generator, subsequent plugins transform
8
+ * Handles plugin errors via onError hooks
9
+ */
10
+ export declare function runPluginPipeline(plugins: Schmock.Plugin[], context: Schmock.PluginContext, initialResponse: unknown, logger: PipelineLogger): Promise<{
11
+ context: Schmock.PluginContext;
12
+ response?: unknown;
13
+ }>;
14
+ export {};
15
+ //# sourceMappingURL=plugin-pipeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-pipeline.d.ts","sourceRoot":"","sources":["../src/plugin-pipeline.ts"],"names":[],"mappings":"AAEA,uEAAuE;AACvE,UAAU,cAAc;IACtB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9D;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,EACzB,OAAO,EAAE,OAAO,CAAC,aAAa,EAC9B,eAAe,EAAE,OAAO,EACxB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAkFjE"}