@tahminator/sapling 1.5.27 → 1.5.28-beta.7e624925
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +755 -0
- package/dist/index.d.cts +521 -0
- package/dist/index.d.mts +521 -0
- package/dist/index.mjs +701 -0
- package/package.json +15 -10
- package/dist/eslint.config.d.ts +0 -2
- package/dist/eslint.config.js +0 -38
- package/dist/exclusions.d.ts +0 -5
- package/dist/exclusions.js +0 -6
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/lib/weakmap.d.ts +0 -15
- package/dist/lib/weakmap.js +0 -77
- package/dist/src/__test__/first.d.ts +0 -6
- package/dist/src/__test__/first.js +0 -20
- package/dist/src/__test__/second.d.ts +0 -6
- package/dist/src/__test__/second.js +0 -20
- package/dist/src/annotation/controller.d.ts +0 -21
- package/dist/src/annotation/controller.js +0 -78
- package/dist/src/annotation/index.d.ts +0 -4
- package/dist/src/annotation/index.js +0 -4
- package/dist/src/annotation/injectable.d.ts +0 -25
- package/dist/src/annotation/injectable.js +0 -72
- package/dist/src/annotation/middleware.d.ts +0 -9
- package/dist/src/annotation/middleware.js +0 -11
- package/dist/src/annotation/route.d.ts +0 -47
- package/dist/src/annotation/route.js +0 -77
- package/dist/src/enum/http.d.ts +0 -68
- package/dist/src/enum/http.js +0 -71
- package/dist/src/enum/index.d.ts +0 -1
- package/dist/src/enum/index.js +0 -1
- package/dist/src/helper/error.d.ts +0 -10
- package/dist/src/helper/error.js +0 -19
- package/dist/src/helper/index.d.ts +0 -4
- package/dist/src/helper/index.js +0 -4
- package/dist/src/helper/redirect.d.ts +0 -14
- package/dist/src/helper/redirect.js +0 -19
- package/dist/src/helper/response.d.ts +0 -68
- package/dist/src/helper/response.js +0 -90
- package/dist/src/helper/sapling.d.ts +0 -101
- package/dist/src/helper/sapling.js +0 -153
- package/dist/src/html/404.d.ts +0 -4
- package/dist/src/html/404.js +0 -14
- package/dist/src/html/index.d.ts +0 -1
- package/dist/src/html/index.js +0 -1
- package/dist/src/index.d.ts +0 -5
- package/dist/src/index.js +0 -5
- package/dist/src/types.d.ts +0 -21
- package/dist/src/types.js +0 -11
- package/dist/vite.config.d.ts +0 -2
- package/dist/vite.config.js +0 -18
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let express = require("express");
|
|
25
|
+
express = __toESM(express);
|
|
26
|
+
//#region src/html/404.ts
|
|
27
|
+
/**
|
|
28
|
+
* Default Express.js 404 error page, as a string.
|
|
29
|
+
*/
|
|
30
|
+
const Html404ErrorPage = (error) => `<!DOCTYPE html>
|
|
31
|
+
<html lang="en">
|
|
32
|
+
<head>
|
|
33
|
+
<meta charset="utf-8">
|
|
34
|
+
<title>Error</title>
|
|
35
|
+
</head>
|
|
36
|
+
<body>
|
|
37
|
+
<pre>${error}</pre>
|
|
38
|
+
</body>
|
|
39
|
+
</html>
|
|
40
|
+
`;
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/helper/redirect.ts
|
|
43
|
+
/**
|
|
44
|
+
* Generic HTTP redirect wrapped modeled after Spring's `RedirectView`.
|
|
45
|
+
*
|
|
46
|
+
* You can either return `new RedirectView(url)` or `RedirectView.redirect(url)` inside of a controller method.
|
|
47
|
+
*/
|
|
48
|
+
var RedirectView = class RedirectView {
|
|
49
|
+
constructor(url) {
|
|
50
|
+
this._url = url;
|
|
51
|
+
}
|
|
52
|
+
getUrl() {
|
|
53
|
+
return this._url;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Instantiate `RedirectView` with the given `url`.
|
|
57
|
+
*/
|
|
58
|
+
static redirect(url) {
|
|
59
|
+
return new RedirectView(url);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/enum/http.ts
|
|
64
|
+
/**
|
|
65
|
+
* Enum of every valid HTTP status code mapped to a specific enum member.
|
|
66
|
+
*
|
|
67
|
+
* @see {@link ResponseEntity}
|
|
68
|
+
*/
|
|
69
|
+
let HttpStatus = /* @__PURE__ */ function(HttpStatus) {
|
|
70
|
+
HttpStatus[HttpStatus["CONTINUE"] = 100] = "CONTINUE";
|
|
71
|
+
HttpStatus[HttpStatus["SWITCHING_PROTOCOLS"] = 101] = "SWITCHING_PROTOCOLS";
|
|
72
|
+
HttpStatus[HttpStatus["PROCESSING"] = 102] = "PROCESSING";
|
|
73
|
+
HttpStatus[HttpStatus["EARLY_HINTS"] = 103] = "EARLY_HINTS";
|
|
74
|
+
HttpStatus[HttpStatus["OK"] = 200] = "OK";
|
|
75
|
+
HttpStatus[HttpStatus["CREATED"] = 201] = "CREATED";
|
|
76
|
+
HttpStatus[HttpStatus["ACCEPTED"] = 202] = "ACCEPTED";
|
|
77
|
+
HttpStatus[HttpStatus["NON_AUTHORITATIVE_INFORMATION"] = 203] = "NON_AUTHORITATIVE_INFORMATION";
|
|
78
|
+
HttpStatus[HttpStatus["NO_CONTENT"] = 204] = "NO_CONTENT";
|
|
79
|
+
HttpStatus[HttpStatus["RESET_CONTENT"] = 205] = "RESET_CONTENT";
|
|
80
|
+
HttpStatus[HttpStatus["PARTIAL_CONTENT"] = 206] = "PARTIAL_CONTENT";
|
|
81
|
+
HttpStatus[HttpStatus["MULTI_STATUS"] = 207] = "MULTI_STATUS";
|
|
82
|
+
HttpStatus[HttpStatus["ALREADY_REPORTED"] = 208] = "ALREADY_REPORTED";
|
|
83
|
+
HttpStatus[HttpStatus["IM_USED"] = 226] = "IM_USED";
|
|
84
|
+
HttpStatus[HttpStatus["MULTIPLE_CHOICES"] = 300] = "MULTIPLE_CHOICES";
|
|
85
|
+
HttpStatus[HttpStatus["MOVED_PERMANENTLY"] = 301] = "MOVED_PERMANENTLY";
|
|
86
|
+
HttpStatus[HttpStatus["FOUND"] = 302] = "FOUND";
|
|
87
|
+
HttpStatus[HttpStatus["SEE_OTHER"] = 303] = "SEE_OTHER";
|
|
88
|
+
HttpStatus[HttpStatus["NOT_MODIFIED"] = 304] = "NOT_MODIFIED";
|
|
89
|
+
HttpStatus[HttpStatus["TEMPORARY_REDIRECT"] = 307] = "TEMPORARY_REDIRECT";
|
|
90
|
+
HttpStatus[HttpStatus["PERMANENT_REDIRECT"] = 308] = "PERMANENT_REDIRECT";
|
|
91
|
+
HttpStatus[HttpStatus["BAD_REQUEST"] = 400] = "BAD_REQUEST";
|
|
92
|
+
HttpStatus[HttpStatus["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
|
|
93
|
+
HttpStatus[HttpStatus["PAYMENT_REQUIRED"] = 402] = "PAYMENT_REQUIRED";
|
|
94
|
+
HttpStatus[HttpStatus["FORBIDDEN"] = 403] = "FORBIDDEN";
|
|
95
|
+
HttpStatus[HttpStatus["NOT_FOUND"] = 404] = "NOT_FOUND";
|
|
96
|
+
HttpStatus[HttpStatus["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
|
|
97
|
+
HttpStatus[HttpStatus["NOT_ACCEPTABLE"] = 406] = "NOT_ACCEPTABLE";
|
|
98
|
+
HttpStatus[HttpStatus["PROXY_AUTHENTICATION_REQUIRED"] = 407] = "PROXY_AUTHENTICATION_REQUIRED";
|
|
99
|
+
HttpStatus[HttpStatus["REQUEST_TIMEOUT"] = 408] = "REQUEST_TIMEOUT";
|
|
100
|
+
HttpStatus[HttpStatus["CONFLICT"] = 409] = "CONFLICT";
|
|
101
|
+
HttpStatus[HttpStatus["GONE"] = 410] = "GONE";
|
|
102
|
+
HttpStatus[HttpStatus["LENGTH_REQUIRED"] = 411] = "LENGTH_REQUIRED";
|
|
103
|
+
HttpStatus[HttpStatus["PRECONDITION_FAILED"] = 412] = "PRECONDITION_FAILED";
|
|
104
|
+
HttpStatus[HttpStatus["PAYLOAD_TOO_LARGE"] = 413] = "PAYLOAD_TOO_LARGE";
|
|
105
|
+
HttpStatus[HttpStatus["URI_TOO_LONG"] = 414] = "URI_TOO_LONG";
|
|
106
|
+
HttpStatus[HttpStatus["UNSUPPORTED_MEDIA_TYPE"] = 415] = "UNSUPPORTED_MEDIA_TYPE";
|
|
107
|
+
HttpStatus[HttpStatus["REQUESTED_RANGE_NOT_SATISFIABLE"] = 416] = "REQUESTED_RANGE_NOT_SATISFIABLE";
|
|
108
|
+
HttpStatus[HttpStatus["EXPECTATION_FAILED"] = 417] = "EXPECTATION_FAILED";
|
|
109
|
+
HttpStatus[HttpStatus["I_AM_A_TEAPOT"] = 418] = "I_AM_A_TEAPOT";
|
|
110
|
+
HttpStatus[HttpStatus["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY";
|
|
111
|
+
HttpStatus[HttpStatus["LOCKED"] = 423] = "LOCKED";
|
|
112
|
+
HttpStatus[HttpStatus["FAILED_DEPENDENCY"] = 424] = "FAILED_DEPENDENCY";
|
|
113
|
+
HttpStatus[HttpStatus["TOO_EARLY"] = 425] = "TOO_EARLY";
|
|
114
|
+
HttpStatus[HttpStatus["UPGRADE_REQUIRED"] = 426] = "UPGRADE_REQUIRED";
|
|
115
|
+
HttpStatus[HttpStatus["PRECONDITION_REQUIRED"] = 428] = "PRECONDITION_REQUIRED";
|
|
116
|
+
HttpStatus[HttpStatus["TOO_MANY_REQUESTS"] = 429] = "TOO_MANY_REQUESTS";
|
|
117
|
+
HttpStatus[HttpStatus["REQUEST_HEADER_FIELDS_TOO_LARGE"] = 431] = "REQUEST_HEADER_FIELDS_TOO_LARGE";
|
|
118
|
+
HttpStatus[HttpStatus["UNAVAILABLE_FOR_LEGAL_REASONS"] = 451] = "UNAVAILABLE_FOR_LEGAL_REASONS";
|
|
119
|
+
HttpStatus[HttpStatus["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
|
|
120
|
+
HttpStatus[HttpStatus["NOT_IMPLEMENTED"] = 501] = "NOT_IMPLEMENTED";
|
|
121
|
+
HttpStatus[HttpStatus["BAD_GATEWAY"] = 502] = "BAD_GATEWAY";
|
|
122
|
+
HttpStatus[HttpStatus["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE";
|
|
123
|
+
HttpStatus[HttpStatus["GATEWAY_TIMEOUT"] = 504] = "GATEWAY_TIMEOUT";
|
|
124
|
+
HttpStatus[HttpStatus["HTTP_VERSION_NOT_SUPPORTED"] = 505] = "HTTP_VERSION_NOT_SUPPORTED";
|
|
125
|
+
HttpStatus[HttpStatus["VARIANT_ALSO_NEGOTIATES"] = 506] = "VARIANT_ALSO_NEGOTIATES";
|
|
126
|
+
HttpStatus[HttpStatus["INSUFFICIENT_STORAGE"] = 507] = "INSUFFICIENT_STORAGE";
|
|
127
|
+
HttpStatus[HttpStatus["LOOP_DETECTED"] = 508] = "LOOP_DETECTED";
|
|
128
|
+
HttpStatus[HttpStatus["BANDWIDTH_LIMIT_EXCEEDED"] = 509] = "BANDWIDTH_LIMIT_EXCEEDED";
|
|
129
|
+
HttpStatus[HttpStatus["NOT_EXTENDED"] = 510] = "NOT_EXTENDED";
|
|
130
|
+
HttpStatus[HttpStatus["NETWORK_AUTHENTICATION_REQUIRED"] = 511] = "NETWORK_AUTHENTICATION_REQUIRED";
|
|
131
|
+
return HttpStatus;
|
|
132
|
+
}({});
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/helper/response.ts
|
|
135
|
+
/**
|
|
136
|
+
* Generic HTTP response wrapper modeled after Spring's `ResponseEntity`.
|
|
137
|
+
*
|
|
138
|
+
* Provides status code, headers, and an optional typed body.
|
|
139
|
+
* The body is serialized through `Sapling.serialize`.
|
|
140
|
+
*
|
|
141
|
+
* @typeParam T - the type of the response body
|
|
142
|
+
*/
|
|
143
|
+
var ResponseEntity = class {
|
|
144
|
+
constructor(body, headers = {}, statusCode = 200) {
|
|
145
|
+
this._headers = {};
|
|
146
|
+
this._body = body;
|
|
147
|
+
this._headers = headers;
|
|
148
|
+
this._statusCode = statusCode;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Create a builder with a 200 status code.
|
|
152
|
+
*
|
|
153
|
+
* @example```ts
|
|
154
|
+
* return ResponseEntity.ok().body({ success: true });
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
static ok() {
|
|
158
|
+
return new ResponseEntityBuilder(200);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Create a builder with a custom status code.
|
|
162
|
+
*
|
|
163
|
+
* @example```ts
|
|
164
|
+
* return ResponseEntity.status(HttpStatus.BAD_REQUEST).body({ success: false });
|
|
165
|
+
* ```
|
|
166
|
+
*
|
|
167
|
+
* @see {@link HttpStatus}
|
|
168
|
+
*/
|
|
169
|
+
static status(statusCode) {
|
|
170
|
+
return new ResponseEntityBuilder(statusCode);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Return status code.
|
|
174
|
+
*/
|
|
175
|
+
getStatusCode() {
|
|
176
|
+
return this._statusCode;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Return headers.
|
|
180
|
+
*/
|
|
181
|
+
getHeaders() {
|
|
182
|
+
return this._headers;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Return the response body.
|
|
186
|
+
*/
|
|
187
|
+
getBody() {
|
|
188
|
+
return this._body;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Builder for {@link ResponseEntity}.
|
|
193
|
+
*
|
|
194
|
+
* Forces the status code to be set first, then headers and body,
|
|
195
|
+
* ensuring type safety when constructing the response.
|
|
196
|
+
*/
|
|
197
|
+
var ResponseEntityBuilder = class {
|
|
198
|
+
constructor(statusCode) {
|
|
199
|
+
this._headers = {};
|
|
200
|
+
this._statusCode = statusCode;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Set all headers as an object with keys and values.
|
|
204
|
+
*/
|
|
205
|
+
headers(headers) {
|
|
206
|
+
this._headers = headers;
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Add/override a single key and value to the headers.
|
|
211
|
+
*/
|
|
212
|
+
setHeader(key, value) {
|
|
213
|
+
this._headers[key] = value;
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Set the response body.
|
|
218
|
+
*/
|
|
219
|
+
body(body) {
|
|
220
|
+
return new ResponseEntity(body, this._headers, this._statusCode);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/helper/error/responsestatus.ts
|
|
225
|
+
/**
|
|
226
|
+
* Ensure that you define a middleware that can handle this error.
|
|
227
|
+
*
|
|
228
|
+
* @see {@link Sapling.loadResponseStatusErrorMiddleware}
|
|
229
|
+
*/
|
|
230
|
+
var ResponseStatusError = class ResponseStatusError extends Error {
|
|
231
|
+
constructor(status, message) {
|
|
232
|
+
super(message ?? "Something went wrong.");
|
|
233
|
+
this.status = status;
|
|
234
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
235
|
+
this.name = `HttpError(${HttpStatus[status]})`;
|
|
236
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, ResponseStatusError);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/helper/sapling.ts
|
|
241
|
+
const settings = {
|
|
242
|
+
serialize: JSON.stringify,
|
|
243
|
+
deserialize: JSON.parse
|
|
244
|
+
};
|
|
245
|
+
/**
|
|
246
|
+
* Collection of utility functions which are essential for Sapling to function.
|
|
247
|
+
*/
|
|
248
|
+
var Sapling = class Sapling {
|
|
249
|
+
/**
|
|
250
|
+
* If you would prefer to manually resolve your controllers instead, call resolve
|
|
251
|
+
* on the controller class.
|
|
252
|
+
*
|
|
253
|
+
* @example```ts
|
|
254
|
+
* import { Sapling } from "@tahminator/sapling";
|
|
255
|
+
* import TestController from "./path/to/test.controller";
|
|
256
|
+
*
|
|
257
|
+
* const app = express();
|
|
258
|
+
*
|
|
259
|
+
* const router = Sapling.resolve(TestController);
|
|
260
|
+
* app.use(router);
|
|
261
|
+
* ```
|
|
262
|
+
*/
|
|
263
|
+
static resolve(clazz) {
|
|
264
|
+
const router = _ControllerRegistry.get(clazz);
|
|
265
|
+
if (!router) throw new Error("Controller cannot be found");
|
|
266
|
+
return router;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Register this function as a middleware in order to utilize Sapling's `deserialize` function.
|
|
270
|
+
*
|
|
271
|
+
* @example```ts
|
|
272
|
+
* import { Sapling } from "@tahminator/sapling";
|
|
273
|
+
* import express from "express";
|
|
274
|
+
*
|
|
275
|
+
* const app = express();
|
|
276
|
+
*
|
|
277
|
+
* app.use(Sapling.json());
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
static json() {
|
|
281
|
+
return (request, _response, next) => {
|
|
282
|
+
try {
|
|
283
|
+
if (!request.body) return next();
|
|
284
|
+
if (request.headers["content-type"] !== "application/json") return next();
|
|
285
|
+
if (typeof request.body === "string") request.body = Sapling.deserialize(request.body);
|
|
286
|
+
else if (typeof request.body === "object") {
|
|
287
|
+
const raw = JSON.stringify(request.body);
|
|
288
|
+
request.body = Sapling.deserialize(raw);
|
|
289
|
+
}
|
|
290
|
+
next();
|
|
291
|
+
} catch (err) {
|
|
292
|
+
next(err);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Register your application with all the necessary middlewares and logics for Sapling to function.
|
|
298
|
+
*
|
|
299
|
+
* @example```ts
|
|
300
|
+
* import { Sapling } from "@tahminator/sapling";
|
|
301
|
+
* import express from "express";
|
|
302
|
+
*
|
|
303
|
+
* const app = express();
|
|
304
|
+
*
|
|
305
|
+
* app.registerApp(app);
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
static registerApp(app) {
|
|
309
|
+
app.use(express.default.text({ type: "application/json" }));
|
|
310
|
+
app.use(Sapling.json());
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Register a middleware that will handle {@link ResponseStatusError}.
|
|
314
|
+
*
|
|
315
|
+
* This middleware will chain to the next middleware if it does not catch {@link ResponseStatusError}.
|
|
316
|
+
* You may still define middleware to handle all other errors in a separate `app.use` call.
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* ```ts
|
|
320
|
+
* import express from "express";
|
|
321
|
+
* import { Sapling } from "@tahminator/sapling";
|
|
322
|
+
*
|
|
323
|
+
* const app = express();
|
|
324
|
+
*
|
|
325
|
+
* Sapling.loadResponseStatusErrorMiddleware(app, (err, req, res, next) => {
|
|
326
|
+
* // `err` is guaranteed to be of type ResponseStatusError
|
|
327
|
+
* res.status(err.status).json({
|
|
328
|
+
* success: false,
|
|
329
|
+
* message: err.message,
|
|
330
|
+
* });
|
|
331
|
+
* });
|
|
332
|
+
* ```
|
|
333
|
+
*/
|
|
334
|
+
static loadResponseStatusErrorMiddleware(app, fn) {
|
|
335
|
+
app.use(((err, req, res, next) => {
|
|
336
|
+
if (err instanceof ResponseStatusError) fn(err, req, res, next);
|
|
337
|
+
else next(err);
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Serialize a value into a JSON string.
|
|
342
|
+
*
|
|
343
|
+
* This function is used in {@link ResponseEntity} to serialize the `body`.
|
|
344
|
+
*
|
|
345
|
+
* Use `setSerializeFn` to override underlying implementation.
|
|
346
|
+
*
|
|
347
|
+
* @defaultValue `JSON.stringify`
|
|
348
|
+
*/
|
|
349
|
+
static serialize(value) {
|
|
350
|
+
return settings.serialize(value);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Replace the function used for `serialize`.
|
|
354
|
+
*/
|
|
355
|
+
static setSerializeFn(fn) {
|
|
356
|
+
settings.serialize = fn;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* De-serialize a JSON string back to a JavaScript object.
|
|
360
|
+
*
|
|
361
|
+
* This function is used to de-serialize a string into a `body`.
|
|
362
|
+
*
|
|
363
|
+
* Use `setDeserializeFn` to override underlying implementation.
|
|
364
|
+
*
|
|
365
|
+
* @defaultValue `JSON.parse`
|
|
366
|
+
*/
|
|
367
|
+
static deserialize(value) {
|
|
368
|
+
return settings.deserialize(value);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Replace the function used for `deserialize`
|
|
372
|
+
*/
|
|
373
|
+
static setDeserializeFn(fn) {
|
|
374
|
+
settings.deserialize = fn;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
//#endregion
|
|
378
|
+
//#region src/types.ts
|
|
379
|
+
const methodResolve = {
|
|
380
|
+
GET: "get",
|
|
381
|
+
PUT: "put",
|
|
382
|
+
POST: "post",
|
|
383
|
+
DELETE: "delete",
|
|
384
|
+
OPTIONS: "options",
|
|
385
|
+
PATCH: "patch",
|
|
386
|
+
HEAD: "head",
|
|
387
|
+
USE: "use"
|
|
388
|
+
};
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region lib/weakmap.ts
|
|
391
|
+
/**
|
|
392
|
+
* WeakMap that is iterable.
|
|
393
|
+
*/
|
|
394
|
+
var IterableWeakMap = class IterableWeakMap {
|
|
395
|
+
#weakMap = /* @__PURE__ */ new WeakMap();
|
|
396
|
+
#refSet = /* @__PURE__ */ new Set();
|
|
397
|
+
#finalizationGroup = new FinalizationRegistry(IterableWeakMap.#cleanup);
|
|
398
|
+
static #cleanup(heldValue) {
|
|
399
|
+
heldValue.set.delete(heldValue.ref);
|
|
400
|
+
}
|
|
401
|
+
constructor(iterable) {
|
|
402
|
+
if (iterable) for (const [key, value] of iterable) this.set(key, value);
|
|
403
|
+
}
|
|
404
|
+
set(key, value) {
|
|
405
|
+
const ref = new WeakRef(key);
|
|
406
|
+
this.#weakMap.set(key, {
|
|
407
|
+
value,
|
|
408
|
+
ref
|
|
409
|
+
});
|
|
410
|
+
this.#refSet.add(ref);
|
|
411
|
+
this.#finalizationGroup.register(key, {
|
|
412
|
+
set: this.#refSet,
|
|
413
|
+
ref
|
|
414
|
+
}, ref);
|
|
415
|
+
return this;
|
|
416
|
+
}
|
|
417
|
+
get(key) {
|
|
418
|
+
return this.#weakMap.get(key)?.value;
|
|
419
|
+
}
|
|
420
|
+
delete(key) {
|
|
421
|
+
const entry = this.#weakMap.get(key);
|
|
422
|
+
if (!entry) return false;
|
|
423
|
+
this.#weakMap.delete(key);
|
|
424
|
+
this.#refSet.delete(entry.ref);
|
|
425
|
+
this.#finalizationGroup.unregister(entry.ref);
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
*[Symbol.iterator]() {
|
|
429
|
+
for (const ref of this.#refSet) {
|
|
430
|
+
const key = ref.deref();
|
|
431
|
+
if (!key) continue;
|
|
432
|
+
const entry = this.#weakMap.get(key);
|
|
433
|
+
if (entry) yield [key, entry.value];
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
entries() {
|
|
437
|
+
return this[Symbol.iterator]();
|
|
438
|
+
}
|
|
439
|
+
*keys() {
|
|
440
|
+
for (const [key] of this) yield key;
|
|
441
|
+
}
|
|
442
|
+
*values() {
|
|
443
|
+
for (const [, value] of this) yield value;
|
|
444
|
+
}
|
|
445
|
+
forEach(callback, thisArg) {
|
|
446
|
+
for (const [key, value] of this) callback.call(thisArg, value, key, this);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
//#endregion
|
|
450
|
+
//#region src/annotation/injectable.ts
|
|
451
|
+
const _InjectableRegistry = /* @__PURE__ */ new WeakMap();
|
|
452
|
+
const _InjectableDeps = new IterableWeakMap();
|
|
453
|
+
/**
|
|
454
|
+
* Mark the class as an injectable to be handled by Sapling. The class can now be
|
|
455
|
+
* be injected into other classes, as well as allow the class to inject other `@Injectable` classes.
|
|
456
|
+
*
|
|
457
|
+
* @argument deps - An optional array to define any dependencies that this class may require.
|
|
458
|
+
*/
|
|
459
|
+
function Injectable(deps = []) {
|
|
460
|
+
return function(target) {
|
|
461
|
+
_InjectableRegistry.set(target, null);
|
|
462
|
+
_InjectableDeps.set(target, deps);
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Resolves and instantiates a class along with all of it's transitive dependencies.
|
|
467
|
+
*
|
|
468
|
+
* Uses topological sort (Kahn's algorithm) to ensure that the dependency graph is created
|
|
469
|
+
* in a correct order.
|
|
470
|
+
*
|
|
471
|
+
* When `resolve` is first called (usually during controller registration),
|
|
472
|
+
* it will compute the dependency graph of all `@Injectable` classes and instantiates
|
|
473
|
+
* them in the correct order.
|
|
474
|
+
*
|
|
475
|
+
* Subsequent calls to dependencies that have already been resolved are cached, so they will
|
|
476
|
+
* re-use the created singletons instead of re-instantiation.
|
|
477
|
+
*/
|
|
478
|
+
function _resolve(ctor) {
|
|
479
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
480
|
+
const graph = /* @__PURE__ */ new Map();
|
|
481
|
+
_InjectableDeps.forEach((deps, node) => {
|
|
482
|
+
inDegree.set(node, inDegree.get(node) || 0);
|
|
483
|
+
deps.forEach((dep) => {
|
|
484
|
+
if (dep === void 0) throw new Error(`There is an @Injectable (${node.name}) which has a dependency that cannot be found. This is likely caused by a circular dependency.`);
|
|
485
|
+
inDegree.set(dep, inDegree.get(dep) || 0);
|
|
486
|
+
inDegree.set(node, inDegree.get(node) + 1);
|
|
487
|
+
if (!graph.has(dep)) graph.set(dep, []);
|
|
488
|
+
graph.get(dep).push(node);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
const queue = [];
|
|
492
|
+
inDegree.forEach((deg, node) => {
|
|
493
|
+
if (deg === 0) queue.push(node);
|
|
494
|
+
});
|
|
495
|
+
while (queue.length) {
|
|
496
|
+
const current = queue.shift();
|
|
497
|
+
if (!_InjectableRegistry.get(current)) {
|
|
498
|
+
const instance = new current(...(_InjectableDeps.get(current) || []).map((dep) => _InjectableRegistry.get(dep)));
|
|
499
|
+
_InjectableRegistry.set(current, instance);
|
|
500
|
+
}
|
|
501
|
+
(graph.get(current) || []).forEach((neighbor) => {
|
|
502
|
+
inDegree.set(neighbor, (inDegree.get(neighbor) ?? 0) - 1);
|
|
503
|
+
if (inDegree.get(neighbor) === 0) queue.push(neighbor);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
if (!_InjectableRegistry.get(ctor)) throw new Error("Circular dependency detected or injectable not registered");
|
|
507
|
+
return _InjectableRegistry.get(ctor);
|
|
508
|
+
}
|
|
509
|
+
//#endregion
|
|
510
|
+
//#region src/helper/error/exception.ts
|
|
511
|
+
/**
|
|
512
|
+
* This error should be thrown when some data cannot be parsed by a given schema.
|
|
513
|
+
*/
|
|
514
|
+
var ParserError = class ParserError extends ResponseStatusError {
|
|
515
|
+
constructor(location, issues, vendor) {
|
|
516
|
+
super(400, ParserError.formatMessage(location, issues, vendor));
|
|
517
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
518
|
+
}
|
|
519
|
+
static formatMessage(location, issues, vendor) {
|
|
520
|
+
const formatted = issues.map((i) => {
|
|
521
|
+
const path = Array.isArray(i.path) ? i.path.map((seg) => typeof seg === "object" && seg ? String(seg.key) : String(seg)).join(".") : "";
|
|
522
|
+
return path ? `${path}: ${i.message}` : i.message;
|
|
523
|
+
}).join("; ");
|
|
524
|
+
return `${vendor} failed to parse ${(() => {
|
|
525
|
+
switch (location) {
|
|
526
|
+
case "reqbody": return "request body";
|
|
527
|
+
case "reqparams": return "request params";
|
|
528
|
+
case "reqquery": return "request query";
|
|
529
|
+
}
|
|
530
|
+
})()}: ${formatted}`;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
//#endregion
|
|
534
|
+
//#region src/annotation/request.ts
|
|
535
|
+
const _requestSchemaStore = /* @__PURE__ */ new WeakMap();
|
|
536
|
+
function _getOrCreateRequestSchemaDefinition(ctor, fnName) {
|
|
537
|
+
const byFn = (() => {
|
|
538
|
+
const fn = _requestSchemaStore.get(ctor);
|
|
539
|
+
if (fn) return fn;
|
|
540
|
+
const newFn = /* @__PURE__ */ new Map();
|
|
541
|
+
_requestSchemaStore.set(ctor, newFn);
|
|
542
|
+
return newFn;
|
|
543
|
+
})();
|
|
544
|
+
const existing = byFn.get(fnName);
|
|
545
|
+
if (existing) return existing;
|
|
546
|
+
const created = {};
|
|
547
|
+
byFn.set(fnName, created);
|
|
548
|
+
return created;
|
|
549
|
+
}
|
|
550
|
+
function _setOnce(def, key, schema, fnName) {
|
|
551
|
+
if (def[key]) throw new Error(`Duplicate request schema for "${String(key)}" on method "${fnName}"`);
|
|
552
|
+
def[key] = schema;
|
|
553
|
+
}
|
|
554
|
+
function RequestBody(schema) {
|
|
555
|
+
return (target, propertyKey) => {
|
|
556
|
+
const ctor = target.constructor;
|
|
557
|
+
const fnName = String(propertyKey);
|
|
558
|
+
_setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "body", schema, fnName);
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
function RequestParam(schema) {
|
|
562
|
+
return (target, propertyKey) => {
|
|
563
|
+
const ctor = target.constructor;
|
|
564
|
+
const fnName = String(propertyKey);
|
|
565
|
+
_setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "param", schema, fnName);
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function RequestQuery(schema) {
|
|
569
|
+
return (target, propertyKey) => {
|
|
570
|
+
const ctor = target.constructor;
|
|
571
|
+
const fnName = String(propertyKey);
|
|
572
|
+
_setOnce(_getOrCreateRequestSchemaDefinition(ctor, fnName), "query", schema, fnName);
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function _getRequestSchemas(ctor, fnName) {
|
|
576
|
+
return _requestSchemaStore.get(ctor)?.get(fnName);
|
|
577
|
+
}
|
|
578
|
+
async function _parseOrThrow(schema, input, kind) {
|
|
579
|
+
const result = await schema["~standard"].validate(input);
|
|
580
|
+
if (result.issues) throw new ParserError(kind, result.issues, schema["~standard"].vendor);
|
|
581
|
+
return result.value;
|
|
582
|
+
}
|
|
583
|
+
//#endregion
|
|
584
|
+
//#region src/annotation/route.ts
|
|
585
|
+
const _routeStore = /* @__PURE__ */ new WeakMap();
|
|
586
|
+
/**
|
|
587
|
+
* Custom annotation that will store all routes inside of a map,
|
|
588
|
+
* which can then be used to initialize all the routes to the router.
|
|
589
|
+
*/
|
|
590
|
+
function _Route({ method, path = "" }) {
|
|
591
|
+
return (target, propertyKey) => {
|
|
592
|
+
const ctor = target.constructor;
|
|
593
|
+
const list = _routeStore.get(ctor) ?? [];
|
|
594
|
+
list.push({
|
|
595
|
+
method,
|
|
596
|
+
path: path ?? "",
|
|
597
|
+
fnName: String(propertyKey)
|
|
598
|
+
});
|
|
599
|
+
_routeStore.set(ctor, list);
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Register GET route on the given path (default "") for the given controller.
|
|
604
|
+
*/
|
|
605
|
+
const GET = (path = "") => _Route({
|
|
606
|
+
method: "GET",
|
|
607
|
+
path
|
|
608
|
+
});
|
|
609
|
+
/**
|
|
610
|
+
* Register POST route on the given path (default "") for the given controller.
|
|
611
|
+
*/
|
|
612
|
+
const POST = (path = "") => _Route({
|
|
613
|
+
method: "POST",
|
|
614
|
+
path
|
|
615
|
+
});
|
|
616
|
+
/**
|
|
617
|
+
* Register PUT route on the given path (default "") for the given controller.
|
|
618
|
+
*/
|
|
619
|
+
const PUT = (path = "") => _Route({
|
|
620
|
+
method: "PUT",
|
|
621
|
+
path
|
|
622
|
+
});
|
|
623
|
+
/**
|
|
624
|
+
* Register DELETE route on the given path (default "") for the given controller.
|
|
625
|
+
*/
|
|
626
|
+
const DELETE = (path = "") => _Route({
|
|
627
|
+
method: "DELETE",
|
|
628
|
+
path
|
|
629
|
+
});
|
|
630
|
+
/**
|
|
631
|
+
* Register OPTIONS route on the given path (default "") for the given controller.
|
|
632
|
+
*/
|
|
633
|
+
const OPTIONS = (path = "") => _Route({
|
|
634
|
+
method: "OPTIONS",
|
|
635
|
+
path
|
|
636
|
+
});
|
|
637
|
+
/**
|
|
638
|
+
* Register PATCH route on the given path (default "") for the given controller.
|
|
639
|
+
*/
|
|
640
|
+
const PATCH = (path = "") => _Route({
|
|
641
|
+
method: "PATCH",
|
|
642
|
+
path
|
|
643
|
+
});
|
|
644
|
+
/**
|
|
645
|
+
* Register HEAD route on the given path (default "") for the given controller.
|
|
646
|
+
*/
|
|
647
|
+
const HEAD = (path = "") => _Route({
|
|
648
|
+
method: "HEAD",
|
|
649
|
+
path
|
|
650
|
+
});
|
|
651
|
+
/**
|
|
652
|
+
* Register a middleware route on the given path (default "") for the given controller.
|
|
653
|
+
*/
|
|
654
|
+
const Middleware = (path = "") => _Route({
|
|
655
|
+
method: "USE",
|
|
656
|
+
path
|
|
657
|
+
});
|
|
658
|
+
/**
|
|
659
|
+
* Given a class constructor, fetch all the routes attached.
|
|
660
|
+
*/
|
|
661
|
+
function _getRoutes(ctor) {
|
|
662
|
+
return _routeStore.get(ctor) ?? [];
|
|
663
|
+
}
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region src/annotation/controller.ts
|
|
666
|
+
const _ControllerRegistry = /* @__PURE__ */ new WeakMap();
|
|
667
|
+
/**
|
|
668
|
+
* Registers a class as an HTTP controller and registers its routes.
|
|
669
|
+
*
|
|
670
|
+
* @param [prefix] Optional URL prefix applied to all routes in the controller. Defaults to "".
|
|
671
|
+
* @param [deps] Optional array of dependencies to be injected into the constructor that are `@Injectable`
|
|
672
|
+
*/
|
|
673
|
+
function Controller({ prefix = "", deps = [] } = {}) {
|
|
674
|
+
return (target) => {
|
|
675
|
+
const targetClass = target;
|
|
676
|
+
const router = (0, express.Router)();
|
|
677
|
+
const routes = _getRoutes(target);
|
|
678
|
+
const usedRoutes = /* @__PURE__ */ new Set();
|
|
679
|
+
_InjectableDeps.set(targetClass, deps);
|
|
680
|
+
const controllerInstance = _resolve(targetClass);
|
|
681
|
+
for (const { method, path, fnName } of routes) {
|
|
682
|
+
const fn = controllerInstance[fnName];
|
|
683
|
+
if (typeof fn !== "function") continue;
|
|
684
|
+
const fp = path instanceof RegExp ? path : prefix + path;
|
|
685
|
+
const routeKey = method + " " + (path instanceof RegExp ? path.source : fp);
|
|
686
|
+
if (method !== "USE" && usedRoutes.has(routeKey)) throw new Error(`Duplicate route [${method}] "${path instanceof RegExp ? path.source : fp}" detected in controller "${target.name}"`);
|
|
687
|
+
if (method !== "USE") usedRoutes.add(routeKey);
|
|
688
|
+
const methodName = methodResolve[method];
|
|
689
|
+
router[methodName](fp, async (request, response, next) => {
|
|
690
|
+
const schemas = _getRequestSchemas(target, fnName);
|
|
691
|
+
if (schemas) {
|
|
692
|
+
if (schemas.body) request.body = await _parseOrThrow(schemas.body, request.body, "reqbody");
|
|
693
|
+
if (schemas.param) request.params = await _parseOrThrow(schemas.param, request.params, "reqparams");
|
|
694
|
+
if (schemas.query) request.query = await _parseOrThrow(schemas.query, request.query, "reqquery");
|
|
695
|
+
}
|
|
696
|
+
const result = await fn.bind(controllerInstance)(request, response, next);
|
|
697
|
+
if (method === "USE") return;
|
|
698
|
+
if (result instanceof ResponseEntity) {
|
|
699
|
+
response.contentType("application/json").status(result.getStatusCode()).set(result.getHeaders()).send(Sapling.serialize(result.getBody()));
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (result instanceof RedirectView) {
|
|
703
|
+
response.redirect(result.getUrl());
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (!response.writableEnded) response.status(404).send(Html404ErrorPage(`Cannot ${methodName.toUpperCase()} ${path instanceof RegExp ? path.source : fp}`));
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
_ControllerRegistry.set(targetClass, router);
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
//#endregion
|
|
713
|
+
//#region src/annotation/middleware.ts
|
|
714
|
+
/**
|
|
715
|
+
* Used to define a middleware-only class.
|
|
716
|
+
*
|
|
717
|
+
* __NOTE:__ `@MiddlewareClass` works exactly the same as `@Controller`. As such, you
|
|
718
|
+
* can still register `@Route` and `@Middleware` methods, though you very well should not
|
|
719
|
+
* for the sake of semantics.
|
|
720
|
+
*/
|
|
721
|
+
function MiddlewareClass(...args) {
|
|
722
|
+
return Controller(...args);
|
|
723
|
+
}
|
|
724
|
+
//#endregion
|
|
725
|
+
exports.Controller = Controller;
|
|
726
|
+
exports.DELETE = DELETE;
|
|
727
|
+
exports.GET = GET;
|
|
728
|
+
exports.HEAD = HEAD;
|
|
729
|
+
exports.Html404ErrorPage = Html404ErrorPage;
|
|
730
|
+
exports.HttpStatus = HttpStatus;
|
|
731
|
+
exports.Injectable = Injectable;
|
|
732
|
+
exports.Middleware = Middleware;
|
|
733
|
+
exports.MiddlewareClass = MiddlewareClass;
|
|
734
|
+
exports.OPTIONS = OPTIONS;
|
|
735
|
+
exports.PATCH = PATCH;
|
|
736
|
+
exports.POST = POST;
|
|
737
|
+
exports.PUT = PUT;
|
|
738
|
+
exports.ParserError = ParserError;
|
|
739
|
+
exports.RedirectView = RedirectView;
|
|
740
|
+
exports.RequestBody = RequestBody;
|
|
741
|
+
exports.RequestParam = RequestParam;
|
|
742
|
+
exports.RequestQuery = RequestQuery;
|
|
743
|
+
exports.ResponseEntity = ResponseEntity;
|
|
744
|
+
exports.ResponseEntityBuilder = ResponseEntityBuilder;
|
|
745
|
+
exports.ResponseStatusError = ResponseStatusError;
|
|
746
|
+
exports.Sapling = Sapling;
|
|
747
|
+
exports._ControllerRegistry = _ControllerRegistry;
|
|
748
|
+
exports._InjectableDeps = _InjectableDeps;
|
|
749
|
+
exports._InjectableRegistry = _InjectableRegistry;
|
|
750
|
+
exports._Route = _Route;
|
|
751
|
+
exports._getRequestSchemas = _getRequestSchemas;
|
|
752
|
+
exports._getRoutes = _getRoutes;
|
|
753
|
+
exports._parseOrThrow = _parseOrThrow;
|
|
754
|
+
exports._resolve = _resolve;
|
|
755
|
+
exports.methodResolve = methodResolve;
|