@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.mjs
CHANGED
|
@@ -23,6 +23,7 @@ const Html404ErrorPage = (error) => `<!DOCTYPE html>
|
|
|
23
23
|
* You can either return `new RedirectView(url)` or `RedirectView.redirect(url)` inside of a controller method.
|
|
24
24
|
*/
|
|
25
25
|
var RedirectView = class RedirectView {
|
|
26
|
+
_url;
|
|
26
27
|
constructor(url) {
|
|
27
28
|
this._url = url;
|
|
28
29
|
}
|
|
@@ -118,8 +119,10 @@ let HttpStatus = /* @__PURE__ */ function(HttpStatus) {
|
|
|
118
119
|
* @typeParam T - the type of the response body
|
|
119
120
|
*/
|
|
120
121
|
var ResponseEntity = class {
|
|
122
|
+
_statusCode;
|
|
123
|
+
_headers = {};
|
|
124
|
+
_body;
|
|
121
125
|
constructor(body, headers = {}, statusCode = 200) {
|
|
122
|
-
this._headers = {};
|
|
123
126
|
this._body = body;
|
|
124
127
|
this._headers = headers;
|
|
125
128
|
this._statusCode = statusCode;
|
|
@@ -172,8 +175,9 @@ var ResponseEntity = class {
|
|
|
172
175
|
* ensuring type safety when constructing the response.
|
|
173
176
|
*/
|
|
174
177
|
var ResponseEntityBuilder = class {
|
|
178
|
+
_statusCode;
|
|
179
|
+
_headers = {};
|
|
175
180
|
constructor(statusCode) {
|
|
176
|
-
this._headers = {};
|
|
177
181
|
this._statusCode = statusCode;
|
|
178
182
|
}
|
|
179
183
|
/**
|
|
@@ -205,6 +209,7 @@ var ResponseEntityBuilder = class {
|
|
|
205
209
|
* @see {@link Sapling.loadResponseStatusErrorMiddleware}
|
|
206
210
|
*/
|
|
207
211
|
var ResponseStatusError = class ResponseStatusError extends Error {
|
|
212
|
+
status;
|
|
208
213
|
constructor(status, message) {
|
|
209
214
|
super(message ?? "Something went wrong.");
|
|
210
215
|
this.status = status;
|
|
@@ -216,25 +221,27 @@ var ResponseStatusError = class ResponseStatusError extends Error {
|
|
|
216
221
|
//#endregion
|
|
217
222
|
//#region src/helper/error/parse.ts
|
|
218
223
|
/**
|
|
219
|
-
* This error should be thrown when some data cannot be parsed by a given schema.
|
|
224
|
+
* This error should be thrown when some data cannot be parsed by a given Standard Schema compatible schema.
|
|
220
225
|
*/
|
|
221
226
|
var ParserError = class ParserError extends ResponseStatusError {
|
|
222
|
-
constructor(location, issues, vendor) {
|
|
223
|
-
super(400, ParserError.formatMessage(location, issues, vendor));
|
|
227
|
+
constructor(location, issues, vendor, functionName) {
|
|
228
|
+
super(400, ParserError.formatMessage(location, issues, vendor, functionName));
|
|
224
229
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
225
230
|
}
|
|
226
|
-
static formatMessage(location, issues, vendor) {
|
|
231
|
+
static formatMessage(location, issues, vendor, functionName) {
|
|
227
232
|
const formatted = issues.map((i) => {
|
|
228
233
|
const path = Array.isArray(i.path) ? i.path.map((seg) => typeof seg === "object" && seg ? String(seg.key) : String(seg)).join(".") : "";
|
|
229
234
|
return path ? `${path}: ${i.message}` : i.message;
|
|
230
235
|
}).join("; ");
|
|
231
|
-
return
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
236
|
+
return `Failed to parse ${this.getPrettyLocationString(location)} with ${vendor} on ${functionName}: ${formatted}`;
|
|
237
|
+
}
|
|
238
|
+
static getPrettyLocationString(location) {
|
|
239
|
+
switch (location) {
|
|
240
|
+
case "reqbody": return "request body";
|
|
241
|
+
case "reqparams": return "request params";
|
|
242
|
+
case "reqquery": return "request query";
|
|
243
|
+
case "resbody": return "response body";
|
|
244
|
+
}
|
|
238
245
|
}
|
|
239
246
|
};
|
|
240
247
|
//#endregion
|
|
@@ -244,7 +251,11 @@ const _settings = {
|
|
|
244
251
|
deserialize: JSON.parse,
|
|
245
252
|
doc: {
|
|
246
253
|
openApiPath: "/openapi.json",
|
|
247
|
-
swaggerPath: "/swagger.html"
|
|
254
|
+
swaggerPath: "/swagger.html",
|
|
255
|
+
metadata: {
|
|
256
|
+
title: "API",
|
|
257
|
+
version: "1.0.0"
|
|
258
|
+
}
|
|
248
259
|
}
|
|
249
260
|
};
|
|
250
261
|
/**
|
|
@@ -350,141 +361,74 @@ var Sapling = class Sapling {
|
|
|
350
361
|
static setDeserializeFn(fn) {
|
|
351
362
|
_settings.deserialize = fn;
|
|
352
363
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
static
|
|
357
|
-
|
|
364
|
+
/**
|
|
365
|
+
* Modify extra settings
|
|
366
|
+
*/
|
|
367
|
+
static Extras = {
|
|
368
|
+
/**
|
|
369
|
+
* Modify default settings applied to OpenAPI & Swagger
|
|
370
|
+
*/
|
|
371
|
+
swaggerAndOpenApi: {
|
|
372
|
+
/**
|
|
373
|
+
* Set base OpenAPI metadata values.
|
|
374
|
+
*
|
|
375
|
+
* @default { title: "API", version: "1.0.0" }
|
|
376
|
+
*/
|
|
377
|
+
setMetadata(metadata) {
|
|
378
|
+
_settings.doc.metadata = metadata;
|
|
379
|
+
},
|
|
380
|
+
/**
|
|
381
|
+
* change default endpoint that will serve OpenAPI spec.
|
|
382
|
+
* Swagger will also load this endpoint on load.
|
|
383
|
+
*
|
|
384
|
+
* @default `/openapi.json`
|
|
385
|
+
*/
|
|
386
|
+
setOpenApiPath(path) {
|
|
387
|
+
_settings.doc.openApiPath = path;
|
|
388
|
+
},
|
|
389
|
+
/**
|
|
390
|
+
* change Swagger endpoint.
|
|
391
|
+
*
|
|
392
|
+
* @default `/swagger.html`
|
|
393
|
+
*/
|
|
394
|
+
setSwaggerPath(path) {
|
|
395
|
+
_settings.doc.swaggerPath = path;
|
|
396
|
+
}
|
|
397
|
+
} };
|
|
398
|
+
/**
|
|
399
|
+
* This method can be used in a `@MiddlewareClass` to register any libraries
|
|
400
|
+
* that expect you to register multiple registers at once. An example is `swagger-ui-express`
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* ```ts
|
|
404
|
+
* ⠀@MiddlewareClass()
|
|
405
|
+
* class Serve {
|
|
406
|
+
* // `swagger.serve` returns multiple Express handlers for all the assets and routes
|
|
407
|
+
* // that will be served
|
|
408
|
+
* private readonly handlers: RequestHandler[] = swagger.serve;
|
|
409
|
+
*
|
|
410
|
+
* ⠀@Middleware(_settings.doc.swaggerPath)
|
|
411
|
+
* handle(request: Request, response: Response, next: NextFunction) {
|
|
412
|
+
* return Sapling.chainHandlers(this.handlers, request, response, next);
|
|
413
|
+
* }
|
|
414
|
+
* }
|
|
415
|
+
* ```
|
|
416
|
+
*/
|
|
417
|
+
static chainHandlers(handlers, request, response, next, index = 0) {
|
|
418
|
+
if (index >= handlers.length) {
|
|
419
|
+
next();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
handlers[index]?.(request, response, (err) => {
|
|
423
|
+
if (err) {
|
|
424
|
+
next(err);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
Sapling.chainHandlers(handlers, request, response, next, index + 1);
|
|
428
|
+
});
|
|
358
429
|
}
|
|
359
430
|
};
|
|
360
431
|
//#endregion
|
|
361
|
-
//#region src/annotation/request.ts
|
|
362
|
-
const _requestSchemaStore = /* @__PURE__ */ new WeakMap();
|
|
363
|
-
/**
|
|
364
|
-
* Apply to a route method to have `request.body` be parsed by `schema`.
|
|
365
|
-
*
|
|
366
|
-
* This annotation will parse `request.body` & then override `request.body`.
|
|
367
|
-
* You can then just simply cast `request.body` for your use
|
|
368
|
-
*
|
|
369
|
-
* @example
|
|
370
|
-
* ```ts
|
|
371
|
-
* const CREATE_BOOK_REQUEST_BODY_SCHEMA = z.object({
|
|
372
|
-
* name: z.string(),
|
|
373
|
-
* description: z.string().optional(),
|
|
374
|
-
* });
|
|
375
|
-
*
|
|
376
|
-
* ⠀@Controller({ prefix: "/api/book" })
|
|
377
|
-
* class BookController {
|
|
378
|
-
* ⠀@RequestBody(CREATE_BOOK_REQUEST_BODY_SCHEMA)
|
|
379
|
-
* ⠀@POST()
|
|
380
|
-
* public createBook(request: e.Request) {
|
|
381
|
-
* const { name, description } = request.body as unknown as z.infer<
|
|
382
|
-
* typeof CREATE_BOOK_REQUEST_BODY_SCHEMA
|
|
383
|
-
* >;
|
|
384
|
-
* }
|
|
385
|
-
* }
|
|
386
|
-
* ```
|
|
387
|
-
*/
|
|
388
|
-
function RequestBody(schema) {
|
|
389
|
-
return (target, propertyKey) => {
|
|
390
|
-
const ctor = target.constructor;
|
|
391
|
-
const fnName = String(propertyKey);
|
|
392
|
-
_setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "body", schema, fnName);
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* Apply to a route method to have `request.param` be parsed by `schema`.
|
|
397
|
-
*
|
|
398
|
-
* This annotation will parse `request.param` & then override `request.param`.
|
|
399
|
-
* You can then just simply cast `request.param` for your use
|
|
400
|
-
*
|
|
401
|
-
* @example
|
|
402
|
-
* ```ts
|
|
403
|
-
* const GET_BOOK_REQUEST_PARAM_SCHEMA = z.object({
|
|
404
|
-
* bookId: z.string(),
|
|
405
|
-
* });
|
|
406
|
-
*
|
|
407
|
-
* ⠀@Controller({ prefix: "/api/book" })
|
|
408
|
-
* class BookController {
|
|
409
|
-
* ⠀@RequestParam(GET_BOOK_REQUEST_PARAM_SCHEMA)
|
|
410
|
-
* ⠀@GET("/:bookId")
|
|
411
|
-
* public getBook(request: e.Request) {
|
|
412
|
-
* const { bookId } = request.param as unknown as z.infer<
|
|
413
|
-
* typeof GET_BOOK_REQUEST_PARAM_SCHEMA
|
|
414
|
-
* >;
|
|
415
|
-
* }
|
|
416
|
-
* }
|
|
417
|
-
* ```
|
|
418
|
-
*/
|
|
419
|
-
function RequestParam(schema) {
|
|
420
|
-
return (target, propertyKey) => {
|
|
421
|
-
const ctor = target.constructor;
|
|
422
|
-
const fnName = String(propertyKey);
|
|
423
|
-
_setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "param", schema, fnName);
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
|
-
* Apply to a route method to have `request.query` be parsed by `schema`.
|
|
428
|
-
*
|
|
429
|
-
* This annotation will parse `request.query` & then override `request.query`.
|
|
430
|
-
* You can then just simply cast `request.query` for your use
|
|
431
|
-
*
|
|
432
|
-
* @example
|
|
433
|
-
* ```ts
|
|
434
|
-
* const LIST_BOOKS_REQUEST_QUERY_SCHEMA = z.object({
|
|
435
|
-
* sort: z.enum(["name", "createdAt"]).optional(),
|
|
436
|
-
* q: z.string().optional(),
|
|
437
|
-
* });
|
|
438
|
-
*
|
|
439
|
-
* ⠀@Controller({ prefix: "/api/book" })
|
|
440
|
-
* class BookController {
|
|
441
|
-
* ⠀@RequestQuery(LIST_BOOKS_REQUEST_QUERY_SCHEMA)
|
|
442
|
-
* ⠀@GET()
|
|
443
|
-
* public listBooks(request: e.Request) {
|
|
444
|
-
* const { sort, q } = request.query as unknown as z.infer<
|
|
445
|
-
* typeof LIST_BOOKS_REQUEST_QUERY_SCHEMA
|
|
446
|
-
* >;
|
|
447
|
-
* }
|
|
448
|
-
* }
|
|
449
|
-
* ```
|
|
450
|
-
*/
|
|
451
|
-
function RequestQuery(schema) {
|
|
452
|
-
return (target, propertyKey) => {
|
|
453
|
-
const ctor = target.constructor;
|
|
454
|
-
const fnName = String(propertyKey);
|
|
455
|
-
_setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "query", schema, fnName);
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
function _getOrCreateRequestSchemaDefinition(ctor, fnName) {
|
|
459
|
-
const byFn = (() => {
|
|
460
|
-
const fn = _requestSchemaStore.get(ctor);
|
|
461
|
-
if (fn) return fn;
|
|
462
|
-
const newFn = /* @__PURE__ */ new Map();
|
|
463
|
-
_requestSchemaStore.set(ctor, newFn);
|
|
464
|
-
return newFn;
|
|
465
|
-
})();
|
|
466
|
-
const existing = byFn.get(fnName);
|
|
467
|
-
if (existing) return existing;
|
|
468
|
-
const created = {};
|
|
469
|
-
byFn.set(fnName, created);
|
|
470
|
-
return created;
|
|
471
|
-
}
|
|
472
|
-
function _setOnce(def, key, schema, fnName) {
|
|
473
|
-
if (def[key]) throw new Error(`Duplicate request schema for "${String(key)}" on method "${fnName}"`);
|
|
474
|
-
def[key] = schema;
|
|
475
|
-
}
|
|
476
|
-
function _getRequestSchemas(ctor, fnName) {
|
|
477
|
-
return _requestSchemaStore.get(ctor)?.get(fnName);
|
|
478
|
-
}
|
|
479
|
-
async function _parseOrThrow(schema, input, kind) {
|
|
480
|
-
const result = await schema["~standard"].validate(input);
|
|
481
|
-
if (result.issues) {
|
|
482
|
-
console.debug(`Failed to parse a schema`);
|
|
483
|
-
throw new ParserError(kind, result.issues, schema["~standard"].vendor);
|
|
484
|
-
}
|
|
485
|
-
return result.value;
|
|
486
|
-
}
|
|
487
|
-
//#endregion
|
|
488
432
|
//#region src/annotation/route.ts
|
|
489
433
|
const _routeStore = /* @__PURE__ */ new WeakMap();
|
|
490
434
|
/**
|
|
@@ -566,63 +510,190 @@ function _getRoutes(ctor) {
|
|
|
566
510
|
return _routeStore.get(ctor) ?? [];
|
|
567
511
|
}
|
|
568
512
|
//#endregion
|
|
513
|
+
//#region src/annotation/schema.ts
|
|
514
|
+
function ControllerSchema(options) {
|
|
515
|
+
return (target) => {
|
|
516
|
+
_setControllerSchema(target, options);
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function RouteSchema(options) {
|
|
520
|
+
return (target, propertyKey) => {
|
|
521
|
+
const ctor = target.constructor;
|
|
522
|
+
_setRouteSchema(ctor, String(propertyKey), options);
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
const _routeSchemaStore = /* @__PURE__ */ new WeakMap();
|
|
526
|
+
const _controllerSchemaStore = /* @__PURE__ */ new WeakMap();
|
|
527
|
+
function getOrCreateRouteSchemaStore(store, ctor) {
|
|
528
|
+
const existing = store.get(ctor);
|
|
529
|
+
if (existing) return existing;
|
|
530
|
+
const created = /* @__PURE__ */ new Map();
|
|
531
|
+
store.set(ctor, created);
|
|
532
|
+
return created;
|
|
533
|
+
}
|
|
534
|
+
function _setRouteSchema(ctor, fnName, options) {
|
|
535
|
+
getOrCreateRouteSchemaStore(_routeSchemaStore, ctor).set(fnName, options);
|
|
536
|
+
}
|
|
537
|
+
function _setControllerSchema(ctor, options) {
|
|
538
|
+
_controllerSchemaStore.set(ctor, options);
|
|
539
|
+
}
|
|
540
|
+
function _getRouteSchema(ctor, fnName) {
|
|
541
|
+
return _routeSchemaStore.get(ctor)?.get(fnName);
|
|
542
|
+
}
|
|
543
|
+
function _getControllerSchema(ctor) {
|
|
544
|
+
return _controllerSchemaStore.get(ctor);
|
|
545
|
+
}
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/annotation/validator.ts
|
|
548
|
+
const _validatorSchemaStore = /* @__PURE__ */ new WeakMap();
|
|
549
|
+
function ResponseBody(schema) {
|
|
550
|
+
return (target, propertyKey) => {
|
|
551
|
+
const ctor = target.constructor;
|
|
552
|
+
const fnName = String(propertyKey);
|
|
553
|
+
_saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "responseBody", schema, fnName);
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
function RequestBody(schema) {
|
|
557
|
+
return (target, propertyKey) => {
|
|
558
|
+
const ctor = target.constructor;
|
|
559
|
+
const fnName = String(propertyKey);
|
|
560
|
+
_saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestBody", schema, fnName);
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function RequestParam(schema) {
|
|
564
|
+
return (target, propertyKey) => {
|
|
565
|
+
const ctor = target.constructor;
|
|
566
|
+
const fnName = String(propertyKey);
|
|
567
|
+
_saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestParam", schema, fnName);
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function RequestQuery(schema) {
|
|
571
|
+
return (target, propertyKey) => {
|
|
572
|
+
const ctor = target.constructor;
|
|
573
|
+
const fnName = String(propertyKey);
|
|
574
|
+
_saveValidatorSchema(_getOrCreateSchemaDefinition(ctor, fnName), "requestQuery", schema, fnName);
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
function getOrCreateValidatorSchemaStore(store, ctor) {
|
|
578
|
+
const existing = store.get(ctor);
|
|
579
|
+
if (existing) return existing;
|
|
580
|
+
const created = /* @__PURE__ */ new Map();
|
|
581
|
+
store.set(ctor, created);
|
|
582
|
+
return created;
|
|
583
|
+
}
|
|
584
|
+
function _getOrCreateSchemaDefinition(ctor, fnName) {
|
|
585
|
+
const byFn = getOrCreateValidatorSchemaStore(_validatorSchemaStore, ctor);
|
|
586
|
+
const existing = byFn.get(fnName);
|
|
587
|
+
if (existing) return existing;
|
|
588
|
+
const created = {};
|
|
589
|
+
byFn.set(fnName, created);
|
|
590
|
+
return created;
|
|
591
|
+
}
|
|
592
|
+
async function _parseOrThrow(schema, input, location, fnName) {
|
|
593
|
+
const result = await schema["~standard"].validate(input);
|
|
594
|
+
if (result.issues) throw new ParserError(location, result.issues, schema["~standard"].vendor, fnName);
|
|
595
|
+
return result.value;
|
|
596
|
+
}
|
|
597
|
+
function _saveValidatorSchema(def, key, schema, fnName) {
|
|
598
|
+
if (def[key]) throw new Error(`Duplicate schema for "${String(key)}" on method "${fnName}"`);
|
|
599
|
+
def[key] = schema;
|
|
600
|
+
}
|
|
601
|
+
function _getValidatorSchema(ctor, fnName) {
|
|
602
|
+
return _validatorSchemaStore.get(ctor)?.get(fnName);
|
|
603
|
+
}
|
|
604
|
+
//#endregion
|
|
569
605
|
//#region src/helper/openapi.ts
|
|
570
606
|
var OpenAPIGenerator = class {
|
|
571
|
-
|
|
572
|
-
this.controllers = /* @__PURE__ */ new Set();
|
|
573
|
-
this.config = {
|
|
574
|
-
title: "API",
|
|
575
|
-
version: "1.0.0"
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
setConfig(config) {
|
|
579
|
-
this.config = config;
|
|
580
|
-
}
|
|
607
|
+
controllers = /* @__PURE__ */ new Set();
|
|
581
608
|
registerController(controllerClass, prefix) {
|
|
582
609
|
this.controllers.add({
|
|
583
610
|
class: controllerClass,
|
|
584
611
|
prefix
|
|
585
612
|
});
|
|
586
613
|
}
|
|
614
|
+
get metadata() {
|
|
615
|
+
return _settings.doc.metadata;
|
|
616
|
+
}
|
|
587
617
|
generateSpec() {
|
|
588
|
-
const
|
|
618
|
+
const metadata = this.metadata;
|
|
589
619
|
const paths = {};
|
|
620
|
+
const tags = [];
|
|
590
621
|
for (const { class: controllerClass, prefix } of this.controllers) {
|
|
591
622
|
const routes = _getRoutes(controllerClass);
|
|
623
|
+
const controllerSchema = _getControllerSchema(controllerClass);
|
|
624
|
+
if (controllerSchema?.title) tags.push({
|
|
625
|
+
name: controllerSchema.title,
|
|
626
|
+
description: controllerSchema.description
|
|
627
|
+
});
|
|
592
628
|
for (const route of routes) {
|
|
593
629
|
if (route.method === "USE") continue;
|
|
594
|
-
const schemas =
|
|
595
|
-
const
|
|
596
|
-
|
|
630
|
+
const schemas = _getValidatorSchema(controllerClass, route.fnName);
|
|
631
|
+
const routeSchema = _getRouteSchema(controllerClass, route.fnName);
|
|
632
|
+
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.`);
|
|
633
|
+
const openApiPath = (prefix + route.path).replace(/:([A-Za-z0-9_]+)/g, "{$1}");
|
|
597
634
|
if (!paths[openApiPath]) paths[openApiPath] = {};
|
|
598
|
-
const
|
|
635
|
+
const responses = {};
|
|
636
|
+
if (schemas?.responseBody) {
|
|
637
|
+
const responseSchema = this.toJsonSchema(schemas.responseBody, "output");
|
|
638
|
+
responses["200"] = {
|
|
639
|
+
description: responseSchema.description ?? "Successful response",
|
|
640
|
+
content: { "application/json": { schema: responseSchema } }
|
|
641
|
+
};
|
|
642
|
+
} else responses["200"] = { description: "Successful response" };
|
|
643
|
+
if (routeSchema?.responses) for (const resp of routeSchema.responses) {
|
|
644
|
+
const statusCode = String(resp.statusCode);
|
|
645
|
+
const existingResponse = responses[statusCode];
|
|
646
|
+
const existingSchema = existingResponse && "content" in existingResponse ? existingResponse.content?.["application/json"]?.schema : void 0;
|
|
647
|
+
const responseSchema = resp.schema ? this.toJsonSchema(resp.schema, "output") : statusCode === "200" ? existingSchema : void 0;
|
|
648
|
+
responses[statusCode] = {
|
|
649
|
+
...existingResponse,
|
|
650
|
+
description: resp.description ?? responseSchema?.description ?? existingResponse?.description ?? `Response ${resp.statusCode}`,
|
|
651
|
+
...responseSchema ? { content: { "application/json": { schema: responseSchema } } } : {}
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
const operation = {
|
|
655
|
+
responses,
|
|
656
|
+
summary: routeSchema?.summary,
|
|
657
|
+
description: routeSchema?.description,
|
|
658
|
+
tags: controllerSchema?.title ? [controllerSchema.title] : void 0
|
|
659
|
+
};
|
|
599
660
|
const parameters = [];
|
|
600
|
-
if (schemas?.
|
|
601
|
-
const paramSchema = this.toJsonSchema(schemas.
|
|
602
|
-
if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties))
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
661
|
+
if (schemas?.requestParam) {
|
|
662
|
+
const paramSchema = this.toJsonSchema(schemas.requestParam, "input");
|
|
663
|
+
if (paramSchema.type === "object" && paramSchema.properties) for (const [name, schema] of Object.entries(paramSchema.properties)) {
|
|
664
|
+
const parameterSchema = schema;
|
|
665
|
+
parameters.push({
|
|
666
|
+
name,
|
|
667
|
+
in: "path",
|
|
668
|
+
required: true,
|
|
669
|
+
description: parameterSchema.description,
|
|
670
|
+
schema: parameterSchema
|
|
671
|
+
});
|
|
672
|
+
}
|
|
608
673
|
}
|
|
609
|
-
if (schemas?.
|
|
610
|
-
const querySchema = this.toJsonSchema(schemas.
|
|
674
|
+
if (schemas?.requestQuery) {
|
|
675
|
+
const querySchema = this.toJsonSchema(schemas.requestQuery, "input");
|
|
611
676
|
if (querySchema.type === "object" && querySchema.properties) for (const [name, schema] of Object.entries(querySchema.properties)) {
|
|
612
677
|
const isRequired = Array.isArray(querySchema.required) && querySchema.required.includes(name);
|
|
678
|
+
const parameterSchema = schema;
|
|
613
679
|
parameters.push({
|
|
614
680
|
name,
|
|
615
681
|
in: "query",
|
|
616
682
|
required: isRequired,
|
|
617
|
-
|
|
683
|
+
description: parameterSchema.description,
|
|
684
|
+
schema: parameterSchema
|
|
618
685
|
});
|
|
619
686
|
}
|
|
620
687
|
}
|
|
621
688
|
if (parameters.length > 0) operation.parameters = parameters;
|
|
622
|
-
if (schemas?.
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
689
|
+
if (schemas?.requestBody) {
|
|
690
|
+
const requestSchema = this.toJsonSchema(schemas.requestBody, "input");
|
|
691
|
+
operation.requestBody = {
|
|
692
|
+
required: true,
|
|
693
|
+
description: requestSchema.description,
|
|
694
|
+
content: { "application/json": { schema: requestSchema } }
|
|
695
|
+
};
|
|
696
|
+
}
|
|
626
697
|
const method = route.method.toLowerCase();
|
|
627
698
|
paths[openApiPath][method] = operation;
|
|
628
699
|
}
|
|
@@ -630,30 +701,29 @@ var OpenAPIGenerator = class {
|
|
|
630
701
|
return {
|
|
631
702
|
openapi: "3.0.0",
|
|
632
703
|
info: {
|
|
633
|
-
title:
|
|
634
|
-
version:
|
|
635
|
-
description:
|
|
704
|
+
title: metadata.title,
|
|
705
|
+
version: metadata.version,
|
|
706
|
+
description: metadata.description
|
|
636
707
|
},
|
|
708
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
637
709
|
paths
|
|
638
710
|
};
|
|
639
711
|
}
|
|
640
|
-
toJsonSchema(schema) {
|
|
712
|
+
toJsonSchema(schema, direction = "output") {
|
|
641
713
|
try {
|
|
642
|
-
|
|
714
|
+
const jsonSchema = schema["~standard"].jsonSchema;
|
|
715
|
+
return direction === "input" ? jsonSchema.input({ target: "openapi-3.0" }) : jsonSchema.output({ target: "openapi-3.0" });
|
|
643
716
|
} catch (e) {
|
|
644
|
-
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
|
|
717
|
+
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 });
|
|
645
718
|
throw e;
|
|
646
719
|
}
|
|
647
720
|
}
|
|
648
721
|
};
|
|
649
722
|
const openApiGenerator = new OpenAPIGenerator();
|
|
650
|
-
function
|
|
723
|
+
function _registerController(controllerClass, prefix) {
|
|
651
724
|
openApiGenerator.registerController(controllerClass, prefix);
|
|
652
725
|
}
|
|
653
|
-
function
|
|
654
|
-
openApiGenerator.setConfig(config);
|
|
655
|
-
}
|
|
656
|
-
function _generateOpenApiSpec() {
|
|
726
|
+
function generateOpenApiSpec() {
|
|
657
727
|
return openApiGenerator.generateSpec();
|
|
658
728
|
}
|
|
659
729
|
//#endregion
|
|
@@ -800,7 +870,7 @@ const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
|
|
|
800
870
|
function Controller({ prefix = "", deps = [] } = {}) {
|
|
801
871
|
return (target) => {
|
|
802
872
|
const targetClass = target;
|
|
803
|
-
|
|
873
|
+
_registerController(target, prefix);
|
|
804
874
|
const router = Router();
|
|
805
875
|
const routes = _getRoutes(target);
|
|
806
876
|
const usedRoutes = /* @__PURE__ */ new Set();
|
|
@@ -825,15 +895,20 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
|
|
|
825
895
|
if (method === "USE" && fn.length >= 4) {
|
|
826
896
|
const middlewareFn = async (err, request, response, next) => {
|
|
827
897
|
try {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
898
|
+
await validate({
|
|
899
|
+
target,
|
|
900
|
+
fnName,
|
|
901
|
+
request
|
|
902
|
+
});
|
|
903
|
+
await handleResult({
|
|
904
|
+
result: fn.bind(controllerInstance)(err, request, response, next),
|
|
905
|
+
response,
|
|
906
|
+
target,
|
|
907
|
+
fnName,
|
|
908
|
+
method,
|
|
909
|
+
path: path instanceof RegExp ? path.source : fp,
|
|
910
|
+
isErrorMiddleware: true
|
|
911
|
+
});
|
|
837
912
|
} catch (e) {
|
|
838
913
|
console.error(e);
|
|
839
914
|
next(e);
|
|
@@ -843,34 +918,52 @@ Split these into separate @MiddlewareClass classes, or merge the logic into a si
|
|
|
843
918
|
return;
|
|
844
919
|
}
|
|
845
920
|
router[methodName](fp, async (request, response, next) => {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
}
|
|
859
|
-
const result = await fn.bind(controllerInstance)(request, response, next);
|
|
860
|
-
if (result instanceof ResponseEntity) {
|
|
861
|
-
response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
if (result instanceof RedirectView) {
|
|
865
|
-
response.redirect(result.getUrl());
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
if (method !== "USE" && !response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${methodName.toUpperCase()} ${path instanceof RegExp ? path.source : fp}`));
|
|
921
|
+
await validate({
|
|
922
|
+
target,
|
|
923
|
+
fnName,
|
|
924
|
+
request
|
|
925
|
+
});
|
|
926
|
+
await handleResult({
|
|
927
|
+
result: await fn.bind(controllerInstance)(request, response, next),
|
|
928
|
+
response,
|
|
929
|
+
target,
|
|
930
|
+
fnName,
|
|
931
|
+
method,
|
|
932
|
+
path: path instanceof RegExp ? path.source : fp
|
|
933
|
+
});
|
|
869
934
|
});
|
|
870
935
|
}
|
|
871
936
|
_ControllerRegistry.set(targetClass, router);
|
|
872
937
|
};
|
|
873
938
|
}
|
|
939
|
+
async function handleResult({ result, target, fnName, response, method, path, isErrorMiddleware = false }) {
|
|
940
|
+
const schemas = _getValidatorSchema(target, fnName);
|
|
941
|
+
if (result instanceof ResponseEntity) {
|
|
942
|
+
const body = schemas && schemas.responseBody ? await _parseOrThrow(schemas.responseBody, result.getBody(), "resbody", fnName) : result.getBody();
|
|
943
|
+
response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(body));
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
if (result instanceof RedirectView) {
|
|
947
|
+
response.redirect(result.getUrl());
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
if (!isErrorMiddleware && method !== "USE" && !response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${method} ${path}`));
|
|
951
|
+
}
|
|
952
|
+
async function validate({ target, fnName, request }) {
|
|
953
|
+
const schemas = _getValidatorSchema(target, fnName);
|
|
954
|
+
if (schemas) {
|
|
955
|
+
if (schemas.requestBody) request.body = await _parseOrThrow(schemas.requestBody, request.body, "reqbody", fnName);
|
|
956
|
+
if (schemas.requestParam) request.params = await _parseOrThrow(schemas.requestParam, request.params, "reqparams", fnName);
|
|
957
|
+
if (schemas.requestQuery) {
|
|
958
|
+
const parsedQuery = await _parseOrThrow(schemas.requestQuery, request.query, "reqquery", fnName);
|
|
959
|
+
Object.defineProperty(request, "query", {
|
|
960
|
+
value: parsedQuery,
|
|
961
|
+
writable: true,
|
|
962
|
+
configurable: true
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
874
967
|
//#endregion
|
|
875
968
|
//#region src/annotation/middleware.ts
|
|
876
969
|
/**
|
|
@@ -905,7 +998,10 @@ DefaultBaseErrorMiddleware = __decorate([MiddlewareClass()], DefaultBaseErrorMid
|
|
|
905
998
|
//#region src/middleware/default/error/parse.ts
|
|
906
999
|
let DefaultParserErrorMiddleware = class DefaultParserErrorMiddleware {
|
|
907
1000
|
handle(err, _request, _response, next) {
|
|
908
|
-
if (err instanceof ParserError)
|
|
1001
|
+
if (err instanceof ParserError) {
|
|
1002
|
+
console.warn(err);
|
|
1003
|
+
return ResponseEntity.status(err.status).body({ message: err.message });
|
|
1004
|
+
}
|
|
909
1005
|
next(err);
|
|
910
1006
|
}
|
|
911
1007
|
};
|
|
@@ -925,26 +1021,57 @@ DefaultResponseStatusErrorMiddleware = __decorate([MiddlewareClass()], DefaultRe
|
|
|
925
1021
|
//#region src/middleware/default/openapi/index.ts
|
|
926
1022
|
let DefaultOpenApiMiddleware = class DefaultOpenApiMiddleware {
|
|
927
1023
|
handle(_request, _response, _next) {
|
|
928
|
-
return ResponseEntity.ok().body(
|
|
1024
|
+
return ResponseEntity.ok().body(generateOpenApiSpec());
|
|
929
1025
|
}
|
|
930
1026
|
};
|
|
931
1027
|
__decorate([GET(_settings.doc.openApiPath)], DefaultOpenApiMiddleware.prototype, "handle", null);
|
|
932
1028
|
DefaultOpenApiMiddleware = __decorate([MiddlewareClass()], DefaultOpenApiMiddleware);
|
|
933
1029
|
//#endregion
|
|
934
1030
|
//#region src/middleware/default/swagger/index.ts
|
|
1031
|
+
/**
|
|
1032
|
+
* Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
|
|
1033
|
+
*
|
|
1034
|
+
* Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
|
|
1035
|
+
*
|
|
1036
|
+
* You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
|
|
1037
|
+
*
|
|
1038
|
+
* ```ts
|
|
1039
|
+
* const middlewares = [
|
|
1040
|
+
* DefaultOpenApiMiddleware,
|
|
1041
|
+
* DefaultSwaggerMiddleware.Serve,
|
|
1042
|
+
* DefaultSwaggerMiddleware.Setup,
|
|
1043
|
+
* ];
|
|
1044
|
+
* middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
1045
|
+
* ```
|
|
1046
|
+
*/
|
|
935
1047
|
let Serve = class Serve {
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
handle(_request, _response, _next) {
|
|
940
|
-
return this.handlers;
|
|
1048
|
+
handlers = swagger.serve;
|
|
1049
|
+
handle(request, response, next) {
|
|
1050
|
+
return Sapling.chainHandlers(this.handlers, request, response, next);
|
|
941
1051
|
}
|
|
942
1052
|
};
|
|
943
1053
|
__decorate([Middleware(_settings.doc.swaggerPath)], Serve.prototype, "handle", null);
|
|
944
1054
|
Serve = __decorate([MiddlewareClass()], Serve);
|
|
1055
|
+
/**
|
|
1056
|
+
* Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
|
|
1057
|
+
*
|
|
1058
|
+
* Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
|
|
1059
|
+
*
|
|
1060
|
+
* You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
|
|
1061
|
+
*
|
|
1062
|
+
* ```ts
|
|
1063
|
+
* const middlewares = [
|
|
1064
|
+
* DefaultOpenApiMiddleware,
|
|
1065
|
+
* DefaultSwaggerMiddleware.Serve,
|
|
1066
|
+
* DefaultSwaggerMiddleware.Setup,
|
|
1067
|
+
* ];
|
|
1068
|
+
* middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
1069
|
+
* ```
|
|
1070
|
+
*/
|
|
945
1071
|
let Setup = class Setup {
|
|
1072
|
+
handler;
|
|
946
1073
|
constructor() {
|
|
947
|
-
this.handler = swagger.setup(
|
|
1074
|
+
this.handler = swagger.setup(null, { swaggerOptions: { url: _settings.doc.openApiPath } });
|
|
948
1075
|
}
|
|
949
1076
|
handle(request, response, next) {
|
|
950
1077
|
return this.handler(request, response, next);
|
|
@@ -952,9 +1079,25 @@ let Setup = class Setup {
|
|
|
952
1079
|
};
|
|
953
1080
|
__decorate([Middleware(_settings.doc.swaggerPath)], Setup.prototype, "handle", null);
|
|
954
1081
|
Setup = __decorate([MiddlewareClass()], Setup);
|
|
1082
|
+
/**
|
|
1083
|
+
* Enable the serving of the Swagger endpoint used to serve the OpenAPI spec generated by Sapling.
|
|
1084
|
+
*
|
|
1085
|
+
* Configure any middleware-specific settings with `Sapling.Extras.swaggerAndOpenApi`
|
|
1086
|
+
*
|
|
1087
|
+
* You must register `DefaultSwaggerMiddleware.Serve` & `DefaultSwaggerMiddleware.Setup` after `DefaultOpenApiMiddleware`
|
|
1088
|
+
*
|
|
1089
|
+
* ```ts
|
|
1090
|
+
* const middlewares = [
|
|
1091
|
+
* DefaultOpenApiMiddleware,
|
|
1092
|
+
* DefaultSwaggerMiddleware.Serve,
|
|
1093
|
+
* DefaultSwaggerMiddleware.Setup,
|
|
1094
|
+
* ];
|
|
1095
|
+
* middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
|
|
1096
|
+
* ```
|
|
1097
|
+
*/
|
|
955
1098
|
const DefaultSwaggerMiddleware = {
|
|
956
1099
|
Serve,
|
|
957
1100
|
Setup
|
|
958
1101
|
};
|
|
959
1102
|
//#endregion
|
|
960
|
-
export { Controller, DELETE, DefaultBaseErrorMiddleware, DefaultOpenApiMiddleware, DefaultParserErrorMiddleware, DefaultResponseStatusErrorMiddleware, DefaultSwaggerMiddleware, GET, HEAD, Html404ErrorPage, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, ParserError, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route,
|
|
1103
|
+
export { Controller, ControllerSchema, DELETE, DefaultBaseErrorMiddleware, DefaultOpenApiMiddleware, DefaultParserErrorMiddleware, DefaultResponseStatusErrorMiddleware, DefaultSwaggerMiddleware, GET, HEAD, Html404ErrorPage, HttpStatus, Injectable, Middleware, MiddlewareClass, OPTIONS, PATCH, POST, PUT, ParserError, RedirectView, RequestBody, RequestParam, RequestQuery, ResponseBody, ResponseEntity, ResponseEntityBuilder, ResponseStatusError, RouteSchema, Sapling, _ControllerRegistry, _InjectableDeps, _InjectableRegistry, _Route, _getControllerSchema, _getOrCreateSchemaDefinition, _getRouteSchema, _getRoutes, _getValidatorSchema, _parseOrThrow, _registerController, _resolve, _saveValidatorSchema, _setControllerSchema, _setRouteSchema, _settings, generateOpenApiSpec, methodResolve, openApiGenerator };
|