@tahminator/sapling 2.0.5-beta.23c37926 → 2.0.5-beta.279125eb
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 +161 -12
- package/dist/index.cjs +383 -232
- package/dist/index.d.cts +256 -177
- package/dist/index.d.mts +256 -177
- package/dist/index.mjs +372 -229
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -48,6 +48,7 @@ const Html404ErrorPage = (error) => `<!DOCTYPE html>
|
|
|
48
48
|
* You can either return `new RedirectView(url)` or `RedirectView.redirect(url)` inside of a controller method.
|
|
49
49
|
*/
|
|
50
50
|
var RedirectView = class RedirectView {
|
|
51
|
+
_url;
|
|
51
52
|
constructor(url) {
|
|
52
53
|
this._url = url;
|
|
53
54
|
}
|
|
@@ -143,8 +144,10 @@ let HttpStatus = /* @__PURE__ */ function(HttpStatus) {
|
|
|
143
144
|
* @typeParam T - the type of the response body
|
|
144
145
|
*/
|
|
145
146
|
var ResponseEntity = class {
|
|
147
|
+
_statusCode;
|
|
148
|
+
_headers = {};
|
|
149
|
+
_body;
|
|
146
150
|
constructor(body, headers = {}, statusCode = 200) {
|
|
147
|
-
this._headers = {};
|
|
148
151
|
this._body = body;
|
|
149
152
|
this._headers = headers;
|
|
150
153
|
this._statusCode = statusCode;
|
|
@@ -197,8 +200,9 @@ var ResponseEntity = class {
|
|
|
197
200
|
* ensuring type safety when constructing the response.
|
|
198
201
|
*/
|
|
199
202
|
var ResponseEntityBuilder = class {
|
|
203
|
+
_statusCode;
|
|
204
|
+
_headers = {};
|
|
200
205
|
constructor(statusCode) {
|
|
201
|
-
this._headers = {};
|
|
202
206
|
this._statusCode = statusCode;
|
|
203
207
|
}
|
|
204
208
|
/**
|
|
@@ -230,6 +234,7 @@ var ResponseEntityBuilder = class {
|
|
|
230
234
|
* @see {@link Sapling.loadResponseStatusErrorMiddleware}
|
|
231
235
|
*/
|
|
232
236
|
var ResponseStatusError = class ResponseStatusError extends Error {
|
|
237
|
+
status;
|
|
233
238
|
constructor(status, message) {
|
|
234
239
|
super(message ?? "Something went wrong.");
|
|
235
240
|
this.status = status;
|
|
@@ -241,25 +246,27 @@ var ResponseStatusError = class ResponseStatusError extends Error {
|
|
|
241
246
|
//#endregion
|
|
242
247
|
//#region src/helper/error/parse.ts
|
|
243
248
|
/**
|
|
244
|
-
* This error should be thrown when some data cannot be parsed by a given schema.
|
|
249
|
+
* This error should be thrown when some data cannot be parsed by a given Standard Schema compatible schema.
|
|
245
250
|
*/
|
|
246
251
|
var ParserError = class ParserError extends ResponseStatusError {
|
|
247
|
-
constructor(location, issues, vendor) {
|
|
248
|
-
super(400, ParserError.formatMessage(location, issues, vendor));
|
|
252
|
+
constructor(location, issues, vendor, functionName) {
|
|
253
|
+
super(400, ParserError.formatMessage(location, issues, vendor, functionName));
|
|
249
254
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
250
255
|
}
|
|
251
|
-
static formatMessage(location, issues, vendor) {
|
|
256
|
+
static formatMessage(location, issues, vendor, functionName) {
|
|
252
257
|
const formatted = issues.map((i) => {
|
|
253
258
|
const path = Array.isArray(i.path) ? i.path.map((seg) => typeof seg === "object" && seg ? String(seg.key) : String(seg)).join(".") : "";
|
|
254
259
|
return path ? `${path}: ${i.message}` : i.message;
|
|
255
260
|
}).join("; ");
|
|
256
|
-
return
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
return `Failed to parse ${this.getPrettyLocationString(location)} with ${vendor} on ${functionName}: ${formatted}`;
|
|
262
|
+
}
|
|
263
|
+
static getPrettyLocationString(location) {
|
|
264
|
+
switch (location) {
|
|
265
|
+
case "reqbody": return "request body";
|
|
266
|
+
case "reqparams": return "request params";
|
|
267
|
+
case "reqquery": return "request query";
|
|
268
|
+
case "resbody": return "response body";
|
|
269
|
+
}
|
|
263
270
|
}
|
|
264
271
|
};
|
|
265
272
|
//#endregion
|
|
@@ -269,7 +276,11 @@ const _settings = {
|
|
|
269
276
|
deserialize: JSON.parse,
|
|
270
277
|
doc: {
|
|
271
278
|
openApiPath: "/openapi.json",
|
|
272
|
-
swaggerPath: "/swagger.html"
|
|
279
|
+
swaggerPath: "/swagger.html",
|
|
280
|
+
metadata: {
|
|
281
|
+
title: "API",
|
|
282
|
+
version: "1.0.0"
|
|
283
|
+
}
|
|
273
284
|
}
|
|
274
285
|
};
|
|
275
286
|
/**
|
|
@@ -375,141 +386,74 @@ var Sapling = class Sapling {
|
|
|
375
386
|
static setDeserializeFn(fn) {
|
|
376
387
|
_settings.deserialize = fn;
|
|
377
388
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
static
|
|
382
|
-
|
|
389
|
+
/**
|
|
390
|
+
* Modify extra settings
|
|
391
|
+
*/
|
|
392
|
+
static Extras = {
|
|
393
|
+
/**
|
|
394
|
+
* Modify default settings applied to OpenAPI & Swagger
|
|
395
|
+
*/
|
|
396
|
+
swaggerAndOpenApi: {
|
|
397
|
+
/**
|
|
398
|
+
* Set base OpenAPI metadata values.
|
|
399
|
+
*
|
|
400
|
+
* @default { title: "API", version: "1.0.0" }
|
|
401
|
+
*/
|
|
402
|
+
setMetadata(metadata) {
|
|
403
|
+
_settings.doc.metadata = metadata;
|
|
404
|
+
},
|
|
405
|
+
/**
|
|
406
|
+
* change default endpoint that will serve OpenAPI spec.
|
|
407
|
+
* Swagger will also load this endpoint on load.
|
|
408
|
+
*
|
|
409
|
+
* @default `/openapi.json`
|
|
410
|
+
*/
|
|
411
|
+
setOpenApiPath(path) {
|
|
412
|
+
_settings.doc.openApiPath = path;
|
|
413
|
+
},
|
|
414
|
+
/**
|
|
415
|
+
* change Swagger endpoint.
|
|
416
|
+
*
|
|
417
|
+
* @default `/swagger.html`
|
|
418
|
+
*/
|
|
419
|
+
setSwaggerPath(path) {
|
|
420
|
+
_settings.doc.swaggerPath = path;
|
|
421
|
+
}
|
|
422
|
+
} };
|
|
423
|
+
/**
|
|
424
|
+
* This method can be used in a `@MiddlewareClass` to register any libraries
|
|
425
|
+
* that expect you to register multiple registers at once. An example is `swagger-ui-express`
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* ```ts
|
|
429
|
+
* ⠀@MiddlewareClass()
|
|
430
|
+
* class Serve {
|
|
431
|
+
* // `swagger.serve` returns multiple Express handlers for all the assets and routes
|
|
432
|
+
* // that will be served
|
|
433
|
+
* private readonly handlers: RequestHandler[] = swagger.serve;
|
|
434
|
+
*
|
|
435
|
+
* ⠀@Middleware(_settings.doc.swaggerPath)
|
|
436
|
+
* handle(request: Request, response: Response, next: NextFunction) {
|
|
437
|
+
* return Sapling.chainHandlers(this.handlers, request, response, next);
|
|
438
|
+
* }
|
|
439
|
+
* }
|
|
440
|
+
* ```
|
|
441
|
+
*/
|
|
442
|
+
static chainHandlers(handlers, request, response, next, index = 0) {
|
|
443
|
+
if (index >= handlers.length) {
|
|
444
|
+
next();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
handlers[index]?.(request, response, (err) => {
|
|
448
|
+
if (err) {
|
|
449
|
+
next(err);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
Sapling.chainHandlers(handlers, request, response, next, index + 1);
|
|
453
|
+
});
|
|
383
454
|
}
|
|
384
455
|
};
|
|
385
456
|
//#endregion
|
|
386
|
-
//#region src/annotation/request.ts
|
|
387
|
-
const _requestSchemaStore = /* @__PURE__ */ new WeakMap();
|
|
388
|
-
/**
|
|
389
|
-
* Apply to a route method to have `request.body` be parsed by `schema`.
|
|
390
|
-
*
|
|
391
|
-
* This annotation will parse `request.body` & then override `request.body`.
|
|
392
|
-
* You can then just simply cast `request.body` for your use
|
|
393
|
-
*
|
|
394
|
-
* @example
|
|
395
|
-
* ```ts
|
|
396
|
-
* const CREATE_BOOK_REQUEST_BODY_SCHEMA = z.object({
|
|
397
|
-
* name: z.string(),
|
|
398
|
-
* description: z.string().optional(),
|
|
399
|
-
* });
|
|
400
|
-
*
|
|
401
|
-
* ⠀@Controller({ prefix: "/api/book" })
|
|
402
|
-
* class BookController {
|
|
403
|
-
* ⠀@RequestBody(CREATE_BOOK_REQUEST_BODY_SCHEMA)
|
|
404
|
-
* ⠀@POST()
|
|
405
|
-
* public createBook(request: e.Request) {
|
|
406
|
-
* const { name, description } = request.body as unknown as z.infer<
|
|
407
|
-
* typeof CREATE_BOOK_REQUEST_BODY_SCHEMA
|
|
408
|
-
* >;
|
|
409
|
-
* }
|
|
410
|
-
* }
|
|
411
|
-
* ```
|
|
412
|
-
*/
|
|
413
|
-
function RequestBody(schema) {
|
|
414
|
-
return (target, propertyKey) => {
|
|
415
|
-
const ctor = target.constructor;
|
|
416
|
-
const fnName = String(propertyKey);
|
|
417
|
-
_setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "body", schema, fnName);
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* Apply to a route method to have `request.param` be parsed by `schema`.
|
|
422
|
-
*
|
|
423
|
-
* This annotation will parse `request.param` & then override `request.param`.
|
|
424
|
-
* You can then just simply cast `request.param` for your use
|
|
425
|
-
*
|
|
426
|
-
* @example
|
|
427
|
-
* ```ts
|
|
428
|
-
* const GET_BOOK_REQUEST_PARAM_SCHEMA = z.object({
|
|
429
|
-
* bookId: z.string(),
|
|
430
|
-
* });
|
|
431
|
-
*
|
|
432
|
-
* ⠀@Controller({ prefix: "/api/book" })
|
|
433
|
-
* class BookController {
|
|
434
|
-
* ⠀@RequestParam(GET_BOOK_REQUEST_PARAM_SCHEMA)
|
|
435
|
-
* ⠀@GET("/:bookId")
|
|
436
|
-
* public getBook(request: e.Request) {
|
|
437
|
-
* const { bookId } = request.param as unknown as z.infer<
|
|
438
|
-
* typeof GET_BOOK_REQUEST_PARAM_SCHEMA
|
|
439
|
-
* >;
|
|
440
|
-
* }
|
|
441
|
-
* }
|
|
442
|
-
* ```
|
|
443
|
-
*/
|
|
444
|
-
function RequestParam(schema) {
|
|
445
|
-
return (target, propertyKey) => {
|
|
446
|
-
const ctor = target.constructor;
|
|
447
|
-
const fnName = String(propertyKey);
|
|
448
|
-
_setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "param", schema, fnName);
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* Apply to a route method to have `request.query` be parsed by `schema`.
|
|
453
|
-
*
|
|
454
|
-
* This annotation will parse `request.query` & then override `request.query`.
|
|
455
|
-
* You can then just simply cast `request.query` for your use
|
|
456
|
-
*
|
|
457
|
-
* @example
|
|
458
|
-
* ```ts
|
|
459
|
-
* const LIST_BOOKS_REQUEST_QUERY_SCHEMA = z.object({
|
|
460
|
-
* sort: z.enum(["name", "createdAt"]).optional(),
|
|
461
|
-
* q: z.string().optional(),
|
|
462
|
-
* });
|
|
463
|
-
*
|
|
464
|
-
* ⠀@Controller({ prefix: "/api/book" })
|
|
465
|
-
* class BookController {
|
|
466
|
-
* ⠀@RequestQuery(LIST_BOOKS_REQUEST_QUERY_SCHEMA)
|
|
467
|
-
* ⠀@GET()
|
|
468
|
-
* public listBooks(request: e.Request) {
|
|
469
|
-
* const { sort, q } = request.query as unknown as z.infer<
|
|
470
|
-
* typeof LIST_BOOKS_REQUEST_QUERY_SCHEMA
|
|
471
|
-
* >;
|
|
472
|
-
* }
|
|
473
|
-
* }
|
|
474
|
-
* ```
|
|
475
|
-
*/
|
|
476
|
-
function RequestQuery(schema) {
|
|
477
|
-
return (target, propertyKey) => {
|
|
478
|
-
const ctor = target.constructor;
|
|
479
|
-
const fnName = String(propertyKey);
|
|
480
|
-
_setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "query", schema, fnName);
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
function _getOrCreateRequestSchemaDefinition(ctor, fnName) {
|
|
484
|
-
const byFn = (() => {
|
|
485
|
-
const fn = _requestSchemaStore.get(ctor);
|
|
486
|
-
if (fn) return fn;
|
|
487
|
-
const newFn = /* @__PURE__ */ new Map();
|
|
488
|
-
_requestSchemaStore.set(ctor, newFn);
|
|
489
|
-
return newFn;
|
|
490
|
-
})();
|
|
491
|
-
const existing = byFn.get(fnName);
|
|
492
|
-
if (existing) return existing;
|
|
493
|
-
const created = {};
|
|
494
|
-
byFn.set(fnName, created);
|
|
495
|
-
return created;
|
|
496
|
-
}
|
|
497
|
-
function _setOnce(def, key, schema, fnName) {
|
|
498
|
-
if (def[key]) throw new Error(`Duplicate request schema for "${String(key)}" on method "${fnName}"`);
|
|
499
|
-
def[key] = schema;
|
|
500
|
-
}
|
|
501
|
-
function _getRequestSchemas(ctor, fnName) {
|
|
502
|
-
return _requestSchemaStore.get(ctor)?.get(fnName);
|
|
503
|
-
}
|
|
504
|
-
async function _parseOrThrow(schema, input, kind) {
|
|
505
|
-
const result = await schema["~standard"].validate(input);
|
|
506
|
-
if (result.issues) {
|
|
507
|
-
console.debug(`Failed to parse a schema`);
|
|
508
|
-
throw new ParserError(kind, result.issues, schema["~standard"].vendor);
|
|
509
|
-
}
|
|
510
|
-
return result.value;
|
|
511
|
-
}
|
|
512
|
-
//#endregion
|
|
513
457
|
//#region src/annotation/route.ts
|
|
514
458
|
const _routeStore = /* @__PURE__ */ new WeakMap();
|
|
515
459
|
/**
|
|
@@ -591,63 +535,190 @@ function _getRoutes(ctor) {
|
|
|
591
535
|
return _routeStore.get(ctor) ?? [];
|
|
592
536
|
}
|
|
593
537
|
//#endregion
|
|
538
|
+
//#region src/annotation/schema.ts
|
|
539
|
+
function ControllerSchema(options) {
|
|
540
|
+
return (target) => {
|
|
541
|
+
_setControllerSchema(target, options);
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function RouteSchema(options) {
|
|
545
|
+
return (target, propertyKey) => {
|
|
546
|
+
const ctor = target.constructor;
|
|
547
|
+
_setRouteSchema(ctor, String(propertyKey), options);
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
const _routeSchemaStore = /* @__PURE__ */ new WeakMap();
|
|
551
|
+
const _controllerSchemaStore = /* @__PURE__ */ new WeakMap();
|
|
552
|
+
function getOrCreateRouteSchemaStore(store, ctor) {
|
|
553
|
+
const existing = store.get(ctor);
|
|
554
|
+
if (existing) return existing;
|
|
555
|
+
const created = /* @__PURE__ */ new Map();
|
|
556
|
+
store.set(ctor, created);
|
|
557
|
+
return created;
|
|
558
|
+
}
|
|
559
|
+
function _setRouteSchema(ctor, fnName, options) {
|
|
560
|
+
getOrCreateRouteSchemaStore(_routeSchemaStore, ctor).set(fnName, options);
|
|
561
|
+
}
|
|
562
|
+
function _setControllerSchema(ctor, options) {
|
|
563
|
+
_controllerSchemaStore.set(ctor, options);
|
|
564
|
+
}
|
|
565
|
+
function _getRouteSchema(ctor, fnName) {
|
|
566
|
+
return _routeSchemaStore.get(ctor)?.get(fnName);
|
|
567
|
+
}
|
|
568
|
+
function _getControllerSchema(ctor) {
|
|
569
|
+
return _controllerSchemaStore.get(ctor);
|
|
570
|
+
}
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region src/annotation/validator.ts
|
|
573
|
+
const _validatorSchemaStore = /* @__PURE__ */ new WeakMap();
|
|
574
|
+
function ResponseBody(schema) {
|
|
575
|
+
return (target, propertyKey) => {
|
|
576
|
+
const ctor = target.constructor;
|
|
577
|
+
const fnName = String(propertyKey);
|
|
578
|
+
_saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "responseBody", schema, fnName);
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function RequestBody(schema) {
|
|
582
|
+
return (target, propertyKey) => {
|
|
583
|
+
const ctor = target.constructor;
|
|
584
|
+
const fnName = String(propertyKey);
|
|
585
|
+
_saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestBody", schema, fnName);
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function RequestParam(schema) {
|
|
589
|
+
return (target, propertyKey) => {
|
|
590
|
+
const ctor = target.constructor;
|
|
591
|
+
const fnName = String(propertyKey);
|
|
592
|
+
_saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestParam", schema, fnName);
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function RequestQuery(schema) {
|
|
596
|
+
return (target, propertyKey) => {
|
|
597
|
+
const ctor = target.constructor;
|
|
598
|
+
const fnName = String(propertyKey);
|
|
599
|
+
_saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestQuery", schema, fnName);
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function getOrCreateValidatorSchemaStore(store, ctor) {
|
|
603
|
+
const existing = store.get(ctor);
|
|
604
|
+
if (existing) return existing;
|
|
605
|
+
const created = /* @__PURE__ */ new Map();
|
|
606
|
+
store.set(ctor, created);
|
|
607
|
+
return created;
|
|
608
|
+
}
|
|
609
|
+
function _getOrCreateSchemaDefinition(ctor, fnName) {
|
|
610
|
+
const byFn = getOrCreateValidatorSchemaStore(_validatorSchemaStore, ctor);
|
|
611
|
+
const existing = byFn.get(fnName);
|
|
612
|
+
if (existing) return existing;
|
|
613
|
+
const created = {};
|
|
614
|
+
byFn.set(fnName, created);
|
|
615
|
+
return created;
|
|
616
|
+
}
|
|
617
|
+
async function _parseOrThrow(schema, input, location, fnName) {
|
|
618
|
+
const result = await schema["~standard"].validate(input);
|
|
619
|
+
if (result.issues) throw new ParserError(location, result.issues, schema["~standard"].vendor, fnName);
|
|
620
|
+
return result.value;
|
|
621
|
+
}
|
|
622
|
+
function _saveValidatorSchema(def, key, schema, fnName) {
|
|
623
|
+
if (def[key]) throw new Error(`Duplicate schema for "${String(key)}" on method "${fnName}"`);
|
|
624
|
+
def[key] = schema;
|
|
625
|
+
}
|
|
626
|
+
function _getValidatorSchema(ctor, fnName) {
|
|
627
|
+
return _validatorSchemaStore.get(ctor)?.get(fnName);
|
|
628
|
+
}
|
|
629
|
+
//#endregion
|
|
594
630
|
//#region src/helper/openapi.ts
|
|
595
631
|
var OpenAPIGenerator = class {
|
|
596
|
-
|
|
597
|
-
this.controllers = /* @__PURE__ */ new Set();
|
|
598
|
-
this.config = {
|
|
599
|
-
title: "API",
|
|
600
|
-
version: "1.0.0"
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
setConfig(config) {
|
|
604
|
-
this.config = config;
|
|
605
|
-
}
|
|
632
|
+
controllers = /* @__PURE__ */ new Set();
|
|
606
633
|
registerController(controllerClass, prefix) {
|
|
607
634
|
this.controllers.add({
|
|
608
635
|
class: controllerClass,
|
|
609
636
|
prefix
|
|
610
637
|
});
|
|
611
638
|
}
|
|
639
|
+
get metadata() {
|
|
640
|
+
return _settings.doc.metadata;
|
|
641
|
+
}
|
|
612
642
|
generateSpec() {
|
|
613
|
-
const
|
|
643
|
+
const metadata = this.metadata;
|
|
614
644
|
const paths = {};
|
|
645
|
+
const tags = [];
|
|
615
646
|
for (const { class: controllerClass, prefix } of this.controllers) {
|
|
616
647
|
const routes = _getRoutes(controllerClass);
|
|
648
|
+
const controllerSchema = _getControllerSchema(controllerClass);
|
|
649
|
+
if (controllerSchema?.title) tags.push({
|
|
650
|
+
name: controllerSchema.title,
|
|
651
|
+
description: controllerSchema.description
|
|
652
|
+
});
|
|
617
653
|
for (const route of routes) {
|
|
618
654
|
if (route.method === "USE") continue;
|
|
619
|
-
const schemas =
|
|
620
|
-
const
|
|
621
|
-
|
|
655
|
+
const schemas = _getValidatorSchema(controllerClass, route.fnName);
|
|
656
|
+
const routeSchema = _getRouteSchema(controllerClass, route.fnName);
|
|
657
|
+
if (route.path instanceof RegExp) throw new Error(`You have a route with a regex path of ${route.path.source}. This is not compatible with OpenAPI.`);
|
|
658
|
+
const openApiPath = (prefix + route.path).replace(/:([A-Za-z0-9_]+)/g, "{$1}");
|
|
622
659
|
if (!paths[openApiPath]) paths[openApiPath] = {};
|
|
623
|
-
const
|
|
660
|
+
const responses = {};
|
|
661
|
+
if (schemas?.responseBody) {
|
|
662
|
+
const responseSchema = this.toJsonSchema(schemas.responseBody, "output");
|
|
663
|
+
responses["200"] = {
|
|
664
|
+
description: responseSchema.description ?? "Successful response",
|
|
665
|
+
content: { "application/json": { schema: responseSchema } }
|
|
666
|
+
};
|
|
667
|
+
} else responses["200"] = { description: "Successful response" };
|
|
668
|
+
if (routeSchema?.responses) for (const resp of routeSchema.responses) {
|
|
669
|
+
const statusCode = String(resp.statusCode);
|
|
670
|
+
const existingResponse = responses[statusCode];
|
|
671
|
+
const existingSchema = existingResponse && "content" in existingResponse ? existingResponse.content?.["application/json"]?.schema : void 0;
|
|
672
|
+
const responseSchema = resp.schema ? this.toJsonSchema(resp.schema, "output") : statusCode === "200" ? existingSchema : void 0;
|
|
673
|
+
responses[statusCode] = {
|
|
674
|
+
...existingResponse,
|
|
675
|
+
description: resp.description ?? responseSchema?.description ?? existingResponse?.description ?? `Response ${resp.statusCode}`,
|
|
676
|
+
...responseSchema ? { content: { "application/json": { schema: responseSchema } } } : {}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
const operation = {
|
|
680
|
+
responses,
|
|
681
|
+
summary: routeSchema?.summary,
|
|
682
|
+
description: routeSchema?.description,
|
|
683
|
+
tags: controllerSchema?.title ? [controllerSchema.title] : void 0
|
|
684
|
+
};
|
|
624
685
|
const parameters = [];
|
|
625
|
-
if (schemas?.
|
|
626
|
-
const paramSchema = this.toJsonSchema(schemas.
|
|
627
|
-
if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties))
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
686
|
+
if (schemas?.requestParam) {
|
|
687
|
+
const paramSchema = this.toJsonSchema(schemas.requestParam, "input");
|
|
688
|
+
if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) {
|
|
689
|
+
const parameterSchema = schema;
|
|
690
|
+
parameters.push({
|
|
691
|
+
name,
|
|
692
|
+
in: "path",
|
|
693
|
+
required: true,
|
|
694
|
+
description: parameterSchema.description,
|
|
695
|
+
schema: parameterSchema
|
|
696
|
+
});
|
|
697
|
+
}
|
|
633
698
|
}
|
|
634
|
-
if (schemas?.
|
|
635
|
-
const querySchema = this.toJsonSchema(schemas.
|
|
699
|
+
if (schemas?.requestQuery) {
|
|
700
|
+
const querySchema = this.toJsonSchema(schemas.requestQuery, "input");
|
|
636
701
|
if (querySchema.type === "object" && querySchema.properties) for (const [name, schema] of Object.entries(querySchema.properties)) {
|
|
637
702
|
const isRequired = Array.isArray(querySchema.required) && querySchema.required.includes(name);
|
|
703
|
+
const parameterSchema = schema;
|
|
638
704
|
parameters.push({
|
|
639
705
|
name,
|
|
640
706
|
in: "query",
|
|
641
707
|
required: isRequired,
|
|
642
|
-
|
|
708
|
+
description: parameterSchema.description,
|
|
709
|
+
schema: parameterSchema
|
|
643
710
|
});
|
|
644
711
|
}
|
|
645
712
|
}
|
|
646
713
|
if (parameters.length > 0) operation.parameters = parameters;
|
|
647
|
-
if (schemas?.
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
714
|
+
if (schemas?.requestBody) {
|
|
715
|
+
const requestSchema = this.toJsonSchema(schemas.requestBody, "input");
|
|
716
|
+
operation.requestBody = {
|
|
717
|
+
required: true,
|
|
718
|
+
description: requestSchema.description,
|
|
719
|
+
content: { "application/json": { schema: requestSchema } }
|
|
720
|
+
};
|
|
721
|
+
}
|
|
651
722
|
const method = route.method.toLowerCase();
|
|
652
723
|
paths[openApiPath][method] = operation;
|
|
653
724
|
}
|
|
@@ -655,30 +726,29 @@ var OpenAPIGenerator = class {
|
|
|
655
726
|
return {
|
|
656
727
|
openapi: "3.0.0",
|
|
657
728
|
info: {
|
|
658
|
-
title:
|
|
659
|
-
version:
|
|
660
|
-
description:
|
|
729
|
+
title: metadata.title,
|
|
730
|
+
version: metadata.version,
|
|
731
|
+
description: metadata.description
|
|
661
732
|
},
|
|
733
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
662
734
|
paths
|
|
663
735
|
};
|
|
664
736
|
}
|
|
665
|
-
toJsonSchema(schema) {
|
|
737
|
+
toJsonSchema(schema, direction = "output") {
|
|
666
738
|
try {
|
|
667
|
-
|
|
739
|
+
const jsonSchema = schema["~standard"].jsonSchema;
|
|
740
|
+
return direction === "input" ? jsonSchema.input({ target: "openapi-3.0" }) : jsonSchema.output({ target: "openapi-3.0" });
|
|
668
741
|
} catch (e) {
|
|
669
|
-
if (e instanceof Error && e.message.includes("Transforms cannot be represented in JSON Schema")) throw new Error(`${e.message}.\nIt appears that you are using z.transform() - it is highly recommended that you use z.codec instead - https://zod.dev/codecs
|
|
742
|
+
if (e instanceof Error && e.message.includes("Transforms cannot be represented in JSON Schema")) throw new Error(`${e.message}.\nIt appears that you are using z.transform() - it is highly recommended that you use z.codec instead - https://zod.dev/codecs`, { cause: e });
|
|
670
743
|
throw e;
|
|
671
744
|
}
|
|
672
745
|
}
|
|
673
746
|
};
|
|
674
747
|
const openApiGenerator = new OpenAPIGenerator();
|
|
675
|
-
function
|
|
748
|
+
function _registerController(controllerClass, prefix) {
|
|
676
749
|
openApiGenerator.registerController(controllerClass, prefix);
|
|
677
750
|
}
|
|
678
|
-
function
|
|
679
|
-
openApiGenerator.setConfig(config);
|
|
680
|
-
}
|
|
681
|
-
function _generateOpenApiSpec() {
|
|
751
|
+
function generateOpenApiSpec() {
|
|
682
752
|
return openApiGenerator.generateSpec();
|
|
683
753
|
}
|
|
684
754
|
//#endregion
|
|
@@ -825,7 +895,7 @@ const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
|
|
|
825
895
|
function Controller({ prefix = "", deps = [] } = {}) {
|
|
826
896
|
return (target) => {
|
|
827
897
|
const targetClass = target;
|
|
828
|
-
|
|
898
|
+
_registerController(target, prefix);
|
|
829
899
|
const router = (0, express.Router)();
|
|
830
900
|
const routes = _getRoutes(target);
|
|
831
901
|
const usedRoutes = /* @__PURE__ */ new Set();
|
|
@@ -850,15 +920,20 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
|
|
|
850
920
|
if (method === "USE" && fn.length >= 4) {
|
|
851
921
|
const middlewareFn = async (err, request, response, next) => {
|
|
852
922
|
try {
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
923
|
+
await validate({
|
|
924
|
+
target,
|
|
925
|
+
fnName,
|
|
926
|
+
request
|
|
927
|
+
});
|
|
928
|
+
await handleResult({
|
|
929
|
+
result: fn.bind(controllerInstance)(err, request, response, next),
|
|
930
|
+
response,
|
|
931
|
+
target,
|
|
932
|
+
fnName,
|
|
933
|
+
method,
|
|
934
|
+
path: path instanceof RegExp ? path.source : fp,
|
|
935
|
+
isErrorMiddleware: true
|
|
936
|
+
});
|
|
862
937
|
} catch (e) {
|
|
863
938
|
console.error(e);
|
|
864
939
|
next(e);
|
|
@@ -868,34 +943,52 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
|
|
|
868
943
|
return;
|
|
869
944
|
}
|
|
870
945
|
router[methodName](fp, async (request, response, next) => {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
}
|
|
884
|
-
const result = await fn.bind(controllerInstance)(request, response, next);
|
|
885
|
-
if (result instanceof ResponseEntity) {
|
|
886
|
-
response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
if (result instanceof RedirectView) {
|
|
890
|
-
response.redirect(result.getUrl());
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
if (method !== "USE" && !response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${methodName.toUpperCase()} ${path instanceof RegExp ? path.source : fp}`));
|
|
946
|
+
await validate({
|
|
947
|
+
target,
|
|
948
|
+
fnName,
|
|
949
|
+
request
|
|
950
|
+
});
|
|
951
|
+
await handleResult({
|
|
952
|
+
result: await fn.bind(controllerInstance)(request, response, next),
|
|
953
|
+
response,
|
|
954
|
+
target,
|
|
955
|
+
fnName,
|
|
956
|
+
method,
|
|
957
|
+
path: path instanceof RegExp ? path.source : fp
|
|
958
|
+
});
|
|
894
959
|
});
|
|
895
960
|
}
|
|
896
961
|
_ControllerRegistry.set(targetClass, router);
|
|
897
962
|
};
|
|
898
963
|
}
|
|
964
|
+
async function handleResult({ result, target, fnName, response, method, path, isErrorMiddleware = false }) {
|
|
965
|
+
const schemas = _getValidatorSchema(target, fnName);
|
|
966
|
+
if (result instanceof ResponseEntity) {
|
|
967
|
+
const body = schemas && schemas.responseBody ? await _parseOrThrow(schemas.responseBody, result.getBody(), "resbody", fnName) : result.getBody();
|
|
968
|
+
response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(body));
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (result instanceof RedirectView) {
|
|
972
|
+
response.redirect(result.getUrl());
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (!isErrorMiddleware && method !== "USE" && !response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${method} ${path}`));
|
|
976
|
+
}
|
|
977
|
+
async function validate({ target, fnName, request }) {
|
|
978
|
+
const schemas = _getValidatorSchema(target, fnName);
|
|
979
|
+
if (schemas) {
|
|
980
|
+
if (schemas.requestBody) request.body = await _parseOrThrow(schemas.requestBody, request.body, "reqbody", fnName);
|
|
981
|
+
if (schemas.requestParam) request.params = await _parseOrThrow(schemas.requestParam, request.params, "reqparams", fnName);
|
|
982
|
+
if (schemas.requestQuery) {
|
|
983
|
+
const parsedQuery = await _parseOrThrow(schemas.requestQuery, request.query, "reqquery", fnName);
|
|
984
|
+
Object.defineProperty(request, "query", {
|
|
985
|
+
value: parsedQuery,
|
|
986
|
+
writable: true,
|
|
987
|
+
configurable: true
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
899
992
|
//#endregion
|
|
900
993
|
//#region src/annotation/middleware.ts
|
|
901
994
|
/**
|
|
@@ -930,7 +1023,10 @@ DefaultBaseErrorMiddleware = __decorate([MiddlewareClass()], DefaultBaseErrorMid
|
|
|
930
1023
|
//#region src/middleware/default/error/parse.ts
|
|
931
1024
|
let DefaultParserErrorMiddleware = class DefaultParserErrorMiddleware {
|
|
932
1025
|
handle(err, _request, _response, next) {
|
|
933
|
-
if (err instanceof ParserError)
|
|
1026
|
+
if (err instanceof ParserError) {
|
|
1027
|
+
console.warn(err);
|
|
1028
|
+
return ResponseEntity.status(err.status).body({ message: err.message });
|
|
1029
|
+
}
|
|
934
1030
|
next(err);
|
|
935
1031
|
}
|
|
936
1032
|
};
|
|
@@ -950,26 +1046,57 @@ DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultRe
|
|
|
950
1046
|
//#region src/middleware/default/openapi/index.ts
|
|
951
1047
|
let DefaultOpenApiMiddleware = class DefaultOpenApiMiddleware {
|
|
952
1048
|
handle(_request, _response, _next) {
|
|
953
|
-
return ResponseEntity.ok().body(
|
|
1049
|
+
return ResponseEntity.ok().body(generateOpenApiSpec());
|
|
954
1050
|
}
|
|
955
1051
|
};
|
|
956
1052
|
__decorate([GET(_settings.doc.openApiPath)], DefaultOpenApiMiddleware.prototype, "handle", null);
|
|
957
1053
|
DefaultOpenApiMiddleware = __decorate([MiddlewareClass()], DefaultOpenApiMiddleware);
|
|
958
1054
|
//#endregion
|
|
959
1055
|
//#region src/middleware/default/swagger/index.ts
|
|
1056
|
+
/**
|
|
1057
|
+
* Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
|
|
1058
|
+
*
|
|
1059
|
+
* Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
|
|
1060
|
+
*
|
|
1061
|
+
* You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
|
|
1062
|
+
*
|
|
1063
|
+
* ```ts
|
|
1064
|
+
* const middlewares = [
|
|
1065
|
+
* DefaultOpenApiMiddleware,
|
|
1066
|
+
* DefaultSwaggerMiddleware.Serve,
|
|
1067
|
+
* DefaultSwaggerMiddleware.Setup,
|
|
1068
|
+
* ];
|
|
1069
|
+
* middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
1070
|
+
* ```
|
|
1071
|
+
*/
|
|
960
1072
|
let Serve = class Serve {
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
handle(_request, _response, _next) {
|
|
965
|
-
return this.handlers;
|
|
1073
|
+
handlers = swagger_ui_express.default.serve;
|
|
1074
|
+
handle(request, response, next) {
|
|
1075
|
+
return Sapling.chainHandlers(this.handlers, request, response, next);
|
|
966
1076
|
}
|
|
967
1077
|
};
|
|
968
1078
|
__decorate([Middleware(_settings.doc.swaggerPath)], Serve.prototype, "handle", null);
|
|
969
1079
|
Serve = __decorate([MiddlewareClass()], Serve);
|
|
1080
|
+
/**
|
|
1081
|
+
* Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
|
|
1082
|
+
*
|
|
1083
|
+
* Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
|
|
1084
|
+
*
|
|
1085
|
+
* You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
|
|
1086
|
+
*
|
|
1087
|
+
* ```ts
|
|
1088
|
+
* const middlewares = [
|
|
1089
|
+
* DefaultOpenApiMiddleware,
|
|
1090
|
+
* DefaultSwaggerMiddleware.Serve,
|
|
1091
|
+
* DefaultSwaggerMiddleware.Setup,
|
|
1092
|
+
* ];
|
|
1093
|
+
* middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
1094
|
+
* ```
|
|
1095
|
+
*/
|
|
970
1096
|
let Setup = class Setup {
|
|
1097
|
+
handler;
|
|
971
1098
|
constructor() {
|
|
972
|
-
this.handler = swagger_ui_express.default.setup(
|
|
1099
|
+
this.handler = swagger_ui_express.default.setup(null, { swaggerOptions: { url: _settings.doc.openApiPath } });
|
|
973
1100
|
}
|
|
974
1101
|
handle(request, response, next) {
|
|
975
1102
|
return this.handler(request, response, next);
|
|
@@ -977,12 +1104,29 @@ let Setup = class Setup {
|
|
|
977
1104
|
};
|
|
978
1105
|
__decorate([Middleware(_settings.doc.swaggerPath)], Setup.prototype, "handle", null);
|
|
979
1106
|
Setup = __decorate([MiddlewareClass()], Setup);
|
|
1107
|
+
/**
|
|
1108
|
+
* Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
|
|
1109
|
+
*
|
|
1110
|
+
* Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
|
|
1111
|
+
*
|
|
1112
|
+
* You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
|
|
1113
|
+
*
|
|
1114
|
+
* ```ts
|
|
1115
|
+
* const middlewares = [
|
|
1116
|
+
* DefaultOpenApiMiddleware,
|
|
1117
|
+
* DefaultSwaggerMiddleware.Serve,
|
|
1118
|
+
* DefaultSwaggerMiddleware.Setup,
|
|
1119
|
+
* ];
|
|
1120
|
+
* middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
1121
|
+
* ```
|
|
1122
|
+
*/
|
|
980
1123
|
const DefaultSwaggerMiddleware = {
|
|
981
1124
|
Serve,
|
|
982
1125
|
Setup
|
|
983
1126
|
};
|
|
984
1127
|
//#endregion
|
|
985
1128
|
exports.Controller = Controller;
|
|
1129
|
+
exports.ControllerSchema = ControllerSchema;
|
|
986
1130
|
exports.DELETE = DELETE;
|
|
987
1131
|
Object.defineProperty(exports, "DefaultBaseErrorMiddleware", {
|
|
988
1132
|
enumerable: true,
|
|
@@ -1025,21 +1169,28 @@ exports.RedirectView = RedirectView;
|
|
|
1025
1169
|
exports.RequestBody = RequestBody;
|
|
1026
1170
|
exports.RequestParam = RequestParam;
|
|
1027
1171
|
exports.RequestQuery = RequestQuery;
|
|
1172
|
+
exports.ResponseBody = ResponseBody;
|
|
1028
1173
|
exports.ResponseEntity = ResponseEntity;
|
|
1029
1174
|
exports.ResponseEntityBuilder = ResponseEntityBuilder;
|
|
1030
1175
|
exports.ResponseStatusError = ResponseStatusError;
|
|
1176
|
+
exports.RouteSchema = RouteSchema;
|
|
1031
1177
|
exports.Sapling = Sapling;
|
|
1032
1178
|
exports._ControllerRegistry = _ControllerRegistry;
|
|
1033
1179
|
exports._InjectableDeps = _InjectableDeps;
|
|
1034
1180
|
exports._InjectableRegistry = _InjectableRegistry;
|
|
1035
1181
|
exports._Route = _Route;
|
|
1036
|
-
exports.
|
|
1037
|
-
exports.
|
|
1182
|
+
exports._getControllerSchema = _getControllerSchema;
|
|
1183
|
+
exports._getOrCreateSchemaDefinition = _getOrCreateSchemaDefinition;
|
|
1184
|
+
exports._getRouteSchema = _getRouteSchema;
|
|
1038
1185
|
exports._getRoutes = _getRoutes;
|
|
1186
|
+
exports._getValidatorSchema = _getValidatorSchema;
|
|
1039
1187
|
exports._parseOrThrow = _parseOrThrow;
|
|
1040
|
-
exports.
|
|
1188
|
+
exports._registerController = _registerController;
|
|
1041
1189
|
exports._resolve = _resolve;
|
|
1042
|
-
exports.
|
|
1190
|
+
exports._saveValidatorSchema = _saveValidatorSchema;
|
|
1191
|
+
exports._setControllerSchema = _setControllerSchema;
|
|
1192
|
+
exports._setRouteSchema = _setRouteSchema;
|
|
1043
1193
|
exports._settings = _settings;
|
|
1194
|
+
exports.generateOpenApiSpec = generateOpenApiSpec;
|
|
1044
1195
|
exports.methodResolve = methodResolve;
|
|
1045
1196
|
exports.openApiGenerator = openApiGenerator;
|