@faasjs/core 8.0.0-beta.8 → 8.0.0-beta.9
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 +72 -3
- package/dist/index.cjs +1580 -26
- package/dist/index.d.ts +722 -9
- package/dist/index.mjs +1549 -24
- package/package.json +30 -24
package/dist/index.cjs
CHANGED
|
@@ -26,16 +26,1534 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
26
|
}) : target, mod));
|
|
27
27
|
|
|
28
28
|
//#endregion
|
|
29
|
-
let
|
|
30
|
-
let
|
|
29
|
+
let node_crypto = require("node:crypto");
|
|
30
|
+
let node_url = require("node:url");
|
|
31
|
+
let _faasjs_node_utils = require("@faasjs/node-utils");
|
|
31
32
|
let zod = require("zod");
|
|
32
33
|
zod = __toESM(zod);
|
|
34
|
+
let node_zlib = require("node:zlib");
|
|
35
|
+
let node_fs = require("node:fs");
|
|
36
|
+
let node_path = require("node:path");
|
|
37
|
+
let mime_types = require("mime-types");
|
|
38
|
+
let node_child_process = require("node:child_process");
|
|
39
|
+
let node_http = require("node:http");
|
|
40
|
+
let node_stream = require("node:stream");
|
|
41
|
+
let node_util = require("node:util");
|
|
42
|
+
let knex = require("knex");
|
|
43
|
+
knex = __toESM(knex);
|
|
33
44
|
|
|
45
|
+
//#region src/plugins/run_handler/index.ts
|
|
46
|
+
const Name$2 = "handler";
|
|
47
|
+
var RunHandler = class {
|
|
48
|
+
type = Name$2;
|
|
49
|
+
name = Name$2;
|
|
50
|
+
async onInvoke(data, next) {
|
|
51
|
+
if (data.handler) if (!data.runHandler) {
|
|
52
|
+
try {
|
|
53
|
+
data.response = await new Promise((resolve, reject) => {
|
|
54
|
+
data.callback = (error, result) => {
|
|
55
|
+
if (error) reject(error);
|
|
56
|
+
else resolve(result);
|
|
57
|
+
};
|
|
58
|
+
Promise.resolve(data.handler?.(data)).then(resolve).catch(reject);
|
|
59
|
+
});
|
|
60
|
+
} catch (error) {
|
|
61
|
+
data.logger.error(error);
|
|
62
|
+
data.response = error;
|
|
63
|
+
}
|
|
64
|
+
data.runHandler = true;
|
|
65
|
+
} else data.logger.warn("handler has been run");
|
|
66
|
+
await next();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/utils.ts
|
|
72
|
+
/**
|
|
73
|
+
* Assigns a name to a given function handler, which will be displayed in logs and error messages.
|
|
74
|
+
*
|
|
75
|
+
* @template T - The type of the function handler.
|
|
76
|
+
* @param {string} name - The name to assign to the function handler.
|
|
77
|
+
* @param {T} handler - The function handler to which the name will be assigned.
|
|
78
|
+
* @returns {T} - The original function handler with the assigned name.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* import { nameFunc } from '@faasjs/core'
|
|
83
|
+
*
|
|
84
|
+
* const handler = nameFunc('myHandler', () => {
|
|
85
|
+
* return 'Hello World'
|
|
86
|
+
* })
|
|
87
|
+
*
|
|
88
|
+
* console.log(handler.name) // => 'myHandler'
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
function nameFunc(name, handler) {
|
|
92
|
+
Object.defineProperty(handler, "name", { value: name });
|
|
93
|
+
return handler;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/func.ts
|
|
98
|
+
function parseFuncFilenameFromStack(stack) {
|
|
99
|
+
if (!stack) return;
|
|
100
|
+
const frame = stack.split("\n").map((line) => line.trim()).find((line) => line.includes(".func.ts"));
|
|
101
|
+
if (!frame) return;
|
|
102
|
+
const content = frame.replace(/^at\s+/, "");
|
|
103
|
+
const match = (content.endsWith(")") && content.includes("(") ? content.slice(content.lastIndexOf("(") + 1, -1) : content).match(/^(.+\.func\.ts):\d+:\d+$/);
|
|
104
|
+
if (!match) return;
|
|
105
|
+
const filename = match[1];
|
|
106
|
+
if (filename.startsWith("file://")) try {
|
|
107
|
+
return (0, node_url.fileURLToPath)(filename);
|
|
108
|
+
} catch (_) {
|
|
109
|
+
return filename;
|
|
110
|
+
}
|
|
111
|
+
return filename;
|
|
112
|
+
}
|
|
113
|
+
function normalizeMountData(data, options) {
|
|
114
|
+
const mountData = data || Object.create(null);
|
|
115
|
+
const ensureEventAndContext = options.ensureEventAndContext ?? true;
|
|
116
|
+
if (!mountData.config) mountData.config = options.defaultConfig || Object.create(null);
|
|
117
|
+
if (ensureEventAndContext) {
|
|
118
|
+
if (!mountData.context) mountData.context = Object.create(null);
|
|
119
|
+
if (!mountData.event) mountData.event = Object.create(null);
|
|
120
|
+
}
|
|
121
|
+
if (!mountData.logger) mountData.logger = new _faasjs_node_utils.Logger(options.loggerLabel);
|
|
122
|
+
return mountData;
|
|
123
|
+
}
|
|
124
|
+
var Func = class {
|
|
125
|
+
plugins;
|
|
126
|
+
handler;
|
|
127
|
+
config;
|
|
128
|
+
mounted = false;
|
|
129
|
+
filename;
|
|
130
|
+
cachedFunctions = Object.create(null);
|
|
131
|
+
/**
|
|
132
|
+
* Create a cloud function.
|
|
133
|
+
*/
|
|
134
|
+
constructor(config) {
|
|
135
|
+
if (config.handler) this.handler = config.handler;
|
|
136
|
+
this.plugins = config.plugins || [];
|
|
137
|
+
this.plugins.push(new RunHandler());
|
|
138
|
+
this.config = { plugins: Object.create(null) };
|
|
139
|
+
try {
|
|
140
|
+
const filename = parseFuncFilenameFromStack((/* @__PURE__ */ new Error()).stack);
|
|
141
|
+
if (filename) this.filename = filename;
|
|
142
|
+
} catch (_) {}
|
|
143
|
+
}
|
|
144
|
+
getCachedFunctions(key) {
|
|
145
|
+
const cached = this.cachedFunctions[key];
|
|
146
|
+
if (cached) return cached;
|
|
147
|
+
const list = [];
|
|
148
|
+
for (const plugin of this.plugins) {
|
|
149
|
+
const handler = plugin[key];
|
|
150
|
+
if (typeof handler !== "function") continue;
|
|
151
|
+
list.push({
|
|
152
|
+
key: plugin.name,
|
|
153
|
+
handler: handler.bind(plugin)
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
this.cachedFunctions[key] = list;
|
|
157
|
+
return list;
|
|
158
|
+
}
|
|
159
|
+
compose(key) {
|
|
160
|
+
const list = this.getCachedFunctions(key);
|
|
161
|
+
return async (data, next) => {
|
|
162
|
+
let index = -1;
|
|
163
|
+
if (!data.logger) data.logger = new _faasjs_node_utils.Logger();
|
|
164
|
+
const dispatch = async (i) => {
|
|
165
|
+
if (i <= index) return Promise.reject(Error("next() called multiple times"));
|
|
166
|
+
index = i;
|
|
167
|
+
let fn = list[i];
|
|
168
|
+
if (i === list.length) fn = next;
|
|
169
|
+
if (!fn) return Promise.resolve();
|
|
170
|
+
if (typeof fn.key === "undefined") fn.key = `uname#${i}`;
|
|
171
|
+
if (!data.context) data.context = Object.create(null);
|
|
172
|
+
if (!data.context.request_at) data.context.request_at = (0, node_crypto.randomBytes)(16).toString("hex");
|
|
173
|
+
const label = `${data.context.request_id}] [${fn.key}] [${key}`;
|
|
174
|
+
data.logger.label = label;
|
|
175
|
+
data.logger.debug("begin");
|
|
176
|
+
data.logger.time(label);
|
|
177
|
+
try {
|
|
178
|
+
const res = await Promise.resolve(fn.handler(data, dispatch.bind(null, i + 1)));
|
|
179
|
+
data.logger.label = label;
|
|
180
|
+
data.logger.timeEnd(label, "end");
|
|
181
|
+
return res;
|
|
182
|
+
} catch (err) {
|
|
183
|
+
data.logger.label = label;
|
|
184
|
+
data.logger.timeEnd(label, "failed");
|
|
185
|
+
data.logger.error(err);
|
|
186
|
+
return Promise.reject(err);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
return await dispatch(0);
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* First time mount the function.
|
|
194
|
+
*/
|
|
195
|
+
async mount(data = {
|
|
196
|
+
event: Object.create(null),
|
|
197
|
+
context: Object.create(null)
|
|
198
|
+
}) {
|
|
199
|
+
const mountData = normalizeMountData(data, {
|
|
200
|
+
loggerLabel: "Func",
|
|
201
|
+
defaultConfig: this.config,
|
|
202
|
+
ensureEventAndContext: false
|
|
203
|
+
});
|
|
204
|
+
if (this.mounted) {
|
|
205
|
+
mountData.logger.warn("mount() has been called, skipped.");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
mountData.logger.debug(`plugins: ${this.plugins.map((p) => `${p.type}#${p.name}`).join(",")}`);
|
|
209
|
+
await this.compose("onMount")(mountData);
|
|
210
|
+
this.mounted = true;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Invoke the function.
|
|
214
|
+
*/
|
|
215
|
+
async invoke(data) {
|
|
216
|
+
if (!this.mounted) await this.mount(data);
|
|
217
|
+
try {
|
|
218
|
+
await this.compose("onInvoke")(data);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
data.logger.error(error);
|
|
221
|
+
data.response = error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Export the function.
|
|
226
|
+
*/
|
|
227
|
+
export() {
|
|
228
|
+
const handler = async (event, context, callback) => {
|
|
229
|
+
if (typeof context === "undefined") context = {};
|
|
230
|
+
if (!context.request_id) context.request_id = event?.headers?.["x-faasjs-request-id"] || (0, node_crypto.randomBytes)(16).toString("hex");
|
|
231
|
+
if (!context.request_at) context.request_at = (0, node_crypto.randomBytes)(16).toString("hex");
|
|
232
|
+
context.callbackWaitsForEmptyEventLoop = false;
|
|
233
|
+
const logger = new _faasjs_node_utils.Logger(context.request_id);
|
|
234
|
+
const data = {
|
|
235
|
+
event: event ?? Object.create(null),
|
|
236
|
+
context,
|
|
237
|
+
callback,
|
|
238
|
+
response: void 0,
|
|
239
|
+
logger,
|
|
240
|
+
config: this.config,
|
|
241
|
+
...this.handler ? { handler: this.handler } : {}
|
|
242
|
+
};
|
|
243
|
+
await this.invoke(data);
|
|
244
|
+
if (Object.prototype.toString.call(data.response) === "[object Error]") throw data.response;
|
|
245
|
+
return data.response;
|
|
246
|
+
};
|
|
247
|
+
return { handler: handler.bind(this) };
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
let plugins = [];
|
|
251
|
+
function usePlugin(plugin) {
|
|
252
|
+
if (!plugins.find((p) => p.name === plugin.name)) plugins.push(plugin);
|
|
253
|
+
if (!plugin.mount) plugin.mount = async (data) => {
|
|
254
|
+
const mountData = normalizeMountData(data, { loggerLabel: plugin.name });
|
|
255
|
+
if (plugin.onMount) await plugin.onMount(mountData, async () => Promise.resolve());
|
|
256
|
+
return plugin;
|
|
257
|
+
};
|
|
258
|
+
return plugin;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Create a cloud function.
|
|
262
|
+
*/
|
|
263
|
+
function useFunc(handler) {
|
|
264
|
+
plugins = [];
|
|
265
|
+
const invokeHandler = handler();
|
|
266
|
+
const func = new Func({
|
|
267
|
+
plugins,
|
|
268
|
+
handler: invokeHandler
|
|
269
|
+
});
|
|
270
|
+
plugins = [];
|
|
271
|
+
return func;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/http/session.ts
|
|
276
|
+
var Session = class {
|
|
277
|
+
content;
|
|
278
|
+
config;
|
|
279
|
+
secret;
|
|
280
|
+
signedSecret;
|
|
281
|
+
cookie;
|
|
282
|
+
changed;
|
|
283
|
+
constructor(cookie, config) {
|
|
284
|
+
this.cookie = cookie;
|
|
285
|
+
if (!config?.secret) cookie.logger?.warn("Session's secret is missing.");
|
|
286
|
+
this.config = Object.assign({
|
|
287
|
+
key: "key",
|
|
288
|
+
secret: (0, node_crypto.randomBytes)(128).toString("hex"),
|
|
289
|
+
salt: "salt",
|
|
290
|
+
signedSalt: "signedSalt",
|
|
291
|
+
keylen: 64,
|
|
292
|
+
iterations: 100,
|
|
293
|
+
digest: "sha256",
|
|
294
|
+
cipherName: "aes-256-cbc"
|
|
295
|
+
}, config);
|
|
296
|
+
this.secret = (0, node_crypto.pbkdf2Sync)(this.config.secret, this.config.salt, this.config.iterations, this.config.keylen / 2, this.config.digest);
|
|
297
|
+
this.signedSecret = (0, node_crypto.pbkdf2Sync)(this.config.secret, this.config.signedSalt, this.config.iterations, this.config.keylen, this.config.digest);
|
|
298
|
+
this.content = Object.create(null);
|
|
299
|
+
}
|
|
300
|
+
invoke(cookie, logger) {
|
|
301
|
+
try {
|
|
302
|
+
this.content = cookie ? this.decode(cookie) : Object.create(null);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
logger?.error(error);
|
|
305
|
+
this.content = Object.create(null);
|
|
306
|
+
}
|
|
307
|
+
this.changed = false;
|
|
308
|
+
}
|
|
309
|
+
encode(text) {
|
|
310
|
+
if (typeof text !== "string") text = JSON.stringify(text);
|
|
311
|
+
const iv = (0, node_crypto.randomBytes)(16);
|
|
312
|
+
const cipher = (0, node_crypto.createCipheriv)(this.config.cipherName, this.secret, iv);
|
|
313
|
+
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]).toString("base64");
|
|
314
|
+
const main = Buffer.from([encrypted, iv.toString("base64")].join("--")).toString("base64");
|
|
315
|
+
const hmac = (0, node_crypto.createHmac)(this.config.digest, this.signedSecret);
|
|
316
|
+
hmac.update(main);
|
|
317
|
+
return `${main}--${hmac.digest("hex")}`;
|
|
318
|
+
}
|
|
319
|
+
decode(text) {
|
|
320
|
+
text = decodeURIComponent(text);
|
|
321
|
+
const signedParts = text.split("--");
|
|
322
|
+
const hmac = (0, node_crypto.createHmac)(this.config.digest, this.signedSecret);
|
|
323
|
+
hmac.update(signedParts[0]);
|
|
324
|
+
const digest = hmac.digest("hex");
|
|
325
|
+
if (signedParts[1] !== digest) throw Error("Session Not valid");
|
|
326
|
+
const parts = Buffer.from(signedParts[0], "base64").toString().split("--").map((part) => Buffer.from(part, "base64"));
|
|
327
|
+
const cipher = (0, node_crypto.createDecipheriv)(this.config.cipherName, this.secret, parts[1]);
|
|
328
|
+
const decrypt = [Buffer.from(cipher.update(parts[0])).toString("utf8"), cipher.final("utf8")].join("");
|
|
329
|
+
return JSON.parse(decrypt);
|
|
330
|
+
}
|
|
331
|
+
read(key) {
|
|
332
|
+
return this.content[key];
|
|
333
|
+
}
|
|
334
|
+
write(key, value) {
|
|
335
|
+
if (value === null || typeof value === "undefined") delete this.content[key];
|
|
336
|
+
else this.content[key] = value;
|
|
337
|
+
this.changed = true;
|
|
338
|
+
return this;
|
|
339
|
+
}
|
|
340
|
+
update() {
|
|
341
|
+
if (this.changed) this.cookie.write(this.config.key, this.encode(this.content));
|
|
342
|
+
return this;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region src/http/cookie.ts
|
|
348
|
+
var Cookie = class {
|
|
349
|
+
session;
|
|
350
|
+
content;
|
|
351
|
+
config;
|
|
352
|
+
logger;
|
|
353
|
+
setCookie;
|
|
354
|
+
constructor(config, logger) {
|
|
355
|
+
this.logger = logger;
|
|
356
|
+
this.config = (0, _faasjs_node_utils.deepMerge)({
|
|
357
|
+
path: "/",
|
|
358
|
+
expires: 31536e3,
|
|
359
|
+
secure: true,
|
|
360
|
+
httpOnly: true,
|
|
361
|
+
session: {}
|
|
362
|
+
}, config);
|
|
363
|
+
this.session = new Session(this, this.config.session);
|
|
364
|
+
this.content = Object.create(null);
|
|
365
|
+
this.setCookie = Object.create(null);
|
|
366
|
+
}
|
|
367
|
+
invoke(cookie, logger) {
|
|
368
|
+
this.content = Object.create(null);
|
|
369
|
+
if (cookie) for (const x of cookie.split(";")) {
|
|
370
|
+
const trimX = x.trim();
|
|
371
|
+
const k = /([^=]+)/.exec(trimX);
|
|
372
|
+
if (k !== null) this.content[k[0]] = decodeURIComponent(trimX.replace(`${k[0]}=`, "").replace(/;$/, ""));
|
|
373
|
+
}
|
|
374
|
+
this.setCookie = Object.create(null);
|
|
375
|
+
this.session.invoke(this.read(this.session.config.key), logger);
|
|
376
|
+
return this;
|
|
377
|
+
}
|
|
378
|
+
read(key) {
|
|
379
|
+
return this.content[key];
|
|
380
|
+
}
|
|
381
|
+
write(key, value, opts) {
|
|
382
|
+
const options = Object.assign({}, this.config, opts || {});
|
|
383
|
+
let cookie;
|
|
384
|
+
if (value === null || typeof value === "undefined") {
|
|
385
|
+
cookie = `${key}=;`;
|
|
386
|
+
cookie += "expires=Thu, 01 Jan 1970 00:00:01 GMT;";
|
|
387
|
+
delete this.content[key];
|
|
388
|
+
} else {
|
|
389
|
+
cookie = `${key}=${encodeURIComponent(value)};`;
|
|
390
|
+
this.content[key] = value;
|
|
391
|
+
if (typeof options.expires === "number") cookie += `max-age=${options.expires};`;
|
|
392
|
+
else if (typeof options.expires === "string") cookie += `expires=${options.expires};`;
|
|
393
|
+
}
|
|
394
|
+
cookie += `path=${options.path || "/"};`;
|
|
395
|
+
if (options.domain) cookie += `domain=${options.domain};`;
|
|
396
|
+
if (options.secure) cookie += "Secure;";
|
|
397
|
+
if (options.httpOnly) cookie += "HttpOnly;";
|
|
398
|
+
if (options.sameSite) cookie += `SameSite=${options.sameSite};`;
|
|
399
|
+
this.setCookie[key] = cookie;
|
|
400
|
+
return this;
|
|
401
|
+
}
|
|
402
|
+
headers() {
|
|
403
|
+
if (Object.keys(this.setCookie).length === 0) return {};
|
|
404
|
+
return { "Set-Cookie": Object.values(this.setCookie) };
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
//#endregion
|
|
409
|
+
//#region src/http/index.ts
|
|
410
|
+
const ContentType = {
|
|
411
|
+
plain: "text/plain",
|
|
412
|
+
html: "text/html",
|
|
413
|
+
xml: "application/xml",
|
|
414
|
+
csv: "text/csv",
|
|
415
|
+
css: "text/css",
|
|
416
|
+
javascript: "application/javascript",
|
|
417
|
+
json: "application/json",
|
|
418
|
+
jsonp: "application/javascript"
|
|
419
|
+
};
|
|
420
|
+
var HttpError = class HttpError extends Error {
|
|
421
|
+
statusCode;
|
|
422
|
+
message;
|
|
423
|
+
constructor({ statusCode, message }) {
|
|
424
|
+
super(message);
|
|
425
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, HttpError);
|
|
426
|
+
this.statusCode = statusCode || 500;
|
|
427
|
+
this.message = message;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
const Name$1 = "http";
|
|
431
|
+
function createCompressedStream(body, encoding) {
|
|
432
|
+
const compressStream = encoding === "br" ? (0, node_zlib.createBrotliCompress)() : encoding === "gzip" ? (0, node_zlib.createGzip)() : (0, node_zlib.createDeflate)();
|
|
433
|
+
return new ReadableStream({ async start(controller) {
|
|
434
|
+
try {
|
|
435
|
+
const compressed = await new Promise((resolve, reject) => {
|
|
436
|
+
const chunks = [];
|
|
437
|
+
compressStream.on("data", (chunk) => chunks.push(chunk));
|
|
438
|
+
compressStream.on("end", () => resolve(Buffer.concat(chunks)));
|
|
439
|
+
compressStream.on("error", reject);
|
|
440
|
+
compressStream.write(Buffer.from(body));
|
|
441
|
+
compressStream.end();
|
|
442
|
+
});
|
|
443
|
+
const chunkSize = 16 * 1024;
|
|
444
|
+
for (let i = 0; i < compressed.length; i += chunkSize) controller.enqueue(compressed.subarray(i, i + chunkSize));
|
|
445
|
+
controller.close();
|
|
446
|
+
} catch (error) {
|
|
447
|
+
controller.error(error);
|
|
448
|
+
}
|
|
449
|
+
} });
|
|
450
|
+
}
|
|
451
|
+
var Http = class {
|
|
452
|
+
type = "http";
|
|
453
|
+
name = Name$1;
|
|
454
|
+
headers = Object.create(null);
|
|
455
|
+
body;
|
|
456
|
+
params = Object.create(null);
|
|
457
|
+
cookie;
|
|
458
|
+
session;
|
|
459
|
+
config;
|
|
460
|
+
response;
|
|
461
|
+
constructor(config) {
|
|
462
|
+
this.name = config?.name || this.type;
|
|
463
|
+
this.config = config?.config || Object.create(null);
|
|
464
|
+
this.cookie = new Cookie(this.config.cookie || {});
|
|
465
|
+
this.session = this.cookie.session;
|
|
466
|
+
this.response = { headers: Object.create(null) };
|
|
467
|
+
}
|
|
468
|
+
async onMount(data, next) {
|
|
469
|
+
data.logger.debug("merge config");
|
|
470
|
+
const prefix = `SECRET_${this.name.toUpperCase()}_`;
|
|
471
|
+
for (let key in process.env) if (key.startsWith(prefix)) {
|
|
472
|
+
const value = process.env[key];
|
|
473
|
+
key = key.replace(prefix, "").toLowerCase();
|
|
474
|
+
if (key.includes("_")) {
|
|
475
|
+
let config = this.config;
|
|
476
|
+
const keys = key.split("_");
|
|
477
|
+
for (const k of keys.slice(0, keys.length - 1)) {
|
|
478
|
+
if (!config[k]) config[k] = Object.create(null);
|
|
479
|
+
config = config[k];
|
|
480
|
+
}
|
|
481
|
+
config[keys[keys.length - 1]] = value;
|
|
482
|
+
} else this.config[key] = value;
|
|
483
|
+
}
|
|
484
|
+
if (!data.config) throw Error(`[${this.name}] Config not found.`);
|
|
485
|
+
if (data.config.plugins?.[this.name || this.type]) this.config = (0, _faasjs_node_utils.deepMerge)(this.config, data.config.plugins[this.name || this.type].config);
|
|
486
|
+
data.logger.debug("prepare cookie & session");
|
|
487
|
+
this.cookie = new Cookie(this.config.cookie || {}, data.logger);
|
|
488
|
+
this.session = this.cookie.session;
|
|
489
|
+
await next();
|
|
490
|
+
}
|
|
491
|
+
async onInvoke(data, next) {
|
|
492
|
+
this.headers = data.event.headers || Object.create(null);
|
|
493
|
+
this.body = data.event.body;
|
|
494
|
+
this.params = data.event.queryString || Object.create(null);
|
|
495
|
+
this.response = { headers: Object.create(null) };
|
|
496
|
+
this.parseEventParams(data);
|
|
497
|
+
data.params = data.event.params;
|
|
498
|
+
this.cookie.invoke(this.headers.cookie, data.logger);
|
|
499
|
+
if (this.headers.cookie) {
|
|
500
|
+
data.logger.debug("Cookie: %j", this.cookie.content);
|
|
501
|
+
data.logger.debug("Session: %s %j", this.session.config.key, this.session.content);
|
|
502
|
+
}
|
|
503
|
+
data.cookie = this.cookie;
|
|
504
|
+
data.session = this.session;
|
|
505
|
+
try {
|
|
506
|
+
await next();
|
|
507
|
+
} catch (error) {
|
|
508
|
+
data.response = error;
|
|
509
|
+
}
|
|
510
|
+
this.session.update();
|
|
511
|
+
this.buildResponse(data);
|
|
512
|
+
this.finalizeBody(data);
|
|
513
|
+
}
|
|
514
|
+
parseEventParams(data) {
|
|
515
|
+
if (!data.event.body) return;
|
|
516
|
+
if (this.headers["content-type"]?.includes("application/json") && typeof data.event.body === "string" && data.event.body.length > 1) {
|
|
517
|
+
data.logger.debug("Parse params from json body");
|
|
518
|
+
try {
|
|
519
|
+
this.params = Object.keys(this.params).length ? Object.assign(this.params, JSON.parse(data.event.body)) : JSON.parse(data.event.body);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
data.logger.error("Parse params from json body failed: %s", error.message);
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
data.logger.debug("Parse params from raw body");
|
|
525
|
+
this.params = data.event.body || Object.create(null);
|
|
526
|
+
}
|
|
527
|
+
if (this.params && typeof this.params === "object" && this.params._) delete this.params._;
|
|
528
|
+
if (this.params && typeof this.params === "object") try {
|
|
529
|
+
data.event.params = structuredClone(this.params);
|
|
530
|
+
} catch {
|
|
531
|
+
data.event.params = this.params;
|
|
532
|
+
}
|
|
533
|
+
else data.event.params = this.params;
|
|
534
|
+
data.logger.debug("Params: %j", this.params);
|
|
535
|
+
}
|
|
536
|
+
buildResponse(data) {
|
|
537
|
+
if (this.response.body && !data.response) data.response = this.response.body;
|
|
538
|
+
if (data.response) if (data.response instanceof Error || data.response.constructor?.name === "Error") {
|
|
539
|
+
data.logger.error(data.response);
|
|
540
|
+
this.response.body = JSON.stringify({ error: { message: data.response.message } });
|
|
541
|
+
try {
|
|
542
|
+
this.response.statusCode = data.response.statusCode || 500;
|
|
543
|
+
} catch (error) {
|
|
544
|
+
data.logger.error(error);
|
|
545
|
+
this.response.statusCode = 500;
|
|
546
|
+
}
|
|
547
|
+
} else if (Object.prototype.toString.call(data.response) === "[object Object]" && data.response.statusCode && data.response.headers) this.response = data.response;
|
|
548
|
+
else if (data.response instanceof ReadableStream) this.response.body = data.response;
|
|
549
|
+
else this.response.body = JSON.stringify({ data: data.response === void 0 ? null : data.response });
|
|
550
|
+
if (!this.response.statusCode) this.response.statusCode = data.response ? 200 : 204;
|
|
551
|
+
this.response.headers = Object.assign({
|
|
552
|
+
"content-type": this.response.body instanceof ReadableStream ? "text/plain; charset=utf-8" : "application/json; charset=utf-8",
|
|
553
|
+
"cache-control": "no-cache, no-store",
|
|
554
|
+
"x-faasjs-request-id": data.context.request_id
|
|
555
|
+
}, this.cookie.headers(), this.response.headers);
|
|
556
|
+
data.response = Object.assign({}, data.response, this.response);
|
|
557
|
+
}
|
|
558
|
+
finalizeBody(data) {
|
|
559
|
+
const originBody = data.response.body;
|
|
560
|
+
data.response.originBody = originBody;
|
|
561
|
+
if (originBody instanceof ReadableStream) {
|
|
562
|
+
data.response.isBase64Encoded = true;
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (originBody === void 0 && data.response.statusCode === 204) return;
|
|
566
|
+
const normalizedBody = originBody === void 0 ? JSON.stringify({ data: null }) : typeof originBody === "string" ? originBody : JSON.stringify(originBody);
|
|
567
|
+
if (normalizedBody.length < 1024) {
|
|
568
|
+
data.response.body = new ReadableStream({ start(controller) {
|
|
569
|
+
try {
|
|
570
|
+
controller.enqueue(new TextEncoder().encode(normalizedBody));
|
|
571
|
+
controller.close();
|
|
572
|
+
} catch (error) {
|
|
573
|
+
controller.error(error);
|
|
574
|
+
}
|
|
575
|
+
} });
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
data.response.body = normalizedBody;
|
|
579
|
+
const acceptEncoding = this.headers["accept-encoding"] || this.headers["Accept-Encoding"];
|
|
580
|
+
if (!acceptEncoding || !/(br|gzip|deflate)/.test(acceptEncoding)) return;
|
|
581
|
+
const encoding = acceptEncoding.includes("br") ? "br" : acceptEncoding.includes("gzip") ? "gzip" : "deflate";
|
|
582
|
+
data.response.headers["Content-Encoding"] = encoding;
|
|
583
|
+
try {
|
|
584
|
+
data.response.body = createCompressedStream(normalizedBody, encoding);
|
|
585
|
+
} catch (error) {
|
|
586
|
+
data.logger.error("Compression failed: %s", error.message);
|
|
587
|
+
data.response.body = normalizedBody;
|
|
588
|
+
delete data.response.headers["Content-Encoding"];
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* set header
|
|
593
|
+
* @param key {string} key
|
|
594
|
+
* @param value {string} value
|
|
595
|
+
*/
|
|
596
|
+
setHeader(key, value) {
|
|
597
|
+
(this.response.headers || (this.response.headers = Object.create(null)))[key.toLowerCase()] = value;
|
|
598
|
+
return this;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* set Content-Type
|
|
602
|
+
* @param type {string} 类型
|
|
603
|
+
* @param charset {string} 编码
|
|
604
|
+
*/
|
|
605
|
+
setContentType(type, charset = "utf-8") {
|
|
606
|
+
this.setHeader("Content-Type", `${ContentType[type] || type}; charset=${charset}`);
|
|
607
|
+
return this;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* set status code
|
|
611
|
+
* @param code {number} 状态码
|
|
612
|
+
*/
|
|
613
|
+
setStatusCode(code) {
|
|
614
|
+
this.response.statusCode = code;
|
|
615
|
+
return this;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* set body
|
|
619
|
+
* @param body {*} 内容
|
|
620
|
+
*/
|
|
621
|
+
setBody(body) {
|
|
622
|
+
this.response.body = body;
|
|
623
|
+
return this;
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
function useHttp(config) {
|
|
627
|
+
return usePlugin(new Http(config));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
//#endregion
|
|
631
|
+
//#region src/response-error.ts
|
|
632
|
+
const INTERNAL_SERVER_ERROR_MESSAGE = "Internal Server Error";
|
|
633
|
+
function getErrorStatusCode(error) {
|
|
634
|
+
if (!error || typeof error !== "object") return void 0;
|
|
635
|
+
const statusCode = error.statusCode;
|
|
636
|
+
if (typeof statusCode !== "number" || !Number.isFinite(statusCode)) return void 0;
|
|
637
|
+
return statusCode;
|
|
638
|
+
}
|
|
639
|
+
function getErrorMessage(error, fallback = INTERNAL_SERVER_ERROR_MESSAGE) {
|
|
640
|
+
if (error && typeof error === "object") {
|
|
641
|
+
const message = error.message;
|
|
642
|
+
if (typeof message === "string" && message.length) return message;
|
|
643
|
+
}
|
|
644
|
+
return fallback;
|
|
645
|
+
}
|
|
646
|
+
function respondWithJsonError(res, statusCode, message) {
|
|
647
|
+
if (res.writableEnded) return;
|
|
648
|
+
if (res.headersSent) {
|
|
649
|
+
res.end();
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
res.statusCode = statusCode;
|
|
653
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
654
|
+
res.end(JSON.stringify({ error: { message } }));
|
|
655
|
+
}
|
|
656
|
+
function respondWithInternalServerError(res) {
|
|
657
|
+
if (res.writableEnded) return;
|
|
658
|
+
if (res.headersSent) {
|
|
659
|
+
res.end();
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
res.statusCode = 500;
|
|
663
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
664
|
+
res.end(INTERNAL_SERVER_ERROR_MESSAGE);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
//#endregion
|
|
668
|
+
//#region src/middleware/middleware.ts
|
|
669
|
+
async function invokeMiddleware(event, logger, handler) {
|
|
670
|
+
const loggerKey = (0, node_crypto.randomUUID)();
|
|
671
|
+
const handlerLogger = new _faasjs_node_utils.Logger(`${logger.label}] [middleware] [${handler.name || "uname"}`);
|
|
672
|
+
handlerLogger.debug("begin");
|
|
673
|
+
handlerLogger.time(loggerKey, "debug");
|
|
674
|
+
try {
|
|
675
|
+
await handler(Object.assign(event.raw.request, { body: event.body }), event.raw.response, { logger: handlerLogger });
|
|
676
|
+
} catch (error) {
|
|
677
|
+
handlerLogger.error("error:", error);
|
|
678
|
+
if (getErrorStatusCode(error) === 500) respondWithJsonError(event.raw.response, 500, getErrorMessage(error));
|
|
679
|
+
else respondWithInternalServerError(event.raw.response);
|
|
680
|
+
} finally {
|
|
681
|
+
handlerLogger.timeEnd(loggerKey, "end");
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Apply a middleware function to handle incoming requests.
|
|
686
|
+
*
|
|
687
|
+
* @param handler - The middleware function to handle the request and response.
|
|
688
|
+
* @returns A function that processes the event and applies the middleware.
|
|
689
|
+
*
|
|
690
|
+
* @example
|
|
691
|
+
* ```typescript
|
|
692
|
+
* import { useMiddleware } from '@faasjs/core'
|
|
693
|
+
*
|
|
694
|
+
* export const func = useMiddleware((request, response, logger) => {
|
|
695
|
+
* response.setHeader('X-Hello', 'World')
|
|
696
|
+
* response.end('Hello, World!')
|
|
697
|
+
* logger.info('Hello, World!')
|
|
698
|
+
* })
|
|
699
|
+
* ```
|
|
700
|
+
*/
|
|
701
|
+
async function useMiddleware(handler) {
|
|
702
|
+
return useFunc(() => async ({ event, logger }) => {
|
|
703
|
+
await invokeMiddleware(event, logger, handler);
|
|
704
|
+
if (!event.raw.response.writableEnded) {
|
|
705
|
+
event.raw.response.statusCode = 404;
|
|
706
|
+
event.raw.response.end("Not Found");
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Apply an array of middleware functions to an event.
|
|
712
|
+
*
|
|
713
|
+
* @param {Middleware[]} handlers - An array of middleware functions to be applied.
|
|
714
|
+
* @returns {Promise<void>} A promise that resolves when all middleware functions have been applied.
|
|
715
|
+
*
|
|
716
|
+
* @example
|
|
717
|
+
* ```typescript
|
|
718
|
+
* import { useMiddlewares } from '@faasjs/core'
|
|
719
|
+
*
|
|
720
|
+
* export const func = useMiddlewares([
|
|
721
|
+
* (request, response) => {
|
|
722
|
+
* if (request.url === '/hi') return
|
|
723
|
+
* response.end('Hello, World!')
|
|
724
|
+
* },
|
|
725
|
+
* (request, response) => {
|
|
726
|
+
* if (request.url === '/hello') return
|
|
727
|
+
* response.end('Hi, World!')
|
|
728
|
+
* }
|
|
729
|
+
* ])
|
|
730
|
+
* ```
|
|
731
|
+
*/
|
|
732
|
+
async function useMiddlewares(handlers) {
|
|
733
|
+
return useFunc(() => async ({ event, logger }) => {
|
|
734
|
+
for (const handler of handlers) {
|
|
735
|
+
if (event.raw.response.writableEnded) break;
|
|
736
|
+
await invokeMiddleware(event, logger, handler);
|
|
737
|
+
}
|
|
738
|
+
if (!event.raw.response.writableEnded) {
|
|
739
|
+
event.raw.response.statusCode = 404;
|
|
740
|
+
event.raw.response.end("Not Found");
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
//#endregion
|
|
746
|
+
//#region src/request-url.ts
|
|
747
|
+
const BAD_REQUEST_URL_MESSAGE = "Bad Request: url is undefined";
|
|
748
|
+
function ensureRequestUrl(req, res) {
|
|
749
|
+
if (req.url) return req.url;
|
|
750
|
+
res.statusCode = 400;
|
|
751
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
752
|
+
res.end(BAD_REQUEST_URL_MESSAGE);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
//#endregion
|
|
756
|
+
//#region src/middleware/static.ts
|
|
757
|
+
const cachedStaticFiles = /* @__PURE__ */ new Map();
|
|
758
|
+
async function respondWithNotFound(options, request, response, logger) {
|
|
759
|
+
if (!options.notFound) return;
|
|
760
|
+
if (options.notFound === true) {
|
|
761
|
+
response.statusCode = 404;
|
|
762
|
+
response.end("Not Found");
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (typeof options.notFound === "string") {
|
|
766
|
+
const path = (0, node_path.resolve)(options.root, options.notFound);
|
|
767
|
+
return await respondWithFile(path, (0, mime_types.lookup)(path) || "application/octet-stream", response);
|
|
768
|
+
}
|
|
769
|
+
return await options.notFound(request, response, { logger });
|
|
770
|
+
}
|
|
771
|
+
async function respondWithFile(path, mimeType, response) {
|
|
772
|
+
const stream = (0, node_fs.createReadStream)(path);
|
|
773
|
+
response.setHeader("Content-Type", mimeType);
|
|
774
|
+
await new Promise((resolve, reject) => {
|
|
775
|
+
stream.on("error", (error) => {
|
|
776
|
+
respondWithInternalServerError(response);
|
|
777
|
+
reject(error);
|
|
778
|
+
}).on("end", resolve);
|
|
779
|
+
stream.pipe(response);
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Middleware to handle static file requests.
|
|
784
|
+
*
|
|
785
|
+
* @param {StaticHandlerOptions} options - Options for the static handler.
|
|
786
|
+
* @returns {Middleware} The middleware function.
|
|
787
|
+
*
|
|
788
|
+
* The middleware resolves the requested URL to a file path within the specified root directory.
|
|
789
|
+
* If the file exists, it reads the file content and sends it in the response.
|
|
790
|
+
* If the file does not exist, it does nothing.
|
|
791
|
+
*
|
|
792
|
+
* @example
|
|
793
|
+
* ```typescript
|
|
794
|
+
* import { useMiddleware, staticHandler } from '@faasjs/core'
|
|
795
|
+
*
|
|
796
|
+
* export const func = useMiddleware(staticHandler({ root: __dirname + '/public' }))
|
|
797
|
+
* ```
|
|
798
|
+
*/
|
|
799
|
+
function staticHandler(options) {
|
|
800
|
+
const handler = async (request, response, { logger }) => {
|
|
801
|
+
if (response.writableEnded) return;
|
|
802
|
+
const requestUrl = ensureRequestUrl(request, response);
|
|
803
|
+
if (!requestUrl) return;
|
|
804
|
+
if (request.method !== "GET" || requestUrl.slice(0, 2) === "/.") return;
|
|
805
|
+
const cacheKey = options.cache !== false ? `${options.cache || options.root}${requestUrl}` : null;
|
|
806
|
+
if (cacheKey) {
|
|
807
|
+
const cached = cachedStaticFiles.get(cacheKey);
|
|
808
|
+
if (cached === false) return await respondWithNotFound(options, request, response, logger);
|
|
809
|
+
if (cached) {
|
|
810
|
+
response.setHeader("Content-Type", cached.mimeType);
|
|
811
|
+
return await respondWithFile(cached.path, cached.mimeType, response);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
let url = options.stripPrefix ? requestUrl.replace(options.stripPrefix, "") : requestUrl;
|
|
815
|
+
if (url === "/") url = "/index.html";
|
|
816
|
+
if (url.startsWith("/")) url = url.slice(1);
|
|
817
|
+
logger.debug("finding:", requestUrl);
|
|
818
|
+
const path = (0, node_path.resolve)(options.root, url);
|
|
819
|
+
if (!(0, node_fs.existsSync)(path)) {
|
|
820
|
+
logger.debug("not found:", path);
|
|
821
|
+
if (cacheKey) cachedStaticFiles.set(cacheKey, false);
|
|
822
|
+
return await respondWithNotFound(options, request, response, logger);
|
|
823
|
+
}
|
|
824
|
+
const mimeType = (0, mime_types.lookup)(path) || "application/octet-stream";
|
|
825
|
+
logger.debug("found:", mimeType, url);
|
|
826
|
+
if (cacheKey) cachedStaticFiles.set(cacheKey, {
|
|
827
|
+
path,
|
|
828
|
+
mimeType
|
|
829
|
+
});
|
|
830
|
+
return await respondWithFile(path, mimeType, response);
|
|
831
|
+
};
|
|
832
|
+
nameFunc("static", handler);
|
|
833
|
+
return handler;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
//#endregion
|
|
837
|
+
//#region src/server/headers.ts
|
|
838
|
+
const AdditionalHeaders = [
|
|
839
|
+
"content-type",
|
|
840
|
+
"authorization",
|
|
841
|
+
"x-faasjs-request-id",
|
|
842
|
+
"x-faasjs-timing-pending",
|
|
843
|
+
"x-faasjs-timing-processing",
|
|
844
|
+
"x-faasjs-timing-total"
|
|
845
|
+
];
|
|
846
|
+
const ExposedHeadersBlacklist = [
|
|
847
|
+
"host",
|
|
848
|
+
"connection",
|
|
849
|
+
"user-agent",
|
|
850
|
+
"accept",
|
|
851
|
+
"accept-language",
|
|
852
|
+
"referer",
|
|
853
|
+
"origin",
|
|
854
|
+
"content-length",
|
|
855
|
+
"content-md5"
|
|
856
|
+
];
|
|
857
|
+
function buildCORSHeaders(headers, extra = {}) {
|
|
858
|
+
const commonHeaderNames = [
|
|
859
|
+
...AdditionalHeaders,
|
|
860
|
+
...Object.keys(headers),
|
|
861
|
+
...Object.keys(extra),
|
|
862
|
+
extra["access-control-request-headers"],
|
|
863
|
+
headers["access-control-request-headers"]
|
|
864
|
+
].filter((key) => !!key && !key.startsWith("access-control-") && !ExposedHeadersBlacklist.includes(key.toLowerCase()));
|
|
865
|
+
return {
|
|
866
|
+
"access-control-allow-origin": headers.origin || "*",
|
|
867
|
+
"access-control-allow-credentials": "true",
|
|
868
|
+
"access-control-allow-methods": "OPTIONS, POST",
|
|
869
|
+
"access-control-allow-headers": Array.from(new Set(commonHeaderNames.concat(extra["access-control-allow-headers"] || headers["access-control-allow-headers"] || []))).sort().join(", "),
|
|
870
|
+
"access-control-expose-headers": Array.from(new Set(commonHeaderNames.concat(extra["access-control-expose-headers"] || headers["access-control-expose-headers"] || []))).sort().join(", "),
|
|
871
|
+
...extra
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
//#endregion
|
|
876
|
+
//#region src/server/routes.ts
|
|
877
|
+
function getRouteFiles(root, path) {
|
|
878
|
+
const normalizedRoot = root.endsWith(node_path.sep) ? root : `${root}${node_path.sep}`;
|
|
879
|
+
const normalizedPath = path.endsWith(node_path.sep) ? path.slice(0, -1) : path;
|
|
880
|
+
const searchPaths = [
|
|
881
|
+
`${normalizedPath}.func.ts`,
|
|
882
|
+
`${normalizedPath}${node_path.sep}index.func.ts`,
|
|
883
|
+
`${normalizedPath}${node_path.sep}default.func.ts`
|
|
884
|
+
];
|
|
885
|
+
let currentPath = normalizedPath;
|
|
886
|
+
while (currentPath.length > normalizedRoot.length) {
|
|
887
|
+
const lastSepIndex = currentPath.lastIndexOf(node_path.sep);
|
|
888
|
+
if (lastSepIndex === -1) break;
|
|
889
|
+
currentPath = currentPath.substring(0, lastSepIndex);
|
|
890
|
+
if (currentPath.length < normalizedRoot.length - 1) break;
|
|
891
|
+
searchPaths.push(`${currentPath}${node_path.sep}default.func.ts`);
|
|
892
|
+
}
|
|
893
|
+
return searchPaths;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
//#endregion
|
|
897
|
+
//#region src/server/index.ts
|
|
898
|
+
const servers = [];
|
|
899
|
+
function getAll() {
|
|
900
|
+
return servers;
|
|
901
|
+
}
|
|
902
|
+
async function closeAll() {
|
|
903
|
+
for (const server of servers) await server.close();
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* FaasJS Server.
|
|
907
|
+
*
|
|
908
|
+
* @param {string} root The root path of the server.
|
|
909
|
+
* @param {ServerOptions} opts The options of the server.
|
|
910
|
+
* @returns {Server}
|
|
911
|
+
* @example
|
|
912
|
+
* ```ts
|
|
913
|
+
* import { Server } from '@faasjs/core'
|
|
914
|
+
*
|
|
915
|
+
* const server = new Server(process.cwd(), {
|
|
916
|
+
* port: 8080,
|
|
917
|
+
* })
|
|
918
|
+
*
|
|
919
|
+
* server.listen()
|
|
920
|
+
* ```
|
|
921
|
+
*/
|
|
922
|
+
var Server = class {
|
|
923
|
+
root;
|
|
924
|
+
logger;
|
|
925
|
+
options;
|
|
926
|
+
closed = false;
|
|
927
|
+
activeRequests = 0;
|
|
928
|
+
cachedFuncs = {};
|
|
929
|
+
onError;
|
|
930
|
+
server;
|
|
931
|
+
sockets = /* @__PURE__ */ new Set();
|
|
932
|
+
constructor(root, opts = {}) {
|
|
933
|
+
if (!process.env.FaasEnv && process.env.NODE_ENV) process.env.FaasEnv = process.env.NODE_ENV;
|
|
934
|
+
this.root = root.endsWith(node_path.sep) ? root : root + node_path.sep;
|
|
935
|
+
this.options = (0, _faasjs_node_utils.deepMerge)({ port: 3e3 }, opts);
|
|
936
|
+
if (this.options.onClose && !node_util.types.isAsyncFunction(this.options.onClose)) throw Error("onClose must be async function");
|
|
937
|
+
if (this.options.onError && !node_util.types.isAsyncFunction(this.options.onError)) throw Error("onError must be async function");
|
|
938
|
+
if (this.options.onStart && !node_util.types.isAsyncFunction(this.options.onStart)) throw Error("onStart must be async function");
|
|
939
|
+
if (!process.env.FaasMode) process.env.FaasMode = "mono";
|
|
940
|
+
this.logger = new _faasjs_node_utils.Logger(`server][${(0, node_crypto.randomBytes)(16).toString("hex")}`);
|
|
941
|
+
this.logger.debug("FaasJS server initialized: [%s] [%s] %s %j", process.env.FaasEnv, process.env.FaasMode, this.root, this.options);
|
|
942
|
+
this.onError = (error) => {
|
|
943
|
+
if (!(error instanceof Error)) error = Error(error);
|
|
944
|
+
this.logger.error(error);
|
|
945
|
+
if (this.options.onError) try {
|
|
946
|
+
this.options.onError(error, { logger: this.logger });
|
|
947
|
+
} catch (error) {
|
|
948
|
+
this.logger.error(error);
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
servers.push(this);
|
|
952
|
+
}
|
|
953
|
+
async handle(req, res, options = {}) {
|
|
954
|
+
if (req.method === "OPTIONS") {
|
|
955
|
+
this.handleOptionRequest(req, res);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const requestedAt = options.requestedAt || Date.now();
|
|
959
|
+
const requestId = req.headers["x-faasjs-request-id"] || req.headers["x-request-id"] || `FS-${(0, node_crypto.randomBytes)(16).toString("hex")}`;
|
|
960
|
+
const logger = new _faasjs_node_utils.Logger(requestId);
|
|
961
|
+
logger.info("%s %s", req.method, req.url);
|
|
962
|
+
const requestUrl = ensureRequestUrl(req, res);
|
|
963
|
+
if (!requestUrl) return;
|
|
964
|
+
const startedAt = Date.now();
|
|
965
|
+
const path = (0, node_path.join)(this.root, requestUrl).replace(/\?.*/, "");
|
|
966
|
+
const body = await this.readRequestBody(req);
|
|
967
|
+
if (!await this.runBeforeHandle(req, res, logger)) return;
|
|
968
|
+
let data;
|
|
969
|
+
try {
|
|
970
|
+
const cache = await this.getOrLoadHandler(path, options.filepath, logger);
|
|
971
|
+
data = await this.invokeHandler(cache, req, res, requestUrl, body, requestId);
|
|
972
|
+
} catch (error) {
|
|
973
|
+
logger.error(error);
|
|
974
|
+
data = error;
|
|
975
|
+
}
|
|
976
|
+
if (res.writableEnded) return;
|
|
977
|
+
const headers = this.buildResponseHeaders(req, requestId, requestedAt, startedAt, data);
|
|
978
|
+
for (const key in headers) res.setHeader(key, headers[key]);
|
|
979
|
+
if (data instanceof Response) {
|
|
980
|
+
res.statusCode = data.status;
|
|
981
|
+
const reader = data.body?.getReader();
|
|
982
|
+
if (reader) {
|
|
983
|
+
const stream = node_stream.Readable.from((async function* () {
|
|
984
|
+
while (true) try {
|
|
985
|
+
const { done, value } = await reader.read();
|
|
986
|
+
if (done) break;
|
|
987
|
+
if (value) yield value;
|
|
988
|
+
} catch (error) {
|
|
989
|
+
logger.error(error);
|
|
990
|
+
break;
|
|
991
|
+
}
|
|
992
|
+
})());
|
|
993
|
+
await new Promise((done) => {
|
|
994
|
+
this.pipeToResponse(stream, res, done);
|
|
995
|
+
});
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
res.end();
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (data.body instanceof ReadableStream) {
|
|
1002
|
+
if (typeof data.statusCode === "number") res.statusCode = data.statusCode;
|
|
1003
|
+
await new Promise((done) => {
|
|
1004
|
+
this.pipeToResponse(node_stream.Readable.fromWeb(data.body), res, done);
|
|
1005
|
+
});
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const statusCode = getErrorStatusCode(data);
|
|
1009
|
+
if (statusCode === 500) {
|
|
1010
|
+
respondWithJsonError(res, 500, getErrorMessage(data));
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
let resBody;
|
|
1014
|
+
if (data instanceof Error || data?.constructor?.name?.includes("Error") || typeof data === "undefined" || data === null) {
|
|
1015
|
+
if (typeof statusCode === "number") respondWithJsonError(res, statusCode, getErrorMessage(data, statusCode === 500 ? INTERNAL_SERVER_ERROR_MESSAGE : "No response"));
|
|
1016
|
+
else respondWithInternalServerError(res);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
if (typeof statusCode === "number") res.statusCode = statusCode;
|
|
1020
|
+
if (data.body) resBody = data.body;
|
|
1021
|
+
if (typeof resBody !== "undefined") {
|
|
1022
|
+
logger.debug("Response %s %j", res.statusCode, headers);
|
|
1023
|
+
res.write(resBody);
|
|
1024
|
+
}
|
|
1025
|
+
res.end();
|
|
1026
|
+
}
|
|
1027
|
+
readRequestBody(req) {
|
|
1028
|
+
return new Promise((resolve) => {
|
|
1029
|
+
let body = "";
|
|
1030
|
+
req.on("readable", () => {
|
|
1031
|
+
body += req.read() || "";
|
|
1032
|
+
});
|
|
1033
|
+
req.on("end", () => {
|
|
1034
|
+
resolve(body);
|
|
1035
|
+
});
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
async runBeforeHandle(req, res, logger) {
|
|
1039
|
+
if (!this.options.beforeHandle) return true;
|
|
1040
|
+
try {
|
|
1041
|
+
await this.options.beforeHandle(req, res, { logger });
|
|
1042
|
+
return !res.writableEnded;
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
logger.error(error);
|
|
1045
|
+
respondWithInternalServerError(res);
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
async getOrLoadHandler(path, filepath, logger) {
|
|
1050
|
+
const cached = this.cachedFuncs[path];
|
|
1051
|
+
if (cached?.handler) {
|
|
1052
|
+
logger.debug("response with cached %s", cached.file);
|
|
1053
|
+
return cached;
|
|
1054
|
+
}
|
|
1055
|
+
const file = filepath || this.getFilePath(path);
|
|
1056
|
+
const cache = { file };
|
|
1057
|
+
logger.debug("response with %s", cache.file);
|
|
1058
|
+
const func = await (0, _faasjs_node_utils.loadPackage)(file, ["func", "default"]);
|
|
1059
|
+
func.config = (0, _faasjs_node_utils.loadConfig)(this.root, path, process.env.FaasEnv || "development", logger);
|
|
1060
|
+
if (!func.config) throw Error("No config file found");
|
|
1061
|
+
cache.handler = func.export().handler;
|
|
1062
|
+
this.cachedFuncs[path] = cache;
|
|
1063
|
+
return cache;
|
|
1064
|
+
}
|
|
1065
|
+
async invokeHandler(cache, req, res, requestUrl, body, requestId) {
|
|
1066
|
+
const url = new URL(requestUrl, `http://${req.headers.host}`);
|
|
1067
|
+
if (!cache.handler) throw Error("handler is undefined");
|
|
1068
|
+
return cache.handler({
|
|
1069
|
+
headers: req.headers,
|
|
1070
|
+
httpMethod: req.method,
|
|
1071
|
+
path: url.pathname,
|
|
1072
|
+
queryString: Object.fromEntries(new URLSearchParams(url.search)),
|
|
1073
|
+
body,
|
|
1074
|
+
raw: {
|
|
1075
|
+
request: req,
|
|
1076
|
+
response: res
|
|
1077
|
+
}
|
|
1078
|
+
}, { request_id: requestId });
|
|
1079
|
+
}
|
|
1080
|
+
buildResponseHeaders(req, requestId, requestedAt, startedAt, data) {
|
|
1081
|
+
const finishedAt = Date.now();
|
|
1082
|
+
let headers = buildCORSHeaders(req.headers, {
|
|
1083
|
+
"x-faasjs-request-id": requestId,
|
|
1084
|
+
"x-faasjs-timing-pending": (startedAt - requestedAt).toString()
|
|
1085
|
+
});
|
|
1086
|
+
if (data.headers) headers = Object.assign(headers, data.headers);
|
|
1087
|
+
if (!headers["x-faasjs-timing-processing"]) headers["x-faasjs-timing-processing"] = (finishedAt - startedAt).toString();
|
|
1088
|
+
if (!headers["x-faasjs-timing-total"]) headers["x-faasjs-timing-total"] = (finishedAt - requestedAt).toString();
|
|
1089
|
+
Object.freeze(headers);
|
|
1090
|
+
return headers;
|
|
1091
|
+
}
|
|
1092
|
+
pipeToResponse(stream, res, done) {
|
|
1093
|
+
stream.pipe(res).on("finish", () => {
|
|
1094
|
+
res.end();
|
|
1095
|
+
done();
|
|
1096
|
+
}).on("error", (err) => {
|
|
1097
|
+
this.onError(err);
|
|
1098
|
+
respondWithInternalServerError(res);
|
|
1099
|
+
done();
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Start server.
|
|
1104
|
+
* @returns {Server}
|
|
1105
|
+
*/
|
|
1106
|
+
listen() {
|
|
1107
|
+
if (this.server) throw Error("Server already running");
|
|
1108
|
+
const mounted = {};
|
|
1109
|
+
if (this.options.onStart) {
|
|
1110
|
+
this.logger.debug("[onStart] begin");
|
|
1111
|
+
this.logger.time(`${this.logger.label}onStart`);
|
|
1112
|
+
this.options.onStart({ logger: this.logger }).catch(this.onError).finally(() => this.logger.timeEnd(`${this.logger.label}onStart`, "[onStart] end"));
|
|
1113
|
+
}
|
|
1114
|
+
this.server = (0, node_http.createServer)(async (req, res) => {
|
|
1115
|
+
this.activeRequests++;
|
|
1116
|
+
res.on("finish", () => this.activeRequests--);
|
|
1117
|
+
if (req.method === "OPTIONS") return this.handleOptionRequest(req, res);
|
|
1118
|
+
const requestUrl = ensureRequestUrl(req, res);
|
|
1119
|
+
if (!requestUrl) return;
|
|
1120
|
+
const path = (0, node_path.join)(this.root, requestUrl).replace(/\?.*/, "");
|
|
1121
|
+
if (!mounted[path]) mounted[path] = { pending: [] };
|
|
1122
|
+
mounted[path].pending.push([
|
|
1123
|
+
req,
|
|
1124
|
+
res,
|
|
1125
|
+
Date.now()
|
|
1126
|
+
]);
|
|
1127
|
+
const pending = mounted[path].pending;
|
|
1128
|
+
mounted[path].pending = [];
|
|
1129
|
+
for (const event of pending) await this.handle(event[0], event[1], { requestedAt: event[2] });
|
|
1130
|
+
}).on("connection", (socket) => {
|
|
1131
|
+
this.sockets.add(socket);
|
|
1132
|
+
socket.on("close", () => {
|
|
1133
|
+
this.sockets.delete(socket);
|
|
1134
|
+
});
|
|
1135
|
+
}).on("error", (e) => {
|
|
1136
|
+
if ("code" in e && e.code === "EADDRINUSE") {
|
|
1137
|
+
(0, node_child_process.execSync)(`lsof -i :${this.options.port}`, { stdio: "inherit" });
|
|
1138
|
+
this.logger.error("Port %s is already in use. Please kill the process or use another port.", this.options.port);
|
|
1139
|
+
}
|
|
1140
|
+
this.onError(e);
|
|
1141
|
+
}).listen(this.options.port, "0.0.0.0");
|
|
1142
|
+
process.on("uncaughtException", (e) => {
|
|
1143
|
+
this.logger.debug("Uncaught exception");
|
|
1144
|
+
this.onError(e);
|
|
1145
|
+
}).on("unhandledRejection", (e) => {
|
|
1146
|
+
this.logger.debug("Unhandled rejection");
|
|
1147
|
+
this.onError(e);
|
|
1148
|
+
}).on("SIGTERM", async () => {
|
|
1149
|
+
this.logger.debug("received SIGTERM");
|
|
1150
|
+
if (this.closed) {
|
|
1151
|
+
this.logger.debug("already closed");
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
await this.close();
|
|
1155
|
+
if (!process.env.JEST_WORKER_ID && !process.env.VITEST_POOL_ID) process.exit(0);
|
|
1156
|
+
}).on("SIGINT", async () => {
|
|
1157
|
+
this.logger.debug("received SIGINT");
|
|
1158
|
+
if (this.closed) {
|
|
1159
|
+
this.logger.debug("already closed");
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
await this.close();
|
|
1163
|
+
if (!process.env.JEST_WORKER_ID && !process.env.VITEST_POOL_ID) process.exit(0);
|
|
1164
|
+
});
|
|
1165
|
+
this.logger.info("[%s] Listen http://localhost:%s with", process.env.FaasEnv, this.options.port, this.root);
|
|
1166
|
+
return this.server;
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Close server.
|
|
1170
|
+
*/
|
|
1171
|
+
async close() {
|
|
1172
|
+
if (this.closed) {
|
|
1173
|
+
this.logger.debug("already closed");
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
this.logger.debug("closing");
|
|
1177
|
+
this.logger.time(`${this.logger.label}close`);
|
|
1178
|
+
if (this.activeRequests) await new Promise((resolve) => {
|
|
1179
|
+
const check = () => {
|
|
1180
|
+
if (this.activeRequests === 0) {
|
|
1181
|
+
resolve();
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
this.logger.debug("waiting for %i requests", this.activeRequests);
|
|
1185
|
+
setTimeout(check, 50);
|
|
1186
|
+
};
|
|
1187
|
+
check();
|
|
1188
|
+
});
|
|
1189
|
+
for (const socket of this.sockets) try {
|
|
1190
|
+
socket.destroy();
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
this.onError(error);
|
|
1193
|
+
} finally {
|
|
1194
|
+
this.sockets.delete(socket);
|
|
1195
|
+
}
|
|
1196
|
+
const server = this.server;
|
|
1197
|
+
if (server) await new Promise((resolve) => {
|
|
1198
|
+
server.close((err) => {
|
|
1199
|
+
if (err) this.onError(err);
|
|
1200
|
+
resolve();
|
|
1201
|
+
});
|
|
1202
|
+
});
|
|
1203
|
+
if (this.options.onClose) {
|
|
1204
|
+
this.logger.debug("[onClose] begin");
|
|
1205
|
+
this.logger.time(`${this.logger.label}onClose`);
|
|
1206
|
+
try {
|
|
1207
|
+
await this.options.onClose({ logger: this.logger });
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
this.onError(error);
|
|
1210
|
+
}
|
|
1211
|
+
this.logger.timeEnd(`${this.logger.label}onClose`, "[onClose] end");
|
|
1212
|
+
}
|
|
1213
|
+
this.logger.timeEnd(`${this.logger.label}close`, "closed");
|
|
1214
|
+
await (0, _faasjs_node_utils.getTransport)().stop();
|
|
1215
|
+
this.closed = true;
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Middleware function to handle incoming HTTP requests.
|
|
1219
|
+
*
|
|
1220
|
+
* @param req - The incoming HTTP request object.
|
|
1221
|
+
* @param res - The server response object.
|
|
1222
|
+
* @param next - A callback function to pass control to the next middleware.
|
|
1223
|
+
* @returns A promise that resolves when the middleware processing is complete.
|
|
1224
|
+
*/
|
|
1225
|
+
async middleware(req, res, next) {
|
|
1226
|
+
const requestUrl = ensureRequestUrl(req, res);
|
|
1227
|
+
if (!requestUrl) {
|
|
1228
|
+
next();
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
try {
|
|
1232
|
+
const filepath = this.getFilePath((0, node_path.join)(this.root, requestUrl).replace(/\?.*/, ""));
|
|
1233
|
+
await this.handle(req, res, {
|
|
1234
|
+
requestedAt: Date.now(),
|
|
1235
|
+
filepath
|
|
1236
|
+
});
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
this.logger.debug("middleware error", error);
|
|
1239
|
+
}
|
|
1240
|
+
next();
|
|
1241
|
+
}
|
|
1242
|
+
getFilePath(path) {
|
|
1243
|
+
if (/^(\.|\|\/)+$/.test(path)) throw Error("Illegal characters");
|
|
1244
|
+
const searchPaths = getRouteFiles(this.root, path);
|
|
1245
|
+
for (const path of searchPaths) if ((0, node_fs.existsSync)(path)) return (0, node_path.resolve)(".", path);
|
|
1246
|
+
throw new HttpError({
|
|
1247
|
+
statusCode: 404,
|
|
1248
|
+
message: process.env.FaasEnv === "production" ? "Not found." : `Not found function file.\nSearch paths:\n${searchPaths.map((p) => `- ${p}`).join("\n")}`
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
handleOptionRequest(req, res) {
|
|
1252
|
+
res.writeHead(204, buildCORSHeaders(req.headers));
|
|
1253
|
+
res.end();
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
//#endregion
|
|
1258
|
+
//#region src/knex/pglite.ts
|
|
1259
|
+
function parsePgliteConnection(connection) {
|
|
1260
|
+
if (typeof connection === "undefined" || connection === null) return `memory://${(0, node_crypto.randomUUID)()}`;
|
|
1261
|
+
if (typeof connection !== "string" || !connection.trim().length) throw Error("[Knex] Invalid \"pglite\" connection, expected non-empty string in config.connection or SECRET_<NAME>_CONNECTION.");
|
|
1262
|
+
return connection;
|
|
1263
|
+
}
|
|
1264
|
+
function ensurePgliteConnectionPath(connection) {
|
|
1265
|
+
if (connection.includes("://")) return;
|
|
1266
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)((0, node_path.resolve)(connection)), { recursive: true });
|
|
1267
|
+
}
|
|
1268
|
+
async function loadPglitePackages() {
|
|
1269
|
+
try {
|
|
1270
|
+
const pgliteModule = await (0, _faasjs_node_utils.loadPackage)("@electric-sql/pglite", []);
|
|
1271
|
+
const PgliteDialect = await (0, _faasjs_node_utils.loadPackage)("knex-pglite");
|
|
1272
|
+
if (typeof pgliteModule.PGlite !== "function" || !pgliteModule.types) throw Error("Invalid @electric-sql/pglite exports");
|
|
1273
|
+
if (typeof PgliteDialect !== "function") throw Error("Invalid knex-pglite exports");
|
|
1274
|
+
return {
|
|
1275
|
+
PGlite: pgliteModule.PGlite,
|
|
1276
|
+
types: pgliteModule.types,
|
|
1277
|
+
PgliteDialect
|
|
1278
|
+
};
|
|
1279
|
+
} catch {
|
|
1280
|
+
throw Error("[Knex] client \"pglite\" requires dependencies \"@electric-sql/pglite\" and \"knex-pglite\". Please install both packages in your project.");
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Create a knex instance backed by `knex-pglite`.
|
|
1285
|
+
* If connection is missing, it defaults to an in-memory database.
|
|
1286
|
+
*/
|
|
1287
|
+
async function createPgliteKnex(config = {}, connection) {
|
|
1288
|
+
const resolvedConnection = parsePgliteConnection(connection ?? config.connection);
|
|
1289
|
+
ensurePgliteConnectionPath(resolvedConnection);
|
|
1290
|
+
const { PGlite, types, PgliteDialect } = await loadPglitePackages();
|
|
1291
|
+
const { client: _client, connection: _connection, pool: _pool, ...restConfig } = config;
|
|
1292
|
+
const pglite = new PGlite(resolvedConnection, { parsers: {
|
|
1293
|
+
[types.INT2]: (v) => Number.parseInt(v, 10),
|
|
1294
|
+
[types.INT4]: (v) => Number.parseInt(v, 10),
|
|
1295
|
+
[types.INT8]: (v) => Number.parseInt(v, 10),
|
|
1296
|
+
[types.FLOAT4]: (v) => Number.parseFloat(v),
|
|
1297
|
+
[types.FLOAT8]: (v) => Number.parseFloat(v),
|
|
1298
|
+
[types.NUMERIC]: (v) => Number.parseFloat(v)
|
|
1299
|
+
} });
|
|
1300
|
+
return (0, knex.default)({
|
|
1301
|
+
...restConfig,
|
|
1302
|
+
client: PgliteDialect,
|
|
1303
|
+
connection: { pglite }
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Mount a knex adapter to `globalThis.FaasJS_Knex` for `@faasjs/core`.
|
|
1308
|
+
*/
|
|
1309
|
+
function mountFaasKnex(db, options = {}) {
|
|
1310
|
+
const globalWithFaasKnex = globalThis;
|
|
1311
|
+
const name = options.name || "knex";
|
|
1312
|
+
if (!globalWithFaasKnex.FaasJS_Knex) globalWithFaasKnex.FaasJS_Knex = {};
|
|
1313
|
+
globalWithFaasKnex.FaasJS_Knex[name] = {
|
|
1314
|
+
adapter: db,
|
|
1315
|
+
query: db,
|
|
1316
|
+
config: options.config || {}
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Remove mounted knex adapter from `globalThis.FaasJS_Knex`.
|
|
1321
|
+
*/
|
|
1322
|
+
function unmountFaasKnex(name = "knex") {
|
|
1323
|
+
const globalWithFaasKnex = globalThis;
|
|
1324
|
+
if (!globalWithFaasKnex.FaasJS_Knex) return;
|
|
1325
|
+
delete globalWithFaasKnex.FaasJS_Knex[name];
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
//#endregion
|
|
1329
|
+
//#region src/knex/plugin.ts
|
|
1330
|
+
/**
|
|
1331
|
+
* Origin [knex](https://knexjs.org/) instance.
|
|
1332
|
+
*/
|
|
1333
|
+
const originKnex = knex.default;
|
|
1334
|
+
const Name = "knex";
|
|
1335
|
+
if (!global.FaasJS_Knex) global.FaasJS_Knex = {};
|
|
1336
|
+
function getGlobalKnexAdapters() {
|
|
1337
|
+
if (!global.FaasJS_Knex) global.FaasJS_Knex = {};
|
|
1338
|
+
return global.FaasJS_Knex;
|
|
1339
|
+
}
|
|
1340
|
+
async function initPostgresTypeParsers() {
|
|
1341
|
+
const pg = await (0, _faasjs_node_utils.loadPackage)("pg");
|
|
1342
|
+
pg.types.setTypeParser(pg.types.builtins.INT2, (v) => Number.parseInt(v, 10));
|
|
1343
|
+
pg.types.setTypeParser(pg.types.builtins.INT4, (v) => Number.parseInt(v, 10));
|
|
1344
|
+
pg.types.setTypeParser(pg.types.builtins.INT8, (v) => Number.parseInt(v, 10));
|
|
1345
|
+
pg.types.setTypeParser(pg.types.builtins.FLOAT4, (v) => Number.parseFloat(v));
|
|
1346
|
+
pg.types.setTypeParser(pg.types.builtins.FLOAT8, (v) => Number.parseFloat(v));
|
|
1347
|
+
pg.types.setTypeParser(pg.types.builtins.NUMERIC, (v) => Number.parseFloat(v));
|
|
1348
|
+
}
|
|
1349
|
+
var Knex = class {
|
|
1350
|
+
type = "knex";
|
|
1351
|
+
name = Name;
|
|
1352
|
+
config;
|
|
1353
|
+
adapter;
|
|
1354
|
+
query;
|
|
1355
|
+
logger;
|
|
1356
|
+
constructor(config) {
|
|
1357
|
+
if (config) {
|
|
1358
|
+
this.name = config.name || this.name;
|
|
1359
|
+
this.config = config.config || Object.create(null);
|
|
1360
|
+
} else this.config = Object.create(null);
|
|
1361
|
+
}
|
|
1362
|
+
async onMount(data, next) {
|
|
1363
|
+
this.logger = data.logger;
|
|
1364
|
+
const existsAdapter = getGlobalKnexAdapters()[this.name];
|
|
1365
|
+
if (existsAdapter) {
|
|
1366
|
+
this.config = existsAdapter.config;
|
|
1367
|
+
this.adapter = existsAdapter.adapter;
|
|
1368
|
+
this.query = this.adapter;
|
|
1369
|
+
this.logger.debug("use exists adapter");
|
|
1370
|
+
await next();
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
const prefix = `SECRET_${this.name.toUpperCase()}_`;
|
|
1374
|
+
for (let key in process.env) if (key.startsWith(prefix)) {
|
|
1375
|
+
const value = process.env[key];
|
|
1376
|
+
key = key.replace(prefix, "").toLowerCase();
|
|
1377
|
+
if (typeof this.config[key] === "undefined") if (key.startsWith("connection_")) {
|
|
1378
|
+
if (typeof this.config.connection === "string") continue;
|
|
1379
|
+
if (!this.config.connection || typeof this.config.connection !== "object") this.config.connection = Object.create(null);
|
|
1380
|
+
this.config.connection[key.replace("connection_", "")] = value;
|
|
1381
|
+
} else this.config[key] = value;
|
|
1382
|
+
}
|
|
1383
|
+
if (data.config.plugins?.[this.name || this.type]?.config) this.config = (0, _faasjs_node_utils.deepMerge)(data.config.plugins[this.name || this.type].config, this.config);
|
|
1384
|
+
if (this.config.client === "pglite") {
|
|
1385
|
+
const connectionEnvKeys = Object.keys(process.env).filter((key) => key.startsWith(`${prefix}CONNECTION_`));
|
|
1386
|
+
if (connectionEnvKeys.length) throw Error(`[Knex] Invalid "pglite" env keys: ${connectionEnvKeys.join(", ")}. Use ${prefix}CONNECTION instead.`);
|
|
1387
|
+
this.adapter = await createPgliteKnex(this.config);
|
|
1388
|
+
} else {
|
|
1389
|
+
switch (this.config.client) {
|
|
1390
|
+
case "sqlite3":
|
|
1391
|
+
this.config.client = "better-sqlite3";
|
|
1392
|
+
this.config.useNullAsDefault = true;
|
|
1393
|
+
break;
|
|
1394
|
+
case "pg":
|
|
1395
|
+
if (!this.config.pool) this.config.pool = Object.create(null);
|
|
1396
|
+
this.config.pool = Object.assign({
|
|
1397
|
+
propagateCreateError: false,
|
|
1398
|
+
min: 0,
|
|
1399
|
+
max: 10,
|
|
1400
|
+
acquireTimeoutMillis: 5e3,
|
|
1401
|
+
idleTimeoutMillis: 3e4
|
|
1402
|
+
}, this.config.pool);
|
|
1403
|
+
if (typeof this.config.connection === "string" && !this.config.connection.includes("json=true")) this.config.connection = `${this.config.connection}?json=true`;
|
|
1404
|
+
break;
|
|
1405
|
+
default:
|
|
1406
|
+
if (typeof this.config.client === "string") {
|
|
1407
|
+
if (this.config.client.startsWith("npm:")) {
|
|
1408
|
+
const client = await (0, _faasjs_node_utils.loadPackage)(this.config.client.replace("npm:", ""));
|
|
1409
|
+
if (!client) throw Error(`Invalid client: ${this.config.client}`);
|
|
1410
|
+
if (typeof client === "function") {
|
|
1411
|
+
this.config.client = client;
|
|
1412
|
+
break;
|
|
1413
|
+
}
|
|
1414
|
+
if (client.default && typeof client.default === "function") {
|
|
1415
|
+
this.config.client = client.default;
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1418
|
+
throw Error(`Invalid client: ${this.config.client}`);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1423
|
+
this.adapter = (0, knex.default)(this.config);
|
|
1424
|
+
if (this.config.client === "pg") await initPostgresTypeParsers();
|
|
1425
|
+
}
|
|
1426
|
+
this.query = this.adapter;
|
|
1427
|
+
this.query.on("query", ({ sql, __knexQueryUid, bindings }) => {
|
|
1428
|
+
if (!__knexQueryUid) return;
|
|
1429
|
+
this.logger.time(`Knex${this.name}${__knexQueryUid}`);
|
|
1430
|
+
this.logger.debug("[%s] query begin: %s %j", __knexQueryUid, sql, bindings);
|
|
1431
|
+
}).on("query-response", (response, { sql, __knexQueryUid, bindings }) => {
|
|
1432
|
+
if (!__knexQueryUid) return;
|
|
1433
|
+
this.logger.timeEnd(`Knex${this.name}${__knexQueryUid}`, "[%s] query done: %s %j %j", __knexQueryUid, sql, bindings, response);
|
|
1434
|
+
}).on("query-error", (_, { __knexQueryUid, sql, bindings }) => {
|
|
1435
|
+
if (!__knexQueryUid) return;
|
|
1436
|
+
this.logger.timeEnd(`Knex${this.name}${__knexQueryUid}`, "[%s] query failed: %s %j", __knexQueryUid, sql, bindings);
|
|
1437
|
+
});
|
|
1438
|
+
data.logger.debug("connected");
|
|
1439
|
+
getGlobalKnexAdapters()[this.name] = this;
|
|
1440
|
+
await next();
|
|
1441
|
+
}
|
|
1442
|
+
async onInvoke(data, next) {
|
|
1443
|
+
this.logger = data.logger;
|
|
1444
|
+
await next();
|
|
1445
|
+
}
|
|
1446
|
+
async raw(sql, bindings = []) {
|
|
1447
|
+
if (!this.adapter) throw Error("[Knex] Client not initialized.");
|
|
1448
|
+
return this.adapter.raw(sql, bindings);
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Wraps a transaction, returning a promise that resolves to the return value of the callback.
|
|
1452
|
+
*
|
|
1453
|
+
* - Support 'commit' and 'rollback' event.
|
|
1454
|
+
*/
|
|
1455
|
+
async transaction(scope, config, options) {
|
|
1456
|
+
if (!this.adapter) throw Error(`[${this.name}] Client not initialized.`);
|
|
1457
|
+
if (options?.trx) return scope(options.trx);
|
|
1458
|
+
const trx = await this.adapter.transaction(config);
|
|
1459
|
+
const trxId = (0, node_crypto.randomUUID)();
|
|
1460
|
+
this.logger.debug("[%s] transaction begin", trxId);
|
|
1461
|
+
try {
|
|
1462
|
+
const result = await scope(trx);
|
|
1463
|
+
if (trx.isCompleted()) {
|
|
1464
|
+
this.logger.debug("[%s] transaction has been finished in scope", trxId);
|
|
1465
|
+
return result;
|
|
1466
|
+
}
|
|
1467
|
+
this.logger.debug("[%s] transaction begin commit", trxId);
|
|
1468
|
+
await trx.commit();
|
|
1469
|
+
this.logger.debug("[%s] transaction committed: %j", trxId, result);
|
|
1470
|
+
trx.emit("commit");
|
|
1471
|
+
return result;
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
await trx.rollback(error);
|
|
1474
|
+
this.logger.error("[%s] transaction rollback: %s", trxId, error);
|
|
1475
|
+
trx.emit("rollback", error);
|
|
1476
|
+
throw error;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
schema() {
|
|
1480
|
+
if (!this.adapter) throw Error(`[${this.name}] Client not initialized.`);
|
|
1481
|
+
return this.adapter.schema;
|
|
1482
|
+
}
|
|
1483
|
+
async quit() {
|
|
1484
|
+
const adapters = getGlobalKnexAdapters();
|
|
1485
|
+
if (!adapters[this.name]) return;
|
|
1486
|
+
try {
|
|
1487
|
+
await adapters[this.name].adapter.destroy();
|
|
1488
|
+
delete adapters[this.name];
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
console.error(error);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
function useKnex(config) {
|
|
1495
|
+
const name = config?.name || Name;
|
|
1496
|
+
const adapters = getGlobalKnexAdapters();
|
|
1497
|
+
if (adapters[name]) return usePlugin(adapters[name]);
|
|
1498
|
+
return usePlugin(new Knex(config));
|
|
1499
|
+
}
|
|
1500
|
+
function query(table) {
|
|
1501
|
+
return useKnex().query(table);
|
|
1502
|
+
}
|
|
1503
|
+
async function transaction(scope, config, options) {
|
|
1504
|
+
return useKnex().transaction(scope, config, options);
|
|
1505
|
+
}
|
|
1506
|
+
async function raw(sql, bindings = []) {
|
|
1507
|
+
return useKnex().raw(sql, bindings);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
//#endregion
|
|
1511
|
+
//#region src/knex/schema.ts
|
|
1512
|
+
const DefaultMigratorConfig = {
|
|
1513
|
+
directory: "./src/db/migrations",
|
|
1514
|
+
extension: "ts"
|
|
1515
|
+
};
|
|
1516
|
+
/**
|
|
1517
|
+
* Migration helper for FaasJS knex plugin.
|
|
1518
|
+
*/
|
|
1519
|
+
var KnexSchema = class {
|
|
1520
|
+
knex;
|
|
1521
|
+
constructor(knex) {
|
|
1522
|
+
this.knex = knex;
|
|
1523
|
+
}
|
|
1524
|
+
getAdapter() {
|
|
1525
|
+
if (!this.knex.adapter) throw Error(`[${this.knex.name}] Client not initialized.`);
|
|
1526
|
+
return this.knex.adapter;
|
|
1527
|
+
}
|
|
1528
|
+
getMigratorConfig() {
|
|
1529
|
+
return Object.assign({}, DefaultMigratorConfig, this.knex.config?.migrations || Object.create(null));
|
|
1530
|
+
}
|
|
1531
|
+
async migrateLatest() {
|
|
1532
|
+
return this.getAdapter().migrate.latest(this.getMigratorConfig());
|
|
1533
|
+
}
|
|
1534
|
+
async migrateRollback() {
|
|
1535
|
+
return this.getAdapter().migrate.rollback(this.getMigratorConfig());
|
|
1536
|
+
}
|
|
1537
|
+
async migrateStatus() {
|
|
1538
|
+
return this.getAdapter().migrate.status(this.getMigratorConfig());
|
|
1539
|
+
}
|
|
1540
|
+
async migrateCurrentVersion() {
|
|
1541
|
+
return this.getAdapter().migrate.currentVersion(this.getMigratorConfig());
|
|
1542
|
+
}
|
|
1543
|
+
async migrateMake(name) {
|
|
1544
|
+
const migrationName = name?.trim();
|
|
1545
|
+
if (!migrationName) throw Error("[KnexSchema] Missing migration name. Usage: npm run migrate:make -- create_users");
|
|
1546
|
+
return this.getAdapter().migrate.make(migrationName, this.getMigratorConfig());
|
|
1547
|
+
}
|
|
1548
|
+
};
|
|
1549
|
+
|
|
1550
|
+
//#endregion
|
|
34
1551
|
//#region src/index.ts
|
|
35
1552
|
function formatPluginModuleName(type) {
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
return
|
|
1553
|
+
const normalizedType = type.startsWith("npm:") ? type.slice(4) : type;
|
|
1554
|
+
if (normalizedType === "http" || normalizedType === "@faasjs/http" || normalizedType === "knex" || normalizedType === "@faasjs/knex") return "@faasjs/core";
|
|
1555
|
+
if (normalizedType.startsWith("@") || normalizedType.startsWith(".") || normalizedType.startsWith("/") || normalizedType.includes(":")) return normalizedType;
|
|
1556
|
+
return `@faasjs/${normalizedType}`;
|
|
39
1557
|
}
|
|
40
1558
|
function formatPluginClassName(type) {
|
|
41
1559
|
return type.replace(/^@[^/]+\//, "").split(/[^A-Za-z0-9]+/).filter(Boolean).map((item) => item.slice(0, 1).toUpperCase() + item.slice(1)).join("");
|
|
@@ -60,7 +1578,15 @@ function formatZodErrorMessage(error) {
|
|
|
60
1578
|
function findPluginByType(func, type) {
|
|
61
1579
|
return func.plugins.find((plugin) => plugin.type === type);
|
|
62
1580
|
}
|
|
63
|
-
|
|
1581
|
+
function resolvePluginConfig(key, rawConfig) {
|
|
1582
|
+
const configValue = rawConfig && typeof rawConfig === "object" ? Object.assign(Object.create(null), rawConfig) : Object.create(null);
|
|
1583
|
+
return {
|
|
1584
|
+
configValue,
|
|
1585
|
+
pluginName: typeof configValue.name === "string" && configValue.name.length ? configValue.name : key,
|
|
1586
|
+
pluginType: typeof configValue.type === "string" && configValue.type || typeof rawConfig === "string" && rawConfig || key
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
var CoreFunc = class extends Func {
|
|
64
1590
|
loadedConfigPlugins = false;
|
|
65
1591
|
insertPluginBeforeRunHandler(plugin) {
|
|
66
1592
|
const index = this.plugins.findIndex((item) => item.type === "handler" && item.name === "handler");
|
|
@@ -72,19 +1598,19 @@ var CoreFunc = class extends _faasjs_func.Func {
|
|
|
72
1598
|
try {
|
|
73
1599
|
mod = await import(moduleName);
|
|
74
1600
|
} catch (error) {
|
|
75
|
-
throw Error(`[
|
|
1601
|
+
throw Error(`[defineApi] Failed to load plugin "${pluginName}" from "${moduleName}": ${error.message}`);
|
|
76
1602
|
}
|
|
77
1603
|
if (className && isPluginConstructor(mod[className])) return mod[className];
|
|
78
1604
|
if (isPluginConstructor(mod.default)) return mod.default;
|
|
79
|
-
throw Error(`[
|
|
1605
|
+
throw Error(`[defineApi] Failed to resolve plugin class "${className}" from "${moduleName}" for plugin "${pluginName}". Supported exports are named class "${className}" or default class export.`);
|
|
80
1606
|
}
|
|
81
1607
|
async loadPluginsFromConfig(config) {
|
|
82
1608
|
const pluginConfigs = config.plugins || Object.create(null);
|
|
83
|
-
for (const
|
|
84
|
-
|
|
85
|
-
const
|
|
1609
|
+
for (const key in pluginConfigs) {
|
|
1610
|
+
if (!Object.hasOwn(pluginConfigs, key)) continue;
|
|
1611
|
+
const rawConfig = pluginConfigs[key];
|
|
1612
|
+
const { configValue, pluginName, pluginType } = resolvePluginConfig(key, rawConfig);
|
|
86
1613
|
if (this.plugins.find((plugin) => plugin.name === pluginName)) continue;
|
|
87
|
-
const pluginType = typeof configValue.type === "string" && configValue.type || typeof rawConfig === "string" && rawConfig || key;
|
|
88
1614
|
const moduleName = formatPluginModuleName(pluginType);
|
|
89
1615
|
const className = formatPluginClassName(pluginType);
|
|
90
1616
|
const PluginConstructor = await this.resolvePluginConstructor(moduleName, className, pluginName);
|
|
@@ -96,9 +1622,9 @@ var CoreFunc = class extends _faasjs_func.Func {
|
|
|
96
1622
|
type: pluginType
|
|
97
1623
|
});
|
|
98
1624
|
} catch (error) {
|
|
99
|
-
throw Error(`[
|
|
1625
|
+
throw Error(`[defineApi] Failed to initialize plugin "${pluginName}" from "${moduleName}": ${error.message}`);
|
|
100
1626
|
}
|
|
101
|
-
if (!plugin || typeof plugin !== "object") throw Error(`[
|
|
1627
|
+
if (!plugin || typeof plugin !== "object") throw Error(`[defineApi] Invalid plugin instance for "${pluginName}" from "${moduleName}".`);
|
|
102
1628
|
this.insertPluginBeforeRunHandler(plugin);
|
|
103
1629
|
}
|
|
104
1630
|
this.loadedConfigPlugins = true;
|
|
@@ -113,35 +1639,35 @@ var CoreFunc = class extends _faasjs_func.Func {
|
|
|
113
1639
|
}
|
|
114
1640
|
};
|
|
115
1641
|
/**
|
|
116
|
-
* Create
|
|
1642
|
+
* Create an HTTP API function with optional Zod validation.
|
|
117
1643
|
*
|
|
118
1644
|
* Plugins are always auto-loaded from `func.config.plugins`.
|
|
119
1645
|
* Plugin module exports must be either a named class (normalized from
|
|
120
1646
|
* plugin type) or a default class export.
|
|
1647
|
+
*
|
|
1648
|
+
* The `http` plugin is required.
|
|
121
1649
|
*/
|
|
122
|
-
function
|
|
1650
|
+
function defineApi(options) {
|
|
123
1651
|
let func;
|
|
124
1652
|
let pluginRefsResolved = false;
|
|
125
1653
|
let hasHttp = false;
|
|
126
1654
|
let knexQuery;
|
|
127
|
-
const resolvePluginRefs = () => {
|
|
128
|
-
if (pluginRefsResolved) return;
|
|
129
|
-
hasHttp = !!findPluginByType(func, "http");
|
|
130
|
-
knexQuery = findPluginByType(func, "knex")?.query;
|
|
131
|
-
pluginRefsResolved = true;
|
|
132
|
-
};
|
|
133
1655
|
const parseParams = async (event) => {
|
|
134
|
-
if (!
|
|
1656
|
+
if (!pluginRefsResolved) {
|
|
1657
|
+
hasHttp = !!findPluginByType(func, "http");
|
|
1658
|
+
knexQuery = findPluginByType(func, "knex")?.query;
|
|
1659
|
+
pluginRefsResolved = true;
|
|
1660
|
+
}
|
|
1661
|
+
if (!hasHttp) throw Error("[defineApi] Missing required \"http\" plugin. Please configure it in func.config.plugins.");
|
|
135
1662
|
if (!options.schema) return {};
|
|
136
1663
|
const result = await options.schema.safeParseAsync(event?.params ?? {});
|
|
137
|
-
if (!result.success) throw new
|
|
1664
|
+
if (!result.success) throw new HttpError({
|
|
138
1665
|
statusCode: 400,
|
|
139
1666
|
message: formatZodErrorMessage(result.error)
|
|
140
1667
|
});
|
|
141
1668
|
return result.data;
|
|
142
1669
|
};
|
|
143
1670
|
const invokeHandler = async (data) => {
|
|
144
|
-
resolvePluginRefs();
|
|
145
1671
|
const params = await parseParams(data.event);
|
|
146
1672
|
const invokeData = {
|
|
147
1673
|
...data,
|
|
@@ -158,7 +1684,35 @@ function defineFunc(options) {
|
|
|
158
1684
|
}
|
|
159
1685
|
|
|
160
1686
|
//#endregion
|
|
161
|
-
exports.
|
|
1687
|
+
exports.ContentType = ContentType;
|
|
1688
|
+
exports.Cookie = Cookie;
|
|
1689
|
+
exports.Func = Func;
|
|
1690
|
+
exports.Http = Http;
|
|
1691
|
+
exports.HttpError = HttpError;
|
|
1692
|
+
exports.Knex = Knex;
|
|
1693
|
+
exports.KnexSchema = KnexSchema;
|
|
1694
|
+
exports.Server = Server;
|
|
1695
|
+
exports.Session = Session;
|
|
1696
|
+
exports.closeAll = closeAll;
|
|
1697
|
+
exports.createPgliteKnex = createPgliteKnex;
|
|
1698
|
+
exports.defineApi = defineApi;
|
|
1699
|
+
exports.getAll = getAll;
|
|
1700
|
+
exports.initPostgresTypeParsers = initPostgresTypeParsers;
|
|
1701
|
+
exports.mountFaasKnex = mountFaasKnex;
|
|
1702
|
+
exports.nameFunc = nameFunc;
|
|
1703
|
+
exports.originKnex = originKnex;
|
|
1704
|
+
exports.parseFuncFilenameFromStack = parseFuncFilenameFromStack;
|
|
1705
|
+
exports.query = query;
|
|
1706
|
+
exports.raw = raw;
|
|
1707
|
+
exports.staticHandler = staticHandler;
|
|
1708
|
+
exports.transaction = transaction;
|
|
1709
|
+
exports.unmountFaasKnex = unmountFaasKnex;
|
|
1710
|
+
exports.useFunc = useFunc;
|
|
1711
|
+
exports.useHttp = useHttp;
|
|
1712
|
+
exports.useKnex = useKnex;
|
|
1713
|
+
exports.useMiddleware = useMiddleware;
|
|
1714
|
+
exports.useMiddlewares = useMiddlewares;
|
|
1715
|
+
exports.usePlugin = usePlugin;
|
|
162
1716
|
Object.defineProperty(exports, 'z', {
|
|
163
1717
|
enumerable: true,
|
|
164
1718
|
get: function () {
|