milkio 0.0.1
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/.co.toml +2 -0
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/c.ts +83 -0
- package/defines/define-api-test-handler.ts +70 -0
- package/defines/define-api-test.ts +14 -0
- package/defines/define-api.ts +15 -0
- package/defines/define-http-handler.ts +203 -0
- package/defines/define-middleware.ts +9 -0
- package/defines/define-use.ts +13 -0
- package/index.ts +33 -0
- package/kernel/config.ts +15 -0
- package/kernel/context.ts +24 -0
- package/kernel/fail.ts +22 -0
- package/kernel/logger.ts +113 -0
- package/kernel/loongbao.ts +269 -0
- package/kernel/meta.ts +9 -0
- package/kernel/middleware.ts +50 -0
- package/kernel/runtime.ts +16 -0
- package/kernel/validate.ts +13 -0
- package/package.json +24 -0
- package/scripts/build-cookbook.ts +233 -0
- package/scripts/build-dto.ts +65 -0
- package/scripts/generate/generate-app-partial.ts +74 -0
- package/scripts/generate/generate-app.ts +153 -0
- package/scripts/generate/generate-database.ts +23 -0
- package/scripts/generate-database.ts +23 -0
- package/scripts/generate-partial.ts +15 -0
- package/scripts/generate.ts +23 -0
- package/templates/api.ts +49 -0
- package/tsconfig.json +33 -0
- package/types.ts +42 -0
- package/utils/create-template.ts +32 -0
- package/utils/create-ulid.ts +5 -0
- package/utils/env-to-boolean.ts +11 -0
- package/utils/env-to-number.ts +5 -0
- package/utils/env-to-string.ts +5 -0
- package/utils/exec.ts +27 -0
- package/utils/handle-catch-error.ts +37 -0
- package/utils/remove-dir.ts +22 -0
- package/utils/tson.ts +3 -0
package/.co.toml
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 southern-aurora
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/c.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-misused-promises, no-console, @typescript-eslint/no-explicit-any */
|
|
4
|
+
|
|
5
|
+
import { argv, cwd, env, exit } from "node:process";
|
|
6
|
+
import { exec } from "./utils/exec";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { $ } from "bun";
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
|
+
|
|
11
|
+
const rootPath = cwd();
|
|
12
|
+
const method = argv[2] as keyof typeof commands;
|
|
13
|
+
const params = argv.slice(3) as Parameters<(typeof commands)[keyof typeof commands]>;
|
|
14
|
+
|
|
15
|
+
const commands = {
|
|
16
|
+
async gen() {
|
|
17
|
+
await exec(rootPath, ["bun", "./node_modules/loongbao/scripts/generate.ts"]);
|
|
18
|
+
},
|
|
19
|
+
async "gen:database"() {
|
|
20
|
+
await exec(rootPath, ["bun", "./node_modules/loongbao/scripts/generate-database.ts"]);
|
|
21
|
+
},
|
|
22
|
+
async "build:cookbook"() {
|
|
23
|
+
await exec(rootPath, ["bun", "./node_modules/loongbao/scripts/build-cookbook.ts"]);
|
|
24
|
+
},
|
|
25
|
+
async "build:dto"() {
|
|
26
|
+
await exec(join(rootPath, "packages", "dto"), ["bun", "i"]);
|
|
27
|
+
await exec(rootPath, ["bun", "./node_modules/loongbao/scripts/generate.ts"]);
|
|
28
|
+
await exec(rootPath, ["bun", "./node_modules/loongbao/scripts/build-dto.ts"]);
|
|
29
|
+
},
|
|
30
|
+
async test(files?: string, reserved?: string) {
|
|
31
|
+
const run = async () => {
|
|
32
|
+
const packageJson = await JSON.parse((await readFile(join(rootPath, "package.json"))).toString());
|
|
33
|
+
// A bug has appeared in the new version of bun: https://github.com/oven-sh/bun/issues/9428
|
|
34
|
+
// Therefore, temporarily switch to another implementation.
|
|
35
|
+
// await $`${{ raw: packageJson.scripts.start }}`.env({
|
|
36
|
+
// ...env,
|
|
37
|
+
// LOONGBAO_RUN_MODE: "API_TEST",
|
|
38
|
+
// LOONGBAO_TEST: files ?? "1"
|
|
39
|
+
// });
|
|
40
|
+
//
|
|
41
|
+
await exec(rootPath, ["bash", "-c", packageJson.scripts.start as string], {
|
|
42
|
+
env: {
|
|
43
|
+
...env,
|
|
44
|
+
LOONGBAO_RUN_MODE: "API_TEST",
|
|
45
|
+
LOONGBAO_TEST: files ?? "1"
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
if (!env.LOONGBAO_TEST_RESERVED && reserved !== "1") {
|
|
50
|
+
// Normal test
|
|
51
|
+
await run();
|
|
52
|
+
} else {
|
|
53
|
+
// Keep after testing, the terminal never exits and is usually used for various IDE extensions.
|
|
54
|
+
try {
|
|
55
|
+
await run();
|
|
56
|
+
} catch (error) {}
|
|
57
|
+
while (true) {
|
|
58
|
+
const result = await new Promise((resolve) => {
|
|
59
|
+
const wasRaw = process.stdin.isRaw;
|
|
60
|
+
process.stdin.setRawMode(true);
|
|
61
|
+
process.stdin.resume();
|
|
62
|
+
process.stdin.once("data", (data) => {
|
|
63
|
+
process.stdin.pause();
|
|
64
|
+
process.stdin.setRawMode(wasRaw);
|
|
65
|
+
resolve(data.toString());
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
// No exit function is set
|
|
69
|
+
// if (result === "q") exit(0);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (method === undefined || !(method in commands)) {
|
|
76
|
+
console.log("Command does not exist, Supported commands are:");
|
|
77
|
+
console.log(" " + Object.keys(commands).join(", "));
|
|
78
|
+
exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await commands[method](...params);
|
|
82
|
+
|
|
83
|
+
exit(0);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument, no-console, @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { exit } from "node:process";
|
|
3
|
+
import schema from "../../../generate/api-schema";
|
|
4
|
+
import { type LoongbaoApp } from "..";
|
|
5
|
+
|
|
6
|
+
export const defineApiTestHandler = async <Paths extends Array<keyof (typeof schema)["apiTestsSchema"]>>(app: LoongbaoApp, paths: Paths | string | 1 | undefined) => {
|
|
7
|
+
console.log(`🧊 Loongbao Api Testing..\n`);
|
|
8
|
+
|
|
9
|
+
if (!paths) {
|
|
10
|
+
console.log("🧊 No paths specified.");
|
|
11
|
+
exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (paths === "1" || paths === 1) {
|
|
15
|
+
paths = Object.keys(schema.apiTestsSchema) as unknown as Paths;
|
|
16
|
+
} else if (typeof paths === "string") {
|
|
17
|
+
if (!paths.startsWith("[")) {
|
|
18
|
+
paths = [paths] as Paths;
|
|
19
|
+
} else {
|
|
20
|
+
paths = JSON.parse(paths) as Paths;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const tests = [];
|
|
25
|
+
const startedAt = new Date().getTime();
|
|
26
|
+
|
|
27
|
+
for (let path of paths) {
|
|
28
|
+
if (path.startsWith("/")) path = path.slice(1) as Paths[number];
|
|
29
|
+
tests.push(
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
(async () => {
|
|
32
|
+
// @ts-ignore
|
|
33
|
+
const module = await schema.apiTestsSchema[path]().module;
|
|
34
|
+
const cases = module.test.getCases();
|
|
35
|
+
let i = 0;
|
|
36
|
+
for (const cs of cases) {
|
|
37
|
+
++i;
|
|
38
|
+
const csStartedAt = new Date().getTime();
|
|
39
|
+
const clear = setTimeout(() => {
|
|
40
|
+
console.error(`------`);
|
|
41
|
+
console.error(`❌ TIMEOUT -- More than ${cs.timeout ?? 6000}ms`);
|
|
42
|
+
console.error(` ${cs.name} | Path: src/apps/${path as string}.ts | Case: ${i}`);
|
|
43
|
+
console.error(`------`);
|
|
44
|
+
exit(1);
|
|
45
|
+
}, cs.timeout ?? 6000);
|
|
46
|
+
await cs.handler({
|
|
47
|
+
execute: async (params: any, headers?: any, options?: any) => app.execute(path, params, headers ?? {}, options),
|
|
48
|
+
reject: (message?: string) => {
|
|
49
|
+
console.error(`------`);
|
|
50
|
+
console.error(`❌ REJECT -- ${message ?? "Test not satisfied"}`);
|
|
51
|
+
console.error(` ${cs.name} | Path: src/apps/${path as string}.ts | Case: ${i} | Time: ${new Date().getTime() - csStartedAt}ms`);
|
|
52
|
+
console.error(`------`);
|
|
53
|
+
exit(1);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
clearTimeout(clear);
|
|
57
|
+
console.log(`✅ DIRECT -- ${cs.name} | Path: src/apps/${path as string}.ts | Case: ${i} | Time: ${new Date().getTime() - csStartedAt}ms`);
|
|
58
|
+
}
|
|
59
|
+
})()
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await Promise.all(tests);
|
|
64
|
+
|
|
65
|
+
const endedAt = new Date().getTime();
|
|
66
|
+
|
|
67
|
+
console.log(`\n✅ All tests passed.`);
|
|
68
|
+
console.log(`🧊 Loongbao Api Testing took ${((endedAt - startedAt) / 1000).toFixed(2)}s\n`);
|
|
69
|
+
exit(0);
|
|
70
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Api, type ExecuteResult, type ExecuteOptions } from "..";
|
|
2
|
+
|
|
3
|
+
export function defineApiTest<ApiT extends Api>(_api: ApiT, cases: Array<ApiTestCases<ApiT>>) {
|
|
4
|
+
return {
|
|
5
|
+
getCases: () => cases,
|
|
6
|
+
isApiTest: true
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ApiTestCases<ApiT extends Api> = {
|
|
11
|
+
handler: (test: { execute: (params: Parameters<ApiT["action"]>[0], headers?: Record<string, string>, options?: ExecuteOptions) => Promise<ExecuteResult<Awaited<ReturnType<ApiT["action"]>>>>; reject: (message?: string) => void }) => Promise<void> | void;
|
|
12
|
+
name: string;
|
|
13
|
+
timeout?: number;
|
|
14
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Meta } from "../../../src/meta";
|
|
2
|
+
import type { Context } from "../../../src/context";
|
|
3
|
+
|
|
4
|
+
export function defineApi<ApiT extends Api>(api: ApiT): ApiT & { isApi: true } {
|
|
5
|
+
return {
|
|
6
|
+
...api,
|
|
7
|
+
isApi: true
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type Api = {
|
|
12
|
+
meta: Meta;
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
action: (params: any, context: Context) => Promise<unknown> | unknown;
|
|
15
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { configFramework, loggerPushTags, loggerSubmit, useLogger, loggerSubmitAll, runtime, MiddlewareEvent } from "..";
|
|
3
|
+
import type { ExecuteId, LoongbaoApp, Mixin } from "..";
|
|
4
|
+
import { hanldeCatchError } from "../utils/handle-catch-error";
|
|
5
|
+
import { routerHandler } from "../../../src/router";
|
|
6
|
+
import schema from "../../../generate/api-schema";
|
|
7
|
+
import { failCode } from "../../../src/fail-code";
|
|
8
|
+
import process, { exit } from "node:process";
|
|
9
|
+
import { TSON } from "@southern-aurora/tson";
|
|
10
|
+
import { createUlid } from "../utils/create-ulid";
|
|
11
|
+
|
|
12
|
+
export type ExecuteHttpServerOptions = {
|
|
13
|
+
/**
|
|
14
|
+
* The execution ID generator
|
|
15
|
+
* If you have enabled this option, the executeId will be generated each time by calling this method. Otherwise, it will be generated using the built-in method.
|
|
16
|
+
*
|
|
17
|
+
* @param request
|
|
18
|
+
* @returns
|
|
19
|
+
*/
|
|
20
|
+
executeIdGenerator?: (request: Request) => string | Promise<string>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function defineHttpHandler(app: LoongbaoApp, options: ExecuteHttpServerOptions = {}) {
|
|
24
|
+
// If an unexpected error occurs, exit the process.
|
|
25
|
+
// For modern production environments such as Serverless, Kubernetes, or Docker Compose:
|
|
26
|
+
// The process will automatically restart after exiting.
|
|
27
|
+
// This helps prevent unexpected errors from contaminating the entire application and causing subsequent requests to fail intermittently.
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
29
|
+
process.on("uncaughtException", async (error) => {
|
|
30
|
+
const logger = useLogger("global");
|
|
31
|
+
logger.error("ErrorCaughtInUncaughtExceptionEvent:", error);
|
|
32
|
+
await loggerSubmitAll();
|
|
33
|
+
exit(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const fetch = async (request: LoongbaoHTTPRequest) => {
|
|
37
|
+
const fullurl = new URL(request.request.url, `http://${request.request.headers.get("host") ?? "localhost"}`);
|
|
38
|
+
const executeId = (options?.executeIdGenerator ? await options.executeIdGenerator(request.request) : createUlid()) as ExecuteId;
|
|
39
|
+
runtime.execute.executeIds.add(executeId);
|
|
40
|
+
const logger = useLogger(executeId);
|
|
41
|
+
const ip = (request.request.headers.get("x-forwarded-for") as string | undefined)?.split(",")[0] ?? "0.0.0.0";
|
|
42
|
+
const headers = request.request.headers;
|
|
43
|
+
|
|
44
|
+
loggerPushTags(executeId, {
|
|
45
|
+
from: "http-server",
|
|
46
|
+
fullUrl: fullurl.pathname,
|
|
47
|
+
ip,
|
|
48
|
+
method: request.request.method,
|
|
49
|
+
requestHeaders: request.request.headers.toJSON(),
|
|
50
|
+
timein: new Date().getTime()
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const response: LoongbaoHTTPResponse = {
|
|
54
|
+
body: "",
|
|
55
|
+
status: 200,
|
|
56
|
+
headers: {
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
"Access-Control-Allow-Methods": configFramework.corsAllowMethods ?? "*",
|
|
59
|
+
"Access-Control-Allow-Headers": configFramework.corsAllowHeaders ?? "*",
|
|
60
|
+
"Access-Control-Allow-Origin": configFramework.corsAllowOrigin ?? "*"
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
// Process OPTIONS pre inspection requests
|
|
66
|
+
if (request.request.method === "OPTIONS") {
|
|
67
|
+
await loggerSubmit(executeId);
|
|
68
|
+
runtime.execute.executeIds.delete(executeId);
|
|
69
|
+
|
|
70
|
+
return new Response("", {
|
|
71
|
+
headers: {
|
|
72
|
+
"Access-Control-Allow-Methods": configFramework.corsAllowMethods ?? "*",
|
|
73
|
+
"Access-Control-Allow-Headers": configFramework.corsAllowHeaders ?? "*",
|
|
74
|
+
"Access-Control-Allow-Origin": configFramework.corsAllowOrigin ?? "*"
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let path = fullurl.pathname.substring(1).split("/");
|
|
80
|
+
|
|
81
|
+
// Compatible with API gateway's ability to differentiate versions by path
|
|
82
|
+
// see: /src/config/ConfigProgram.ts in "ignorePathLevel"
|
|
83
|
+
if (configFramework.ignorePathLevel !== 0) path = path.slice(configFramework.ignorePathLevel);
|
|
84
|
+
|
|
85
|
+
let pathstr = path.join("/") as keyof (typeof schema)["apiMethodsSchema"];
|
|
86
|
+
|
|
87
|
+
// Special processing: do not run middleware when encountering 404 and return quickly
|
|
88
|
+
if (!(pathstr in schema.apiMethodsSchema)) {
|
|
89
|
+
const redirectPath = await routerHandler(pathstr, fullurl);
|
|
90
|
+
if (!redirectPath) {
|
|
91
|
+
const rawbody = await request.request.text();
|
|
92
|
+
loggerPushTags(executeId, {
|
|
93
|
+
body: rawbody || "no body"
|
|
94
|
+
});
|
|
95
|
+
response.body = `{"executeId":"${executeId}","success":false,"fail":{"code":"not-found","message":${JSON.stringify(failCode["not-found"]())}}}`;
|
|
96
|
+
|
|
97
|
+
loggerPushTags(executeId, {
|
|
98
|
+
status: response.status,
|
|
99
|
+
responseHeaders: response.headers,
|
|
100
|
+
timeout: new Date().getTime()
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await loggerSubmit(executeId);
|
|
104
|
+
runtime.execute.executeIds.delete(executeId);
|
|
105
|
+
|
|
106
|
+
return new Response(response.body, response);
|
|
107
|
+
}
|
|
108
|
+
pathstr = redirectPath as typeof pathstr;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
loggerPushTags(executeId, {
|
|
112
|
+
path: pathstr
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const detail = {
|
|
116
|
+
path: pathstr,
|
|
117
|
+
ip,
|
|
118
|
+
executeId,
|
|
119
|
+
fullurl,
|
|
120
|
+
request: request.request,
|
|
121
|
+
response
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// execute api
|
|
125
|
+
// after request middleware
|
|
126
|
+
await MiddlewareEvent.handle("afterHTTPRequest", [headers, detail]);
|
|
127
|
+
|
|
128
|
+
const rawbody = await request.request.text();
|
|
129
|
+
loggerPushTags(executeId, {
|
|
130
|
+
body: rawbody || "no body"
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
134
|
+
let params: any;
|
|
135
|
+
if (rawbody === "") {
|
|
136
|
+
params = undefined;
|
|
137
|
+
} else {
|
|
138
|
+
try {
|
|
139
|
+
params = JSON.parse(rawbody);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const logger = useLogger(executeId);
|
|
142
|
+
logger.log("TIP: body is not json, the content is not empty, but the content is not in a valid JSON format. The original content value can be retrieved via request.request.text()");
|
|
143
|
+
params = undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
loggerPushTags(executeId, {
|
|
148
|
+
params
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
152
|
+
const result = await app._executeCoreToJson(pathstr, params, headers, {
|
|
153
|
+
executeId,
|
|
154
|
+
logger,
|
|
155
|
+
detail
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// @ts-ignore
|
|
159
|
+
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression, @typescript-eslint/no-explicit-any
|
|
160
|
+
response.body = `${response.body ?? ""}${result}`;
|
|
161
|
+
|
|
162
|
+
// before response middleware
|
|
163
|
+
const middlewareResponse = {
|
|
164
|
+
value: response.body
|
|
165
|
+
};
|
|
166
|
+
await MiddlewareEvent.handle("beforeHTTPResponse", [middlewareResponse, detail]);
|
|
167
|
+
|
|
168
|
+
response.body = middlewareResponse.value;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const result = hanldeCatchError(error, executeId);
|
|
171
|
+
response.body = TSON.stringify(result);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
loggerPushTags(executeId, {
|
|
175
|
+
status: response.status,
|
|
176
|
+
responseHeaders: response.headers,
|
|
177
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
178
|
+
body: response.body || "",
|
|
179
|
+
timeout: new Date().getTime()
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await loggerSubmit(executeId);
|
|
183
|
+
runtime.execute.executeIds.delete(executeId);
|
|
184
|
+
|
|
185
|
+
return new Response(response.body, response);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return fetch;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type LoongbaoHTTPRequest = {
|
|
192
|
+
request: Request;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export type LoongbaoHTTPResponse = Mixin<
|
|
196
|
+
ResponseInit,
|
|
197
|
+
{
|
|
198
|
+
//
|
|
199
|
+
body: string;
|
|
200
|
+
status: number;
|
|
201
|
+
headers: Record<string, string>;
|
|
202
|
+
}
|
|
203
|
+
>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const defineUse = <CreatorFn extends () => Promise<unknown> | unknown>(creatorFn: CreatorFn): (() => Promise<Awaited<ReturnType<CreatorFn>>>) => {
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3
|
+
let use: any | undefined;
|
|
4
|
+
|
|
5
|
+
const getUse = async () => {
|
|
6
|
+
if (use === undefined) {
|
|
7
|
+
use = await creatorFn();
|
|
8
|
+
}
|
|
9
|
+
return use;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
return getUse;
|
|
13
|
+
};
|
package/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// types
|
|
2
|
+
export * from "./types";
|
|
3
|
+
|
|
4
|
+
// utils
|
|
5
|
+
export * from "./utils/tson";
|
|
6
|
+
export * from "./utils/create-ulid";
|
|
7
|
+
export * from "./utils/env-to-string";
|
|
8
|
+
export * from "./utils/env-to-number";
|
|
9
|
+
export * from "./utils/env-to-boolean";
|
|
10
|
+
export * from "./utils/create-template";
|
|
11
|
+
|
|
12
|
+
// defines
|
|
13
|
+
export * from "./defines/define-use";
|
|
14
|
+
export * from "./defines/define-api";
|
|
15
|
+
export * from "./defines/define-api-test";
|
|
16
|
+
export * from "./defines/define-middleware";
|
|
17
|
+
|
|
18
|
+
// kernel
|
|
19
|
+
export * from "./kernel/runtime";
|
|
20
|
+
export * from "./kernel/logger";
|
|
21
|
+
export * from "./kernel/fail";
|
|
22
|
+
export * from "./kernel/meta";
|
|
23
|
+
export * from "./kernel/config";
|
|
24
|
+
export * from "./kernel/context";
|
|
25
|
+
export * from "./kernel/validate";
|
|
26
|
+
export * from "./kernel/middleware";
|
|
27
|
+
|
|
28
|
+
// handler
|
|
29
|
+
export * from "./defines/define-http-handler";
|
|
30
|
+
export * from "./defines/define-api-test-handler";
|
|
31
|
+
|
|
32
|
+
// loongbao
|
|
33
|
+
export * from "./kernel/loongbao";
|
package/kernel/config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { cwd, env } from "node:process";
|
|
2
|
+
import { envToBoolean, envToNumber, envToString } from "..";
|
|
3
|
+
|
|
4
|
+
export const configFramework = {
|
|
5
|
+
cwd: cwd(),
|
|
6
|
+
port: envToNumber(env.PORT, 9000),
|
|
7
|
+
loongbaoRunMode: envToString(env.LOONGBAO_RUN_MODE, "DEFAULT"),
|
|
8
|
+
loongbaoTest: envToString(env.LOONGBAO_TEST, ""),
|
|
9
|
+
debug: envToBoolean(env.DEBUG, false),
|
|
10
|
+
redisUrl: envToString(env.REDIS_URL, "redis://:123456@your-not-redis-url:6379"),
|
|
11
|
+
ignorePathLevel: envToNumber(env.IGNORE_PATH_LEVEL, 0),
|
|
12
|
+
corsAllowMethods: envToString(env.CORS_ALLOW_METHODS, "*"),
|
|
13
|
+
corsAllowHeaders: envToString(env.CORS_ALLOW_HEADERS, "*"),
|
|
14
|
+
corsAllowOrigin: envToString(env.CORS_ALLOW_ORIGIN, "*")
|
|
15
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { URL } from "node:url";
|
|
2
|
+
import type { ExecuteId, Logger, LoongbaoHTTPResponse } from "..";
|
|
3
|
+
|
|
4
|
+
export type FrameworkContext = {
|
|
5
|
+
path: string;
|
|
6
|
+
executeId: ExecuteId;
|
|
7
|
+
headers: Headers;
|
|
8
|
+
logger: Logger;
|
|
9
|
+
/**
|
|
10
|
+
* Additional information about the request
|
|
11
|
+
* These are usually only fully implemented when called by an HTTP server
|
|
12
|
+
* During testing or when calling between microservices, some or all of the values may be undefined
|
|
13
|
+
*/
|
|
14
|
+
detail: Partial<FrameworkHTTPDetail>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type FrameworkHTTPDetail = {
|
|
18
|
+
path: string;
|
|
19
|
+
executeId: ExecuteId;
|
|
20
|
+
fullurl: URL;
|
|
21
|
+
ip: string;
|
|
22
|
+
request: Request;
|
|
23
|
+
response: LoongbaoHTTPResponse;
|
|
24
|
+
};
|
package/kernel/fail.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { failCode } from "../../../src/fail-code";
|
|
2
|
+
|
|
3
|
+
export function reject<Code extends keyof typeof failCode, FailData extends (typeof failCode)[Code]>(code: Code, data: Parameters<FailData>[0]) {
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
5
|
+
const message = failCode[code]?.(data as any) ?? "";
|
|
6
|
+
const error = {
|
|
7
|
+
name: "LoongbaoReject",
|
|
8
|
+
code,
|
|
9
|
+
message,
|
|
10
|
+
data,
|
|
11
|
+
stack: ""
|
|
12
|
+
};
|
|
13
|
+
Error.captureStackTrace(error);
|
|
14
|
+
error.stack = error.stack.replace(/\n.*\n/, "\n");
|
|
15
|
+
|
|
16
|
+
return error;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type LoongbaoReject = ReturnType<typeof reject>;
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
export type LoongbaoFailCode = Record<string, (arg: any) => string>;
|
package/kernel/logger.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
3
|
+
import { runtime, type ExecuteId } from "..";
|
|
4
|
+
import { loggerOptions } from "../../../src/logger";
|
|
5
|
+
|
|
6
|
+
export type LoggerItem = {
|
|
7
|
+
executeId: ExecuteId;
|
|
8
|
+
loggerLevel: LoggerLevel;
|
|
9
|
+
description: string;
|
|
10
|
+
params: Array<unknown>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type LoggerTags = Record<string, any>;
|
|
14
|
+
|
|
15
|
+
export type LoggerOptions = {
|
|
16
|
+
onInsert: (options: LoggerItem) => boolean;
|
|
17
|
+
onSubmit: (tags: LoggerTags, logs: Array<LoggerItem>) => Promise<void> | void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type Logger = {
|
|
21
|
+
debug: (description: string, ...params: Array<unknown>) => void;
|
|
22
|
+
log: (description: string, ...params: Array<unknown>) => void;
|
|
23
|
+
warn: (description: string, ...params: Array<unknown>) => void;
|
|
24
|
+
error: (description: string, ...params: Array<unknown>) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const loggerController = (() => {
|
|
28
|
+
const logs = new Map<ExecuteId, { __LOG_DEATIL__: Array<LoggerItem>; [key: string]: any }>();
|
|
29
|
+
|
|
30
|
+
const loggerPushTags = (executeId: ExecuteId, tags: Record<string, any>) => {
|
|
31
|
+
if (!logs.has(executeId)) logs.set(executeId, { __LOG_DEATIL__: [] });
|
|
32
|
+
const logItem = logs.get(executeId);
|
|
33
|
+
for (const key in tags) {
|
|
34
|
+
logItem![key] = tags[key];
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const loggerSubmit = async (executeId: ExecuteId) => {
|
|
39
|
+
if (!logs.has(executeId)) return;
|
|
40
|
+
if (executeId === "global") return;
|
|
41
|
+
const loggerSubmitOptions: Record<string, string> = {
|
|
42
|
+
executeId
|
|
43
|
+
};
|
|
44
|
+
const log = logs.get(executeId)!;
|
|
45
|
+
for (const key in log) {
|
|
46
|
+
if (key === "__LOG_DEATIL__") continue;
|
|
47
|
+
loggerSubmitOptions[key] = log[key];
|
|
48
|
+
}
|
|
49
|
+
logs.delete(executeId);
|
|
50
|
+
loggerOptions.onSubmit(loggerSubmitOptions, log.__LOG_DEATIL__);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const loggerSubmitAll = async () => {
|
|
54
|
+
for (const executeId of logs.keys()) {
|
|
55
|
+
await loggerSubmit(executeId);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const insertItem = (executeId: ExecuteId, level: LoggerLevel, description: string, params: Array<unknown>): void => {
|
|
60
|
+
let executeIds: Array<string> = [];
|
|
61
|
+
if (executeId === "global") {
|
|
62
|
+
executeIds = Array.from(new Set([...Array.from(runtime.execute.executeIds), ...Array.from(runtime.execute.executeIds)]));
|
|
63
|
+
} else {
|
|
64
|
+
executeIds = [executeId];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const executeId of executeIds) {
|
|
68
|
+
if (!logs.has(executeId as ExecuteId)) logs.set(executeId as ExecuteId, { __LOG_DEATIL__: [] });
|
|
69
|
+
const loggerItem = {
|
|
70
|
+
executeId: executeId as ExecuteId,
|
|
71
|
+
loggerLevel: level,
|
|
72
|
+
description,
|
|
73
|
+
params
|
|
74
|
+
} satisfies LoggerItem;
|
|
75
|
+
if (!loggerOptions.onInsert(loggerItem)) return;
|
|
76
|
+
logs.get(executeId as ExecuteId)!.__LOG_DEATIL__.push(loggerItem);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const useLogger = (executeId: ExecuteId) => {
|
|
81
|
+
return {
|
|
82
|
+
debug(description: string, ...params: Array<unknown>) {
|
|
83
|
+
insertItem(executeId, "debug", description, params);
|
|
84
|
+
},
|
|
85
|
+
log(description: string, ...params: Array<unknown>) {
|
|
86
|
+
insertItem(executeId, "log", description, params);
|
|
87
|
+
},
|
|
88
|
+
warn(description: string, ...params: Array<unknown>) {
|
|
89
|
+
insertItem(executeId, "warn", description, params);
|
|
90
|
+
},
|
|
91
|
+
error(description: string, ...params: Array<unknown>) {
|
|
92
|
+
insertItem(executeId, "error", description, params);
|
|
93
|
+
}
|
|
94
|
+
} satisfies Logger;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
loggerPushTags,
|
|
99
|
+
loggerSubmit,
|
|
100
|
+
loggerSubmitAll,
|
|
101
|
+
useLogger
|
|
102
|
+
};
|
|
103
|
+
})();
|
|
104
|
+
|
|
105
|
+
export const useLogger = loggerController.useLogger;
|
|
106
|
+
|
|
107
|
+
export const loggerPushTags = loggerController.loggerPushTags;
|
|
108
|
+
|
|
109
|
+
export const loggerSubmit = loggerController.loggerSubmit;
|
|
110
|
+
|
|
111
|
+
export const loggerSubmitAll = loggerController.loggerSubmitAll;
|
|
112
|
+
|
|
113
|
+
export type LoggerLevel = "debug" | "log" | "warn" | "error";
|