@faasjs/core 8.0.0-beta.10

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 ADDED
@@ -0,0 +1,1951 @@
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") {
11
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) {
14
+ __defProp(to, key, {
15
+ get: ((k) => from[k]).bind(null, key),
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ });
18
+ }
19
+ }
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+
28
+ //#endregion
29
+ let zod = require("zod");
30
+ zod = __toESM(zod);
31
+ let node_crypto = require("node:crypto");
32
+ let node_url = require("node:url");
33
+ let _faasjs_node_utils = require("@faasjs/node-utils");
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);
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/cron/index.ts
838
+ const CronFields = [
839
+ {
840
+ name: "minute",
841
+ min: 0,
842
+ max: 59
843
+ },
844
+ {
845
+ name: "hour",
846
+ min: 0,
847
+ max: 23
848
+ },
849
+ {
850
+ name: "dayOfMonth",
851
+ min: 1,
852
+ max: 31
853
+ },
854
+ {
855
+ name: "month",
856
+ min: 1,
857
+ max: 12
858
+ },
859
+ {
860
+ name: "dayOfWeek",
861
+ min: 0,
862
+ max: 6
863
+ }
864
+ ];
865
+ function toError(error) {
866
+ if (error instanceof Error) return error;
867
+ return Error(String(error));
868
+ }
869
+ function parsePositiveInt(value) {
870
+ if (!/^\d+$/.test(value)) return NaN;
871
+ return Number.parseInt(value, 10);
872
+ }
873
+ function invalidExpressionError(expression) {
874
+ return Error(`[CronJob] Invalid expression "${expression}". Expected 5 fields: minute hour dayOfMonth month dayOfWeek.`);
875
+ }
876
+ function createCronMatcher(segment, field) {
877
+ if (segment === "*") return () => true;
878
+ if (segment.startsWith("*/")) {
879
+ const step = parsePositiveInt(segment.slice(2));
880
+ if (!Number.isInteger(step) || step <= 0) throw Error(`[CronJob] Invalid ${field.name} segment "${segment}".`);
881
+ return (value) => (value - field.min) % step === 0;
882
+ }
883
+ const fixed = parsePositiveInt(segment);
884
+ if (!Number.isInteger(fixed)) throw Error(`[CronJob] Invalid ${field.name} segment "${segment}".`);
885
+ if (fixed < field.min || fixed > field.max) throw Error(`[CronJob] ${field.name} value out of range: ${fixed}. Allowed range is ${field.min}-${field.max}.`);
886
+ return (value) => value === fixed;
887
+ }
888
+ function parseExpression(expression) {
889
+ const trimmed = expression.trim();
890
+ if (!trimmed) throw Error("[CronJob] expression is required.");
891
+ const segments = trimmed.split(/\s+/);
892
+ if (segments.length !== 5) throw invalidExpressionError(expression);
893
+ const matchers = [];
894
+ for (const [index, field] of CronFields.entries()) {
895
+ const segment = segments[index];
896
+ if (typeof segment !== "string") throw invalidExpressionError(expression);
897
+ matchers.push(createCronMatcher(segment, field));
898
+ }
899
+ return matchers;
900
+ }
901
+ function shouldRun(matchers, now) {
902
+ const values = [
903
+ now.getMinutes(),
904
+ now.getHours(),
905
+ now.getDate(),
906
+ now.getMonth() + 1,
907
+ now.getDay()
908
+ ];
909
+ return matchers.every((matcher, index) => matcher(values[index]));
910
+ }
911
+ function getDelayToNextMinute(nowMs = Date.now()) {
912
+ const now = new Date(nowMs);
913
+ now.setSeconds(0, 0);
914
+ now.setMinutes(now.getMinutes() + 1);
915
+ return now.getTime() - nowMs;
916
+ }
917
+ /**
918
+ * Simple cron job scheduler with 5-field cron expression support.
919
+ */
920
+ var CronJob = class {
921
+ name;
922
+ expression;
923
+ handler;
924
+ onError;
925
+ logger;
926
+ matchers;
927
+ timer;
928
+ started = false;
929
+ lastTickMinute = -1;
930
+ constructor(options) {
931
+ this.expression = options.expression;
932
+ this.handler = options.handler;
933
+ this.onError = options.onError;
934
+ this.name = options.name || `cron#${(0, node_crypto.randomBytes)(8).toString("hex")}`;
935
+ this.logger = options.logger || new _faasjs_node_utils.Logger(`cron][${this.name}`);
936
+ this.matchers = parseExpression(this.expression);
937
+ }
938
+ start() {
939
+ if (this.started) {
940
+ this.logger.warn("start() has been called, skipped.");
941
+ return;
942
+ }
943
+ this.started = true;
944
+ this.lastTickMinute = -1;
945
+ this.logger.debug("[start] %s", this.expression);
946
+ this.scheduleNext();
947
+ }
948
+ stop() {
949
+ if (!this.started) return;
950
+ this.started = false;
951
+ if (this.timer) {
952
+ clearTimeout(this.timer);
953
+ this.timer = void 0;
954
+ }
955
+ this.logger.debug("[stop]");
956
+ }
957
+ get isStarted() {
958
+ return this.started;
959
+ }
960
+ scheduleNext() {
961
+ if (!this.started) return;
962
+ const delay = getDelayToNextMinute();
963
+ this.timer = setTimeout(() => {
964
+ this.tick();
965
+ }, delay);
966
+ }
967
+ tick() {
968
+ if (!this.started) return;
969
+ this.scheduleNext();
970
+ const now = /* @__PURE__ */ new Date();
971
+ const minute = Math.floor(now.getTime() / 6e4);
972
+ if (minute === this.lastTickMinute) return;
973
+ this.lastTickMinute = minute;
974
+ if (!shouldRun(this.matchers, now)) return;
975
+ this.execute(now);
976
+ }
977
+ async execute(now) {
978
+ const context = {
979
+ now,
980
+ logger: this.logger,
981
+ job: this
982
+ };
983
+ try {
984
+ await this.handler(context);
985
+ } catch (error) {
986
+ this.handleError(toError(error), context);
987
+ }
988
+ }
989
+ handleError(error, context) {
990
+ this.logger.error(error);
991
+ if (!this.onError) return;
992
+ Promise.resolve(this.onError(error, context)).catch((onErrorError) => {
993
+ this.logger.error(toError(onErrorError));
994
+ });
995
+ }
996
+ };
997
+ var CronJobRegistry = class {
998
+ cronJobs = /* @__PURE__ */ new Set();
999
+ mountedServerCount = 0;
1000
+ create(options) {
1001
+ const cronJob = new CronJob(options);
1002
+ this.cronJobs.add(cronJob);
1003
+ if (this.mountedServerCount > 0) cronJob.start();
1004
+ return cronJob;
1005
+ }
1006
+ remove(cronJob) {
1007
+ const removed = this.cronJobs.delete(cronJob);
1008
+ if (removed && this.mountedServerCount > 0) cronJob.stop();
1009
+ return removed;
1010
+ }
1011
+ list() {
1012
+ return Array.from(this.cronJobs);
1013
+ }
1014
+ mountServer() {
1015
+ this.mountedServerCount++;
1016
+ if (this.mountedServerCount > 1) return;
1017
+ for (const cronJob of this.cronJobs) cronJob.start();
1018
+ }
1019
+ unmountServer() {
1020
+ if (this.mountedServerCount === 0) return;
1021
+ this.mountedServerCount--;
1022
+ if (this.mountedServerCount > 0) return;
1023
+ for (const cronJob of this.cronJobs) cronJob.stop();
1024
+ }
1025
+ };
1026
+ const cronJobRegistry = new CronJobRegistry();
1027
+ /**
1028
+ * Create and register a cron job.
1029
+ *
1030
+ * Registered jobs are managed by `Server` lifecycle automatically.
1031
+ */
1032
+ function createCronJob(options) {
1033
+ return cronJobRegistry.create(options);
1034
+ }
1035
+ function removeCronJob(cronJob) {
1036
+ return cronJobRegistry.remove(cronJob);
1037
+ }
1038
+ function listCronJobs() {
1039
+ return cronJobRegistry.list();
1040
+ }
1041
+ function mountServerCronJobs() {
1042
+ cronJobRegistry.mountServer();
1043
+ }
1044
+ function unmountServerCronJobs() {
1045
+ cronJobRegistry.unmountServer();
1046
+ }
1047
+
1048
+ //#endregion
1049
+ //#region src/server/headers.ts
1050
+ const AdditionalHeaders = [
1051
+ "content-type",
1052
+ "authorization",
1053
+ "x-faasjs-request-id",
1054
+ "x-faasjs-timing-pending",
1055
+ "x-faasjs-timing-processing",
1056
+ "x-faasjs-timing-total"
1057
+ ];
1058
+ const ExposedHeadersBlacklist = [
1059
+ "host",
1060
+ "connection",
1061
+ "user-agent",
1062
+ "accept",
1063
+ "accept-language",
1064
+ "referer",
1065
+ "origin",
1066
+ "content-length",
1067
+ "content-md5"
1068
+ ];
1069
+ function buildCORSHeaders(headers, extra = {}) {
1070
+ const commonHeaderNames = [
1071
+ ...AdditionalHeaders,
1072
+ ...Object.keys(headers),
1073
+ ...Object.keys(extra),
1074
+ extra["access-control-request-headers"],
1075
+ headers["access-control-request-headers"]
1076
+ ].filter((key) => !!key && !key.startsWith("access-control-") && !ExposedHeadersBlacklist.includes(key.toLowerCase()));
1077
+ return {
1078
+ "access-control-allow-origin": headers.origin || "*",
1079
+ "access-control-allow-credentials": "true",
1080
+ "access-control-allow-methods": "OPTIONS, POST",
1081
+ "access-control-allow-headers": Array.from(new Set(commonHeaderNames.concat(extra["access-control-allow-headers"] || headers["access-control-allow-headers"] || []))).sort().join(", "),
1082
+ "access-control-expose-headers": Array.from(new Set(commonHeaderNames.concat(extra["access-control-expose-headers"] || headers["access-control-expose-headers"] || []))).sort().join(", "),
1083
+ ...extra
1084
+ };
1085
+ }
1086
+
1087
+ //#endregion
1088
+ //#region src/server/routes.ts
1089
+ function getRouteFiles(root, path) {
1090
+ const normalizedRoot = root.endsWith(node_path.sep) ? root : `${root}${node_path.sep}`;
1091
+ const normalizedPath = path.endsWith(node_path.sep) ? path.slice(0, -1) : path;
1092
+ const searchPaths = [
1093
+ `${normalizedPath}.func.ts`,
1094
+ `${normalizedPath}${node_path.sep}index.func.ts`,
1095
+ `${normalizedPath}${node_path.sep}default.func.ts`
1096
+ ];
1097
+ let currentPath = normalizedPath;
1098
+ while (currentPath.length > normalizedRoot.length) {
1099
+ const lastSepIndex = currentPath.lastIndexOf(node_path.sep);
1100
+ if (lastSepIndex === -1) break;
1101
+ currentPath = currentPath.substring(0, lastSepIndex);
1102
+ if (currentPath.length < normalizedRoot.length - 1) break;
1103
+ searchPaths.push(`${currentPath}${node_path.sep}default.func.ts`);
1104
+ }
1105
+ return searchPaths;
1106
+ }
1107
+
1108
+ //#endregion
1109
+ //#region src/server/index.ts
1110
+ const servers = [];
1111
+ function getAll() {
1112
+ return servers;
1113
+ }
1114
+ async function closeAll() {
1115
+ for (const server of servers) await server.close();
1116
+ }
1117
+ /**
1118
+ * FaasJS Server.
1119
+ *
1120
+ * @param {string} root The root path of the server.
1121
+ * @param {ServerOptions} opts The options of the server.
1122
+ * @returns {Server}
1123
+ * @example
1124
+ * ```ts
1125
+ * import { Server } from '@faasjs/core'
1126
+ *
1127
+ * const server = new Server(process.cwd(), {
1128
+ * port: 8080,
1129
+ * })
1130
+ *
1131
+ * server.listen()
1132
+ * ```
1133
+ */
1134
+ var Server = class {
1135
+ root;
1136
+ logger;
1137
+ options;
1138
+ closed = false;
1139
+ activeRequests = 0;
1140
+ cachedFuncs = {};
1141
+ onError;
1142
+ server;
1143
+ sockets = /* @__PURE__ */ new Set();
1144
+ constructor(root, opts = {}) {
1145
+ (0, _faasjs_node_utils.loadEnvFileIfExists)();
1146
+ if (!process.env.FaasEnv && process.env.NODE_ENV) process.env.FaasEnv = process.env.NODE_ENV;
1147
+ this.root = root.endsWith(node_path.sep) ? root : root + node_path.sep;
1148
+ this.options = (0, _faasjs_node_utils.deepMerge)({
1149
+ port: 3e3,
1150
+ cronJob: true
1151
+ }, opts);
1152
+ if (this.options.onClose && !node_util.types.isAsyncFunction(this.options.onClose)) throw Error("onClose must be async function");
1153
+ if (this.options.onError && !node_util.types.isAsyncFunction(this.options.onError)) throw Error("onError must be async function");
1154
+ if (this.options.onStart && !node_util.types.isAsyncFunction(this.options.onStart)) throw Error("onStart must be async function");
1155
+ if (!process.env.FaasMode) process.env.FaasMode = "mono";
1156
+ this.logger = new _faasjs_node_utils.Logger(`server][${(0, node_crypto.randomBytes)(16).toString("hex")}`);
1157
+ this.logger.debug("FaasJS server initialized: [%s] [%s] %s %j", process.env.FaasEnv, process.env.FaasMode, this.root, this.options);
1158
+ this.onError = (error) => {
1159
+ if (!(error instanceof Error)) error = Error(error);
1160
+ this.logger.error(error);
1161
+ if (this.options.onError) try {
1162
+ this.options.onError(error, { logger: this.logger });
1163
+ } catch (error) {
1164
+ this.logger.error(error);
1165
+ }
1166
+ };
1167
+ servers.push(this);
1168
+ }
1169
+ async handle(req, res, options = {}) {
1170
+ if (req.method === "OPTIONS") {
1171
+ this.handleOptionRequest(req, res);
1172
+ return;
1173
+ }
1174
+ const requestedAt = options.requestedAt || Date.now();
1175
+ const requestId = req.headers["x-faasjs-request-id"] || req.headers["x-request-id"] || `FS-${(0, node_crypto.randomBytes)(16).toString("hex")}`;
1176
+ const logger = new _faasjs_node_utils.Logger(requestId);
1177
+ logger.info("%s %s", req.method, req.url);
1178
+ const requestUrl = ensureRequestUrl(req, res);
1179
+ if (!requestUrl) return;
1180
+ const startedAt = Date.now();
1181
+ const path = (0, node_path.join)(this.root, requestUrl).replace(/\?.*/, "");
1182
+ const body = await this.readRequestBody(req);
1183
+ if (!await this.runBeforeHandle(req, res, logger)) return;
1184
+ let data;
1185
+ try {
1186
+ const cache = await this.getOrLoadHandler(path, options.filepath, logger);
1187
+ data = await this.invokeHandler(cache, req, res, requestUrl, body, requestId);
1188
+ } catch (error) {
1189
+ logger.error(error);
1190
+ data = error;
1191
+ }
1192
+ if (res.writableEnded) return;
1193
+ const headers = this.buildResponseHeaders(req, requestId, requestedAt, startedAt, data);
1194
+ for (const key in headers) res.setHeader(key, headers[key]);
1195
+ if (data instanceof Response) {
1196
+ res.statusCode = data.status;
1197
+ const reader = data.body?.getReader();
1198
+ if (reader) {
1199
+ const stream = node_stream.Readable.from((async function* () {
1200
+ while (true) try {
1201
+ const { done, value } = await reader.read();
1202
+ if (done) break;
1203
+ if (value) yield value;
1204
+ } catch (error) {
1205
+ logger.error(error);
1206
+ break;
1207
+ }
1208
+ })());
1209
+ await new Promise((done) => {
1210
+ this.pipeToResponse(stream, res, done);
1211
+ });
1212
+ return;
1213
+ }
1214
+ res.end();
1215
+ return;
1216
+ }
1217
+ if (data.body instanceof ReadableStream) {
1218
+ if (typeof data.statusCode === "number") res.statusCode = data.statusCode;
1219
+ await new Promise((done) => {
1220
+ this.pipeToResponse(node_stream.Readable.fromWeb(data.body), res, done);
1221
+ });
1222
+ return;
1223
+ }
1224
+ const statusCode = getErrorStatusCode(data);
1225
+ if (statusCode === 500) {
1226
+ respondWithJsonError(res, 500, getErrorMessage(data));
1227
+ return;
1228
+ }
1229
+ let resBody;
1230
+ if (data instanceof Error || data?.constructor?.name?.includes("Error") || typeof data === "undefined" || data === null) {
1231
+ if (typeof statusCode === "number") respondWithJsonError(res, statusCode, getErrorMessage(data, statusCode === 500 ? INTERNAL_SERVER_ERROR_MESSAGE : "No response"));
1232
+ else respondWithInternalServerError(res);
1233
+ return;
1234
+ }
1235
+ if (typeof statusCode === "number") res.statusCode = statusCode;
1236
+ if (data.body) resBody = data.body;
1237
+ if (typeof resBody !== "undefined") {
1238
+ logger.debug("Response %s %j", res.statusCode, headers);
1239
+ res.write(resBody);
1240
+ }
1241
+ res.end();
1242
+ }
1243
+ readRequestBody(req) {
1244
+ return new Promise((resolve) => {
1245
+ let body = "";
1246
+ req.on("readable", () => {
1247
+ body += req.read() || "";
1248
+ });
1249
+ req.on("end", () => {
1250
+ resolve(body);
1251
+ });
1252
+ });
1253
+ }
1254
+ async runBeforeHandle(req, res, logger) {
1255
+ if (!this.options.beforeHandle) return true;
1256
+ try {
1257
+ await this.options.beforeHandle(req, res, { logger });
1258
+ return !res.writableEnded;
1259
+ } catch (error) {
1260
+ logger.error(error);
1261
+ respondWithInternalServerError(res);
1262
+ return false;
1263
+ }
1264
+ }
1265
+ async getOrLoadHandler(path, filepath, logger) {
1266
+ const cached = this.cachedFuncs[path];
1267
+ if (cached?.handler) {
1268
+ logger.debug("response with cached %s", cached.file);
1269
+ return cached;
1270
+ }
1271
+ const file = filepath || this.getFilePath(path);
1272
+ const cache = { file };
1273
+ logger.debug("response with %s", cache.file);
1274
+ const func = await (0, _faasjs_node_utils.loadPackage)(file, ["func", "default"]);
1275
+ func.config = (0, _faasjs_node_utils.loadConfig)(this.root, path, process.env.FaasEnv || "development", logger);
1276
+ if (!func.config) throw Error("No config file found");
1277
+ cache.handler = func.export().handler;
1278
+ this.cachedFuncs[path] = cache;
1279
+ return cache;
1280
+ }
1281
+ async invokeHandler(cache, req, res, requestUrl, body, requestId) {
1282
+ const url = new URL(requestUrl, `http://${req.headers.host}`);
1283
+ if (!cache.handler) throw Error("handler is undefined");
1284
+ return cache.handler({
1285
+ headers: req.headers,
1286
+ httpMethod: req.method,
1287
+ path: url.pathname,
1288
+ queryString: Object.fromEntries(new URLSearchParams(url.search)),
1289
+ body,
1290
+ raw: {
1291
+ request: req,
1292
+ response: res
1293
+ }
1294
+ }, { request_id: requestId });
1295
+ }
1296
+ buildResponseHeaders(req, requestId, requestedAt, startedAt, data) {
1297
+ const finishedAt = Date.now();
1298
+ let headers = buildCORSHeaders(req.headers, {
1299
+ "x-faasjs-request-id": requestId,
1300
+ "x-faasjs-timing-pending": (startedAt - requestedAt).toString()
1301
+ });
1302
+ if (data.headers) headers = Object.assign(headers, data.headers);
1303
+ if (!headers["x-faasjs-timing-processing"]) headers["x-faasjs-timing-processing"] = (finishedAt - startedAt).toString();
1304
+ if (!headers["x-faasjs-timing-total"]) headers["x-faasjs-timing-total"] = (finishedAt - requestedAt).toString();
1305
+ Object.freeze(headers);
1306
+ return headers;
1307
+ }
1308
+ pipeToResponse(stream, res, done) {
1309
+ stream.pipe(res).on("finish", () => {
1310
+ res.end();
1311
+ done();
1312
+ }).on("error", (err) => {
1313
+ this.onError(err);
1314
+ respondWithInternalServerError(res);
1315
+ done();
1316
+ });
1317
+ }
1318
+ /**
1319
+ * Start server.
1320
+ * @returns {Server}
1321
+ */
1322
+ listen() {
1323
+ if (this.server) throw Error("Server already running");
1324
+ const mounted = {};
1325
+ if (this.options.onStart) {
1326
+ this.logger.debug("[onStart] begin");
1327
+ this.logger.time(`${this.logger.label}onStart`);
1328
+ this.options.onStart({ logger: this.logger }).catch(this.onError).finally(() => this.logger.timeEnd(`${this.logger.label}onStart`, "[onStart] end"));
1329
+ }
1330
+ this.server = (0, node_http.createServer)(async (req, res) => {
1331
+ this.activeRequests++;
1332
+ res.on("finish", () => this.activeRequests--);
1333
+ if (req.method === "OPTIONS") return this.handleOptionRequest(req, res);
1334
+ const requestUrl = ensureRequestUrl(req, res);
1335
+ if (!requestUrl) return;
1336
+ const path = (0, node_path.join)(this.root, requestUrl).replace(/\?.*/, "");
1337
+ if (!mounted[path]) mounted[path] = { pending: [] };
1338
+ mounted[path].pending.push([
1339
+ req,
1340
+ res,
1341
+ Date.now()
1342
+ ]);
1343
+ const pending = mounted[path].pending;
1344
+ mounted[path].pending = [];
1345
+ for (const event of pending) await this.handle(event[0], event[1], { requestedAt: event[2] });
1346
+ }).on("connection", (socket) => {
1347
+ this.sockets.add(socket);
1348
+ socket.on("close", () => {
1349
+ this.sockets.delete(socket);
1350
+ });
1351
+ }).on("error", (e) => {
1352
+ if ("code" in e && e.code === "EADDRINUSE") {
1353
+ (0, node_child_process.execSync)(`lsof -i :${this.options.port}`, { stdio: "inherit" });
1354
+ this.logger.error("Port %s is already in use. Please kill the process or use another port.", this.options.port);
1355
+ }
1356
+ this.onError(e);
1357
+ }).listen(this.options.port, "0.0.0.0");
1358
+ if (this.options.cronJob) try {
1359
+ mountServerCronJobs();
1360
+ } catch (error) {
1361
+ this.onError(error);
1362
+ }
1363
+ process.on("uncaughtException", (e) => {
1364
+ this.logger.debug("Uncaught exception");
1365
+ this.onError(e);
1366
+ }).on("unhandledRejection", (e) => {
1367
+ this.logger.debug("Unhandled rejection");
1368
+ this.onError(e);
1369
+ }).on("SIGTERM", async () => {
1370
+ this.logger.debug("received SIGTERM");
1371
+ if (this.closed) {
1372
+ this.logger.debug("already closed");
1373
+ return;
1374
+ }
1375
+ await this.close();
1376
+ if (!process.env.JEST_WORKER_ID && !process.env.VITEST_POOL_ID) process.exit(0);
1377
+ }).on("SIGINT", async () => {
1378
+ this.logger.debug("received SIGINT");
1379
+ if (this.closed) {
1380
+ this.logger.debug("already closed");
1381
+ return;
1382
+ }
1383
+ await this.close();
1384
+ if (!process.env.JEST_WORKER_ID && !process.env.VITEST_POOL_ID) process.exit(0);
1385
+ });
1386
+ this.logger.info("[%s] Listen http://localhost:%s with", process.env.FaasEnv, this.options.port, this.root);
1387
+ return this.server;
1388
+ }
1389
+ /**
1390
+ * Close server.
1391
+ */
1392
+ async close() {
1393
+ if (this.closed) {
1394
+ this.logger.debug("already closed");
1395
+ return;
1396
+ }
1397
+ this.logger.debug("closing");
1398
+ this.logger.time(`${this.logger.label}close`);
1399
+ if (this.options.cronJob) try {
1400
+ unmountServerCronJobs();
1401
+ } catch (error) {
1402
+ this.onError(error);
1403
+ }
1404
+ if (this.activeRequests) await new Promise((resolve) => {
1405
+ const check = () => {
1406
+ if (this.activeRequests === 0) {
1407
+ resolve();
1408
+ return;
1409
+ }
1410
+ this.logger.debug("waiting for %i requests", this.activeRequests);
1411
+ setTimeout(check, 50);
1412
+ };
1413
+ check();
1414
+ });
1415
+ for (const socket of this.sockets) try {
1416
+ socket.destroy();
1417
+ } catch (error) {
1418
+ this.onError(error);
1419
+ } finally {
1420
+ this.sockets.delete(socket);
1421
+ }
1422
+ const server = this.server;
1423
+ if (server) await new Promise((resolve) => {
1424
+ server.close((err) => {
1425
+ if (err) this.onError(err);
1426
+ resolve();
1427
+ });
1428
+ });
1429
+ if (this.options.onClose) {
1430
+ this.logger.debug("[onClose] begin");
1431
+ this.logger.time(`${this.logger.label}onClose`);
1432
+ try {
1433
+ await this.options.onClose({ logger: this.logger });
1434
+ } catch (error) {
1435
+ this.onError(error);
1436
+ }
1437
+ this.logger.timeEnd(`${this.logger.label}onClose`, "[onClose] end");
1438
+ }
1439
+ this.logger.timeEnd(`${this.logger.label}close`, "closed");
1440
+ await (0, _faasjs_node_utils.getTransport)().stop();
1441
+ this.closed = true;
1442
+ }
1443
+ /**
1444
+ * Middleware function to handle incoming HTTP requests.
1445
+ *
1446
+ * @param req - The incoming HTTP request object.
1447
+ * @param res - The server response object.
1448
+ * @param next - A callback function to pass control to the next middleware.
1449
+ * @returns A promise that resolves when the middleware processing is complete.
1450
+ */
1451
+ async middleware(req, res, next) {
1452
+ const requestUrl = ensureRequestUrl(req, res);
1453
+ if (!requestUrl) {
1454
+ next();
1455
+ return;
1456
+ }
1457
+ try {
1458
+ const filepath = this.getFilePath((0, node_path.join)(this.root, requestUrl).replace(/\?.*/, ""));
1459
+ await this.handle(req, res, {
1460
+ requestedAt: Date.now(),
1461
+ filepath
1462
+ });
1463
+ } catch (error) {
1464
+ this.logger.debug("middleware error", error);
1465
+ }
1466
+ next();
1467
+ }
1468
+ getFilePath(path) {
1469
+ if (/^(\.|\|\/)+$/.test(path)) throw Error("Illegal characters");
1470
+ const searchPaths = getRouteFiles(this.root, path);
1471
+ for (const path of searchPaths) if ((0, node_fs.existsSync)(path)) return (0, node_path.resolve)(".", path);
1472
+ throw new HttpError({
1473
+ statusCode: 404,
1474
+ message: process.env.FaasEnv === "production" ? "Not found." : `Not found function file.\nSearch paths:\n${searchPaths.map((p) => `- ${p}`).join("\n")}`
1475
+ });
1476
+ }
1477
+ handleOptionRequest(req, res) {
1478
+ res.writeHead(204, buildCORSHeaders(req.headers));
1479
+ res.end();
1480
+ }
1481
+ };
1482
+
1483
+ //#endregion
1484
+ //#region src/knex/pglite.ts
1485
+ function parsePgliteConnection(connection) {
1486
+ if (typeof connection === "undefined" || connection === null) return `memory://${(0, node_crypto.randomUUID)()}`;
1487
+ if (typeof connection !== "string" || !connection.trim().length) throw Error("[Knex] Invalid \"pglite\" connection, expected non-empty string in config.connection or SECRET_<NAME>_CONNECTION.");
1488
+ return connection;
1489
+ }
1490
+ function ensurePgliteConnectionPath(connection) {
1491
+ if (connection.includes("://")) return;
1492
+ (0, node_fs.mkdirSync)((0, node_path.dirname)((0, node_path.resolve)(connection)), { recursive: true });
1493
+ }
1494
+ async function loadPglitePackages() {
1495
+ try {
1496
+ const pgliteModule = await (0, _faasjs_node_utils.loadPackage)("@electric-sql/pglite", []);
1497
+ const PgliteDialect = await (0, _faasjs_node_utils.loadPackage)("knex-pglite");
1498
+ if (typeof pgliteModule.PGlite !== "function" || !pgliteModule.types) throw Error("Invalid @electric-sql/pglite exports");
1499
+ if (typeof PgliteDialect !== "function") throw Error("Invalid knex-pglite exports");
1500
+ return {
1501
+ PGlite: pgliteModule.PGlite,
1502
+ types: pgliteModule.types,
1503
+ PgliteDialect
1504
+ };
1505
+ } catch {
1506
+ throw Error("[Knex] client \"pglite\" requires dependencies \"@electric-sql/pglite\" and \"knex-pglite\". Please install both packages in your project.");
1507
+ }
1508
+ }
1509
+ /**
1510
+ * Create a knex instance backed by `knex-pglite`.
1511
+ * If connection is missing, it defaults to an in-memory database.
1512
+ */
1513
+ async function createPgliteKnex(config = {}, connection) {
1514
+ const resolvedConnection = parsePgliteConnection(connection ?? config.connection);
1515
+ ensurePgliteConnectionPath(resolvedConnection);
1516
+ const { PGlite, types, PgliteDialect } = await loadPglitePackages();
1517
+ const { client: _client, connection: _connection, pool: _pool, ...restConfig } = config;
1518
+ const pglite = new PGlite(resolvedConnection, { parsers: {
1519
+ [types.INT2]: (v) => Number.parseInt(v, 10),
1520
+ [types.INT4]: (v) => Number.parseInt(v, 10),
1521
+ [types.INT8]: (v) => Number.parseInt(v, 10),
1522
+ [types.FLOAT4]: (v) => Number.parseFloat(v),
1523
+ [types.FLOAT8]: (v) => Number.parseFloat(v),
1524
+ [types.NUMERIC]: (v) => Number.parseFloat(v)
1525
+ } });
1526
+ return (0, knex.default)({
1527
+ ...restConfig,
1528
+ client: PgliteDialect,
1529
+ connection: { pglite }
1530
+ });
1531
+ }
1532
+ /**
1533
+ * Mount a knex adapter to `globalThis.FaasJS_Knex` for `@faasjs/core`.
1534
+ */
1535
+ function mountFaasKnex(db, options = {}) {
1536
+ const globalWithFaasKnex = globalThis;
1537
+ const name = options.name || "knex";
1538
+ if (!globalWithFaasKnex.FaasJS_Knex) globalWithFaasKnex.FaasJS_Knex = {};
1539
+ globalWithFaasKnex.FaasJS_Knex[name] = {
1540
+ adapter: db,
1541
+ query: db,
1542
+ config: options.config || {}
1543
+ };
1544
+ }
1545
+ /**
1546
+ * Remove mounted knex adapter from `globalThis.FaasJS_Knex`.
1547
+ */
1548
+ function unmountFaasKnex(name = "knex") {
1549
+ const globalWithFaasKnex = globalThis;
1550
+ if (!globalWithFaasKnex.FaasJS_Knex) return;
1551
+ delete globalWithFaasKnex.FaasJS_Knex[name];
1552
+ }
1553
+
1554
+ //#endregion
1555
+ //#region src/knex/plugin.ts
1556
+ /**
1557
+ * Origin [knex](https://knexjs.org/) instance.
1558
+ */
1559
+ const originKnex = knex.default;
1560
+ const Name = "knex";
1561
+ if (!global.FaasJS_Knex) global.FaasJS_Knex = {};
1562
+ function getGlobalKnexAdapters() {
1563
+ if (!global.FaasJS_Knex) global.FaasJS_Knex = {};
1564
+ return global.FaasJS_Knex;
1565
+ }
1566
+ async function initPostgresTypeParsers() {
1567
+ const pg = await (0, _faasjs_node_utils.loadPackage)("pg");
1568
+ pg.types.setTypeParser(pg.types.builtins.INT2, (v) => Number.parseInt(v, 10));
1569
+ pg.types.setTypeParser(pg.types.builtins.INT4, (v) => Number.parseInt(v, 10));
1570
+ pg.types.setTypeParser(pg.types.builtins.INT8, (v) => Number.parseInt(v, 10));
1571
+ pg.types.setTypeParser(pg.types.builtins.FLOAT4, (v) => Number.parseFloat(v));
1572
+ pg.types.setTypeParser(pg.types.builtins.FLOAT8, (v) => Number.parseFloat(v));
1573
+ pg.types.setTypeParser(pg.types.builtins.NUMERIC, (v) => Number.parseFloat(v));
1574
+ }
1575
+ var Knex = class {
1576
+ type = "knex";
1577
+ name = Name;
1578
+ config;
1579
+ adapter;
1580
+ query;
1581
+ logger;
1582
+ constructor(config) {
1583
+ if (config) {
1584
+ this.name = config.name || this.name;
1585
+ this.config = config.config || Object.create(null);
1586
+ } else this.config = Object.create(null);
1587
+ }
1588
+ async onMount(data, next) {
1589
+ this.logger = data.logger;
1590
+ const existsAdapter = getGlobalKnexAdapters()[this.name];
1591
+ if (existsAdapter) {
1592
+ this.config = existsAdapter.config;
1593
+ this.adapter = existsAdapter.adapter;
1594
+ this.query = this.adapter;
1595
+ this.logger.debug("use exists adapter");
1596
+ await next();
1597
+ return;
1598
+ }
1599
+ const prefix = `SECRET_${this.name.toUpperCase()}_`;
1600
+ for (let key in process.env) if (key.startsWith(prefix)) {
1601
+ const value = process.env[key];
1602
+ key = key.replace(prefix, "").toLowerCase();
1603
+ if (typeof this.config[key] === "undefined") if (key.startsWith("connection_")) {
1604
+ if (typeof this.config.connection === "string") continue;
1605
+ if (!this.config.connection || typeof this.config.connection !== "object") this.config.connection = Object.create(null);
1606
+ this.config.connection[key.replace("connection_", "")] = value;
1607
+ } else this.config[key] = value;
1608
+ }
1609
+ 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);
1610
+ if (this.config.client === "pglite") {
1611
+ const connectionEnvKeys = Object.keys(process.env).filter((key) => key.startsWith(`${prefix}CONNECTION_`));
1612
+ if (connectionEnvKeys.length) throw Error(`[Knex] Invalid "pglite" env keys: ${connectionEnvKeys.join(", ")}. Use ${prefix}CONNECTION instead.`);
1613
+ this.adapter = await createPgliteKnex(this.config);
1614
+ } else {
1615
+ switch (this.config.client) {
1616
+ case "sqlite3":
1617
+ this.config.client = "better-sqlite3";
1618
+ this.config.useNullAsDefault = true;
1619
+ break;
1620
+ case "pg":
1621
+ if (!this.config.pool) this.config.pool = Object.create(null);
1622
+ this.config.pool = Object.assign({
1623
+ propagateCreateError: false,
1624
+ min: 0,
1625
+ max: 10,
1626
+ acquireTimeoutMillis: 5e3,
1627
+ idleTimeoutMillis: 3e4
1628
+ }, this.config.pool);
1629
+ if (typeof this.config.connection === "string" && !this.config.connection.includes("json=true")) this.config.connection = `${this.config.connection}?json=true`;
1630
+ break;
1631
+ default:
1632
+ if (typeof this.config.client === "string") {
1633
+ if (this.config.client.startsWith("npm:")) {
1634
+ const client = await (0, _faasjs_node_utils.loadPackage)(this.config.client.replace("npm:", ""));
1635
+ if (!client) throw Error(`Invalid client: ${this.config.client}`);
1636
+ if (typeof client === "function") {
1637
+ this.config.client = client;
1638
+ break;
1639
+ }
1640
+ if (client.default && typeof client.default === "function") {
1641
+ this.config.client = client.default;
1642
+ break;
1643
+ }
1644
+ throw Error(`Invalid client: ${this.config.client}`);
1645
+ }
1646
+ }
1647
+ break;
1648
+ }
1649
+ this.adapter = (0, knex.default)(this.config);
1650
+ if (this.config.client === "pg") await initPostgresTypeParsers();
1651
+ }
1652
+ this.query = this.adapter;
1653
+ this.query.on("query", ({ sql, __knexQueryUid, bindings }) => {
1654
+ if (!__knexQueryUid) return;
1655
+ this.logger.time(`Knex${this.name}${__knexQueryUid}`);
1656
+ this.logger.debug("[%s] query begin: %s %j", __knexQueryUid, sql, bindings);
1657
+ }).on("query-response", (response, { sql, __knexQueryUid, bindings }) => {
1658
+ if (!__knexQueryUid) return;
1659
+ this.logger.timeEnd(`Knex${this.name}${__knexQueryUid}`, "[%s] query done: %s %j %j", __knexQueryUid, sql, bindings, response);
1660
+ }).on("query-error", (_, { __knexQueryUid, sql, bindings }) => {
1661
+ if (!__knexQueryUid) return;
1662
+ this.logger.timeEnd(`Knex${this.name}${__knexQueryUid}`, "[%s] query failed: %s %j", __knexQueryUid, sql, bindings);
1663
+ });
1664
+ data.logger.debug("connected");
1665
+ getGlobalKnexAdapters()[this.name] = this;
1666
+ await next();
1667
+ }
1668
+ async onInvoke(data, next) {
1669
+ this.logger = data.logger;
1670
+ await next();
1671
+ }
1672
+ async raw(sql, bindings = []) {
1673
+ if (!this.adapter) throw Error("[Knex] Client not initialized.");
1674
+ return this.adapter.raw(sql, bindings);
1675
+ }
1676
+ /**
1677
+ * Wraps a transaction, returning a promise that resolves to the return value of the callback.
1678
+ *
1679
+ * - Support 'commit' and 'rollback' event.
1680
+ */
1681
+ async transaction(scope, config, options) {
1682
+ if (!this.adapter) throw Error(`[${this.name}] Client not initialized.`);
1683
+ if (options?.trx) return scope(options.trx);
1684
+ const trx = await this.adapter.transaction(config);
1685
+ const trxId = (0, node_crypto.randomUUID)();
1686
+ this.logger.debug("[%s] transaction begin", trxId);
1687
+ try {
1688
+ const result = await scope(trx);
1689
+ if (trx.isCompleted()) {
1690
+ this.logger.debug("[%s] transaction has been finished in scope", trxId);
1691
+ return result;
1692
+ }
1693
+ this.logger.debug("[%s] transaction begin commit", trxId);
1694
+ await trx.commit();
1695
+ this.logger.debug("[%s] transaction committed: %j", trxId, result);
1696
+ trx.emit("commit");
1697
+ return result;
1698
+ } catch (error) {
1699
+ await trx.rollback(error);
1700
+ this.logger.error("[%s] transaction rollback: %s", trxId, error);
1701
+ trx.emit("rollback", error);
1702
+ throw error;
1703
+ }
1704
+ }
1705
+ schema() {
1706
+ if (!this.adapter) throw Error(`[${this.name}] Client not initialized.`);
1707
+ return this.adapter.schema;
1708
+ }
1709
+ async quit() {
1710
+ const adapters = getGlobalKnexAdapters();
1711
+ if (!adapters[this.name]) return;
1712
+ try {
1713
+ await adapters[this.name].adapter.destroy();
1714
+ delete adapters[this.name];
1715
+ } catch (error) {
1716
+ console.error(error);
1717
+ }
1718
+ }
1719
+ };
1720
+ function useKnex(config) {
1721
+ const name = config?.name || Name;
1722
+ const adapters = getGlobalKnexAdapters();
1723
+ if (adapters[name]) return usePlugin(adapters[name]);
1724
+ return usePlugin(new Knex(config));
1725
+ }
1726
+ function query(table) {
1727
+ return useKnex().query(table);
1728
+ }
1729
+ async function transaction(scope, config, options) {
1730
+ return useKnex().transaction(scope, config, options);
1731
+ }
1732
+ async function raw(sql, bindings = []) {
1733
+ return useKnex().raw(sql, bindings);
1734
+ }
1735
+
1736
+ //#endregion
1737
+ //#region src/knex/schema.ts
1738
+ const DefaultMigratorConfig = {
1739
+ directory: "./src/db/migrations",
1740
+ extension: "ts"
1741
+ };
1742
+ /**
1743
+ * Migration helper for FaasJS knex plugin.
1744
+ */
1745
+ var KnexSchema = class {
1746
+ knex;
1747
+ constructor(knex) {
1748
+ this.knex = knex;
1749
+ }
1750
+ getAdapter() {
1751
+ if (!this.knex.adapter) throw Error(`[${this.knex.name}] Client not initialized.`);
1752
+ return this.knex.adapter;
1753
+ }
1754
+ getMigratorConfig() {
1755
+ return Object.assign({}, DefaultMigratorConfig, this.knex.config?.migrations || Object.create(null));
1756
+ }
1757
+ async migrateLatest() {
1758
+ return this.getAdapter().migrate.latest(this.getMigratorConfig());
1759
+ }
1760
+ async migrateRollback() {
1761
+ return this.getAdapter().migrate.rollback(this.getMigratorConfig());
1762
+ }
1763
+ async migrateStatus() {
1764
+ return this.getAdapter().migrate.status(this.getMigratorConfig());
1765
+ }
1766
+ async migrateCurrentVersion() {
1767
+ return this.getAdapter().migrate.currentVersion(this.getMigratorConfig());
1768
+ }
1769
+ async migrateMake(name) {
1770
+ const migrationName = name?.trim();
1771
+ if (!migrationName) throw Error("[KnexSchema] Missing migration name. Usage: npm run migrate:make -- create_users");
1772
+ return this.getAdapter().migrate.make(migrationName, this.getMigratorConfig());
1773
+ }
1774
+ };
1775
+
1776
+ //#endregion
1777
+ //#region src/index.ts
1778
+ function formatPluginModuleName(type) {
1779
+ const normalizedType = type.startsWith("npm:") ? type.slice(4) : type;
1780
+ if (normalizedType === "http" || normalizedType === "@faasjs/http" || normalizedType === "knex" || normalizedType === "@faasjs/knex") return "@faasjs/core";
1781
+ if (normalizedType.startsWith("@") || normalizedType.startsWith(".") || normalizedType.startsWith("/") || normalizedType.includes(":")) return normalizedType;
1782
+ return `@faasjs/${normalizedType}`;
1783
+ }
1784
+ function formatPluginClassName(type) {
1785
+ return type.replace(/^@[^/]+\//, "").split(/[^A-Za-z0-9]+/).filter(Boolean).map((item) => item.slice(0, 1).toUpperCase() + item.slice(1)).join("");
1786
+ }
1787
+ function isPluginConstructor(value) {
1788
+ if (typeof value !== "function") return false;
1789
+ const prototype = value.prototype;
1790
+ if (!prototype || typeof prototype !== "object") return false;
1791
+ return typeof prototype.onMount === "function" || typeof prototype.onInvoke === "function";
1792
+ }
1793
+ function normalizeIssueMessage(message) {
1794
+ return message.replace(": expected", ", expected").replace(/>=\s+/g, ">=").replace(/<=\s+/g, "<=");
1795
+ }
1796
+ function formatZodErrorMessage(error) {
1797
+ const lines = ["Invalid params"];
1798
+ for (const issue of error.issues) {
1799
+ const path = issue.path.length ? issue.path.map((item) => String(item)).join(".") : "<root>";
1800
+ lines.push(`${path}: ${normalizeIssueMessage(issue.message)}`);
1801
+ }
1802
+ return lines.join("\n");
1803
+ }
1804
+ function findPluginByType(func, type) {
1805
+ return func.plugins.find((plugin) => plugin.type === type);
1806
+ }
1807
+ function resolvePluginConfig(key, rawConfig) {
1808
+ const configValue = rawConfig && typeof rawConfig === "object" ? Object.assign(Object.create(null), rawConfig) : Object.create(null);
1809
+ return {
1810
+ configValue,
1811
+ pluginName: typeof configValue.name === "string" && configValue.name.length ? configValue.name : key,
1812
+ pluginType: typeof configValue.type === "string" && configValue.type || typeof rawConfig === "string" && rawConfig || key
1813
+ };
1814
+ }
1815
+ var CoreFunc = class extends Func {
1816
+ loadedConfigPlugins = false;
1817
+ insertPluginBeforeRunHandler(plugin) {
1818
+ const index = this.plugins.findIndex((item) => item.type === "handler" && item.name === "handler");
1819
+ if (index === -1) this.plugins.push(plugin);
1820
+ else this.plugins.splice(index, 0, plugin);
1821
+ }
1822
+ async resolvePluginConstructor(moduleName, className, pluginName) {
1823
+ let mod;
1824
+ try {
1825
+ mod = await import(moduleName);
1826
+ } catch (error) {
1827
+ throw Error(`[defineApi] Failed to load plugin "${pluginName}" from "${moduleName}": ${error.message}`);
1828
+ }
1829
+ if (className && isPluginConstructor(mod[className])) return mod[className];
1830
+ if (isPluginConstructor(mod.default)) return mod.default;
1831
+ throw Error(`[defineApi] Failed to resolve plugin class "${className}" from "${moduleName}" for plugin "${pluginName}". Supported exports are named class "${className}" or default class export.`);
1832
+ }
1833
+ async loadPluginsFromConfig(config) {
1834
+ const pluginConfigs = config.plugins || Object.create(null);
1835
+ for (const key in pluginConfigs) {
1836
+ if (!Object.hasOwn(pluginConfigs, key)) continue;
1837
+ const rawConfig = pluginConfigs[key];
1838
+ const { configValue, pluginName, pluginType } = resolvePluginConfig(key, rawConfig);
1839
+ if (this.plugins.find((plugin) => plugin.name === pluginName)) continue;
1840
+ const moduleName = formatPluginModuleName(pluginType);
1841
+ const className = formatPluginClassName(pluginType);
1842
+ const PluginConstructor = await this.resolvePluginConstructor(moduleName, className, pluginName);
1843
+ let plugin;
1844
+ try {
1845
+ plugin = new PluginConstructor({
1846
+ ...configValue,
1847
+ name: pluginName,
1848
+ type: pluginType
1849
+ });
1850
+ } catch (error) {
1851
+ throw Error(`[defineApi] Failed to initialize plugin "${pluginName}" from "${moduleName}": ${error.message}`);
1852
+ }
1853
+ if (!plugin || typeof plugin !== "object") throw Error(`[defineApi] Invalid plugin instance for "${pluginName}" from "${moduleName}".`);
1854
+ this.insertPluginBeforeRunHandler(plugin);
1855
+ }
1856
+ this.loadedConfigPlugins = true;
1857
+ }
1858
+ async mount(data = {
1859
+ event: Object.create(null),
1860
+ context: Object.create(null)
1861
+ }) {
1862
+ if (!data.config) data.config = this.config;
1863
+ if (!this.loadedConfigPlugins) await this.loadPluginsFromConfig(data.config);
1864
+ await super.mount(data);
1865
+ }
1866
+ };
1867
+ /**
1868
+ * Create an HTTP API function with optional Zod validation.
1869
+ *
1870
+ * Plugins are always auto-loaded from `func.config.plugins`.
1871
+ * Plugin module exports must be either a named class (normalized from
1872
+ * plugin type) or a default class export.
1873
+ *
1874
+ * The `http` plugin is required.
1875
+ */
1876
+ function defineApi(options) {
1877
+ let func;
1878
+ let pluginRefsResolved = false;
1879
+ let hasHttp = false;
1880
+ let knexQuery;
1881
+ const parseParams = async (event) => {
1882
+ if (!pluginRefsResolved) {
1883
+ hasHttp = !!findPluginByType(func, "http");
1884
+ knexQuery = findPluginByType(func, "knex")?.query;
1885
+ pluginRefsResolved = true;
1886
+ }
1887
+ if (!hasHttp) throw Error("[defineApi] Missing required \"http\" plugin. Please configure it in func.config.plugins.");
1888
+ if (!options.schema) return {};
1889
+ const result = await options.schema.safeParseAsync(event?.params ?? {});
1890
+ if (!result.success) throw new HttpError({
1891
+ statusCode: 400,
1892
+ message: formatZodErrorMessage(result.error)
1893
+ });
1894
+ return result.data;
1895
+ };
1896
+ const invokeHandler = async (data) => {
1897
+ const params = await parseParams(data.event);
1898
+ const invokeData = {
1899
+ ...data,
1900
+ params,
1901
+ knex: knexQuery
1902
+ };
1903
+ return options.handler(invokeData);
1904
+ };
1905
+ func = new CoreFunc({
1906
+ plugins: [],
1907
+ handler: invokeHandler
1908
+ });
1909
+ return func;
1910
+ }
1911
+
1912
+ //#endregion
1913
+ exports.ContentType = ContentType;
1914
+ exports.Cookie = Cookie;
1915
+ exports.CronJob = CronJob;
1916
+ exports.Func = Func;
1917
+ exports.Http = Http;
1918
+ exports.HttpError = HttpError;
1919
+ exports.Knex = Knex;
1920
+ exports.KnexSchema = KnexSchema;
1921
+ exports.Server = Server;
1922
+ exports.Session = Session;
1923
+ exports.closeAll = closeAll;
1924
+ exports.createCronJob = createCronJob;
1925
+ exports.createPgliteKnex = createPgliteKnex;
1926
+ exports.defineApi = defineApi;
1927
+ exports.getAll = getAll;
1928
+ exports.initPostgresTypeParsers = initPostgresTypeParsers;
1929
+ exports.listCronJobs = listCronJobs;
1930
+ exports.mountFaasKnex = mountFaasKnex;
1931
+ exports.nameFunc = nameFunc;
1932
+ exports.originKnex = originKnex;
1933
+ exports.parseFuncFilenameFromStack = parseFuncFilenameFromStack;
1934
+ exports.query = query;
1935
+ exports.raw = raw;
1936
+ exports.removeCronJob = removeCronJob;
1937
+ exports.staticHandler = staticHandler;
1938
+ exports.transaction = transaction;
1939
+ exports.unmountFaasKnex = unmountFaasKnex;
1940
+ exports.useFunc = useFunc;
1941
+ exports.useHttp = useHttp;
1942
+ exports.useKnex = useKnex;
1943
+ exports.useMiddleware = useMiddleware;
1944
+ exports.useMiddlewares = useMiddlewares;
1945
+ exports.usePlugin = usePlugin;
1946
+ Object.defineProperty(exports, 'z', {
1947
+ enumerable: true,
1948
+ get: function () {
1949
+ return zod;
1950
+ }
1951
+ });