@hazejs/cli 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +26 -2
- package/bin/haze.mjs +2 -0
- package/dist/build-1rAV1kEg.mjs +62 -0
- package/dist/dev-B4V8ouF6.mjs +59 -0
- package/dist/index.mjs +826 -0
- package/dist/key-CmUJM4SG.mjs +51 -0
- package/dist/preview-B6OYX5qM.mjs +36 -0
- package/dist/routes-DF0y4tVg.mjs +59 -0
- package/dist/start-DYW9Ar9y.mjs +30 -0
- package/dist/stub-publish-BvtOsp9D.mjs +35 -0
- package/dist/test-CUMzRaAD.mjs +39 -0
- package/dist/typegen-BljZvG5y.mjs +34 -0
- package/package.json +51 -3
- package/stubs/config.stub +6 -0
- package/stubs/controller.resource.stub +35 -0
- package/stubs/controller.stub +6 -0
- package/stubs/event.stub +6 -0
- package/stubs/interceptor.stub +9 -0
- package/stubs/job.stub +25 -0
- package/stubs/listener.stub +12 -0
- package/stubs/middleware.stub +13 -0
- package/stubs/model.mongodb.stub +17 -0
- package/stubs/model.stub +10 -0
- package/stubs/policy.stub +21 -0
- package/stubs/provider.stub +11 -0
- package/stubs/repository.stub +6 -0
- package/stubs/route.stub +7 -0
- package/stubs/schema.mysql.stub +10 -0
- package/stubs/schema.postgres.stub +10 -0
- package/stubs/schema.sqlite.stub +10 -0
- package/stubs/seeder.stub +6 -0
- package/stubs/service.stub +9 -0
- package/stubs/test.stub +12 -0
- package/stubs/validate.stub +10 -0
- package/stubs/validate.zod.stub +10 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { a as resolveRoot, o as rootArg } from "./index.mjs";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join, relative } from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { consola } from "consola";
|
|
7
|
+
import { Buffer } from "node:buffer";
|
|
8
|
+
// src/commands/key.ts
|
|
9
|
+
/** 256 位随机密钥(base64url),与 @hazejs/core 的 generateKey() 相同 */
|
|
10
|
+
function generateAppKey() {
|
|
11
|
+
return Buffer.from(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32))).toString("base64url");
|
|
12
|
+
}
|
|
13
|
+
var key_default = defineCommand({
|
|
14
|
+
meta: {
|
|
15
|
+
name: "key:generate",
|
|
16
|
+
description: "生成 APP_KEY(签名 Cookie、加密 Session、JWT 默认密钥)并写入 .env"
|
|
17
|
+
},
|
|
18
|
+
args: {
|
|
19
|
+
...rootArg,
|
|
20
|
+
show: {
|
|
21
|
+
type: "boolean",
|
|
22
|
+
description: "只输出,不写入文件",
|
|
23
|
+
default: false
|
|
24
|
+
},
|
|
25
|
+
force: {
|
|
26
|
+
type: "boolean",
|
|
27
|
+
description: "覆盖已有的 APP_KEY(已签发的 Cookie / Session / 令牌会失效)",
|
|
28
|
+
default: false
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
run({ args }) {
|
|
32
|
+
const key = generateAppKey();
|
|
33
|
+
if (args.show) {
|
|
34
|
+
console.log(key);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const root = resolveRoot(args.root);
|
|
38
|
+
const file = join(root, ".env");
|
|
39
|
+
const source = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
40
|
+
const line = /^APP_KEY=(.*)$/m.exec(source);
|
|
41
|
+
if (line?.[1] && !args.force) {
|
|
42
|
+
consola.warn(`APP_KEY already exists in ${relative(process.cwd(), file) || ".env"}; use --force to replace it`);
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const next = line ? source.replace(/^APP_KEY=.*$/m, `APP_KEY=${key}`) : `${source}${source && !source.endsWith("\n") ? "\n" : ""}APP_KEY=${key}\n`;
|
|
47
|
+
writeFileSync(file, next);
|
|
48
|
+
consola.success(`APP_KEY written to ${relative(process.cwd(), file) || ".env"}`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
export { key_default as default };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { a as resolveRoot, i as parsePort, o as rootArg, s as serverArgs } from "./index.mjs";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { previewServer } from "@hazejs/vite";
|
|
5
|
+
// src/commands/preview.ts
|
|
6
|
+
var preview_default = defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "preview",
|
|
9
|
+
description: "按构建时的部署预设本地预览产物(需先 build)"
|
|
10
|
+
},
|
|
11
|
+
args: {
|
|
12
|
+
...rootArg,
|
|
13
|
+
...serverArgs,
|
|
14
|
+
open: {
|
|
15
|
+
type: "boolean",
|
|
16
|
+
description: "启动后打开浏览器",
|
|
17
|
+
default: false
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
async run({ args }) {
|
|
21
|
+
const server = await previewServer({
|
|
22
|
+
root: resolveRoot(args.root),
|
|
23
|
+
port: parsePort(args.port),
|
|
24
|
+
host: args.host,
|
|
25
|
+
open: args.open
|
|
26
|
+
});
|
|
27
|
+
server.printUrls();
|
|
28
|
+
const close = async () => {
|
|
29
|
+
await server.close();
|
|
30
|
+
process.exit(0);
|
|
31
|
+
};
|
|
32
|
+
process.once("SIGINT", close);
|
|
33
|
+
process.once("SIGTERM", close);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
export { preview_default as default };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { a as resolveRoot, o as rootArg } from "./index.mjs";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import { createDevServer } from "@hazejs/vite";
|
|
4
|
+
import { consola } from "consola";
|
|
5
|
+
import { colors } from "consola/utils";
|
|
6
|
+
// src/commands/routes.ts
|
|
7
|
+
var routes_default = defineCommand({
|
|
8
|
+
meta: {
|
|
9
|
+
name: "route:list",
|
|
10
|
+
description: "列出所有路由"
|
|
11
|
+
},
|
|
12
|
+
args: {
|
|
13
|
+
...rootArg,
|
|
14
|
+
json: {
|
|
15
|
+
type: "boolean",
|
|
16
|
+
description: "以 JSON 输出",
|
|
17
|
+
default: false
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
async run({ args }) {
|
|
21
|
+
const server = await createDevServer({
|
|
22
|
+
root: resolveRoot(args.root),
|
|
23
|
+
listen: false
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
if (!server.app) throw new Error("Application failed to start");
|
|
27
|
+
const routes = server.app.router.list();
|
|
28
|
+
if (args.json) {
|
|
29
|
+
consola.log(JSON.stringify(routes, null, 2));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const multiApp = routes.some((route) => route.app);
|
|
33
|
+
const rows = [[
|
|
34
|
+
...multiApp ? ["APP", "DOMAIN"] : [],
|
|
35
|
+
"METHOD",
|
|
36
|
+
"PATH",
|
|
37
|
+
"NAME",
|
|
38
|
+
"HANDLER",
|
|
39
|
+
"MIDDLEWARE"
|
|
40
|
+
], ...routes.map((route) => [
|
|
41
|
+
...multiApp ? [route.app ?? "-", route.domain ?? ""] : [],
|
|
42
|
+
route.method,
|
|
43
|
+
route.path,
|
|
44
|
+
route.name ?? "",
|
|
45
|
+
route.handler,
|
|
46
|
+
route.middleware.join(", ")
|
|
47
|
+
])];
|
|
48
|
+
const widths = rows[0].map((_, i) => Math.max(...rows.map((row) => row[i].length)));
|
|
49
|
+
rows.forEach((row, index) => {
|
|
50
|
+
const line = row.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
|
|
51
|
+
consola.log(index === 0 ? colors.dim(line) : line);
|
|
52
|
+
});
|
|
53
|
+
consola.log(colors.dim(`\n${routes.length} route(s)`));
|
|
54
|
+
} finally {
|
|
55
|
+
await server.close();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
export { routes_default as default };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { a as resolveRoot, o as rootArg, s as serverArgs } from "./index.mjs";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join, relative } from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { loadHazeConfig } from "@hazejs/vite";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
// src/commands/start.ts
|
|
9
|
+
var start_default = defineCommand({
|
|
10
|
+
meta: {
|
|
11
|
+
name: "start",
|
|
12
|
+
description: "运行生产环境产物(需先 build)"
|
|
13
|
+
},
|
|
14
|
+
args: {
|
|
15
|
+
...rootArg,
|
|
16
|
+
...serverArgs
|
|
17
|
+
},
|
|
18
|
+
async run({ args }) {
|
|
19
|
+
const root = resolveRoot(args.root);
|
|
20
|
+
const { outDir } = await loadHazeConfig(root, "production");
|
|
21
|
+
const entry = [join(outDir, "index.mjs"), join(outDir, "server", "index.mjs")].find((file) => existsSync(file));
|
|
22
|
+
if (!entry) throw new Error(`No build output found in ${relative(process.cwd(), outDir) || "."}. Run \`haze build\` first.`);
|
|
23
|
+
process.env.NODE_ENV = "production";
|
|
24
|
+
if (args.port) process.env.PORT = args.port;
|
|
25
|
+
if (args.host) process.env.HOST = args.host;
|
|
26
|
+
process.chdir(root);
|
|
27
|
+
await import(pathToFileURL(entry).href);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
export { start_default as default };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { a as resolveRoot, o as rootArg, t as STUBS_DIR } from "./index.mjs";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
4
|
+
import { join, relative } from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { consola } from "consola";
|
|
7
|
+
import { copyFile, mkdir } from "node:fs/promises";
|
|
8
|
+
// src/commands/stub-publish.ts
|
|
9
|
+
var stub_publish_default = defineCommand({
|
|
10
|
+
meta: {
|
|
11
|
+
name: "stub:publish",
|
|
12
|
+
description: "把内置 stub 复制到项目 stubs/ 目录以便定制 make:* 生成的代码"
|
|
13
|
+
},
|
|
14
|
+
args: {
|
|
15
|
+
...rootArg,
|
|
16
|
+
force: {
|
|
17
|
+
type: "boolean",
|
|
18
|
+
description: "覆盖已存在的 stub",
|
|
19
|
+
default: false
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
async run({ args }) {
|
|
23
|
+
const target = join(resolveRoot(args.root), "stubs");
|
|
24
|
+
await mkdir(target, { recursive: true });
|
|
25
|
+
let copied = 0;
|
|
26
|
+
for (const file of readdirSync(STUBS_DIR)) {
|
|
27
|
+
const destination = join(target, file);
|
|
28
|
+
if (existsSync(destination) && !args.force) continue;
|
|
29
|
+
await copyFile(join(STUBS_DIR, file), destination);
|
|
30
|
+
copied++;
|
|
31
|
+
}
|
|
32
|
+
consola.success(`Published ${copied} stub(s) to ${relative(process.cwd(), target) || "."}/`);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
export { stub_publish_default as default };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { a as resolveRoot, n as binOf } from "./index.mjs";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
// src/commands/test.ts
|
|
6
|
+
var test_default = defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "test",
|
|
9
|
+
description: "运行 Vitest(其余参数原样透传)"
|
|
10
|
+
},
|
|
11
|
+
args: { root: {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "项目根目录",
|
|
14
|
+
default: "."
|
|
15
|
+
} },
|
|
16
|
+
async run({ args, rawArgs }) {
|
|
17
|
+
const root = resolveRoot(args.root);
|
|
18
|
+
const bin = binOf(root, "vitest");
|
|
19
|
+
const passthrough = stripRootArg(rawArgs);
|
|
20
|
+
const vitestArgs = passthrough.length ? passthrough : ["run"];
|
|
21
|
+
const child = spawn(process.execPath, [bin, ...vitestArgs], {
|
|
22
|
+
cwd: root,
|
|
23
|
+
stdio: "inherit",
|
|
24
|
+
env: {
|
|
25
|
+
...process.env,
|
|
26
|
+
NODE_ENV: "test"
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
const code = await new Promise((resolve) => child.on("close", (code) => resolve(code ?? 1)));
|
|
30
|
+
process.exitCode = code;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
function stripRootArg(rawArgs) {
|
|
34
|
+
const result = [];
|
|
35
|
+
for (let i = 0; i < rawArgs.length; i++) if (rawArgs[i] === "--root") i++;
|
|
36
|
+
else if (!rawArgs[i].startsWith("--root=")) result.push(rawArgs[i]);
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
export { test_default as default };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { a as resolveRoot, o as rootArg } from "./index.mjs";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import { relative } from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { createDevServer, loadHazeConfig, writeTypes } from "@hazejs/vite";
|
|
6
|
+
import { consola } from "consola";
|
|
7
|
+
// src/commands/typegen.ts
|
|
8
|
+
var typegen_default = defineCommand({
|
|
9
|
+
meta: {
|
|
10
|
+
name: "typegen",
|
|
11
|
+
description: "生成配置键与路由名类型(.haze/types/haze.d.ts)"
|
|
12
|
+
},
|
|
13
|
+
args: rootArg,
|
|
14
|
+
async run({ args }) {
|
|
15
|
+
const root = resolveRoot(args.root);
|
|
16
|
+
const { srcDir } = await loadHazeConfig(root);
|
|
17
|
+
const server = await createDevServer({
|
|
18
|
+
root,
|
|
19
|
+
listen: false
|
|
20
|
+
});
|
|
21
|
+
try {
|
|
22
|
+
if (!server.app) throw new Error("Application failed to start");
|
|
23
|
+
const result = await writeTypes({
|
|
24
|
+
root,
|
|
25
|
+
srcDir,
|
|
26
|
+
routes: server.app.router.list()
|
|
27
|
+
});
|
|
28
|
+
consola.success(`Types written to ${relative(process.cwd(), result.file)} (${result.configs.length} config file(s), ${result.routes.length} named route(s))`);
|
|
29
|
+
} finally {
|
|
30
|
+
await server.close();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
export { typegen_default as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hazejs/cli",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.1",
|
|
5
|
+
"description": "The haze command line: dev / build / preview / make:* / db:* / queue:* / schedule:*",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/penna-org/haze/tree/main/packages/cli#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/penna-org/haze.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/penna-org/haze/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"hazejs",
|
|
18
|
+
"haze",
|
|
19
|
+
"nodejs",
|
|
20
|
+
"typescript",
|
|
21
|
+
"cli",
|
|
22
|
+
"framework",
|
|
23
|
+
"generator",
|
|
24
|
+
"scaffold"
|
|
25
|
+
],
|
|
26
|
+
"bin": {
|
|
27
|
+
"haze": "./bin/haze.mjs"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"bin",
|
|
31
|
+
"dist",
|
|
32
|
+
"stubs"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22.12.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"vite": "^8.0.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"citty": "^0.2.2",
|
|
42
|
+
"consola": "^3.4.2",
|
|
43
|
+
"@hazejs/vite": "0.1.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"vite": "^8.3.5",
|
|
47
|
+
"@hazejs/orm": "0.1.1",
|
|
48
|
+
"@hazejs/core": "0.1.1"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsdown",
|
|
52
|
+
"dev": "tsdown --watch"
|
|
53
|
+
}
|
|
6
54
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { HttpContext } from '@hazejs/core'
|
|
2
|
+
import { NotFoundException } from '@hazejs/core'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* RESTful 资源控制器,在 route/*.ts 中注册:
|
|
6
|
+
* r.resource('/{{routePath}}', {{className}})
|
|
7
|
+
*/
|
|
8
|
+
export class {{className}} {
|
|
9
|
+
/** GET /{{routePath}} */
|
|
10
|
+
index() {
|
|
11
|
+
return []
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** GET /{{routePath}}/:id */
|
|
15
|
+
show(ctx: HttpContext) {
|
|
16
|
+
throw new NotFoundException(`{{name}} ${ctx.param('id')} 不存在`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** POST /{{routePath}} */
|
|
20
|
+
async store(ctx: HttpContext) {
|
|
21
|
+
const data = await ctx.body<Record<string, unknown>>()
|
|
22
|
+
return ctx.created(data)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** PUT | PATCH /{{routePath}}/:id */
|
|
26
|
+
async update(ctx: HttpContext) {
|
|
27
|
+
const data = await ctx.body<Record<string, unknown>>()
|
|
28
|
+
return { id: ctx.param('id'), ...data }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** DELETE /{{routePath}}/:id */
|
|
32
|
+
destroy(ctx: HttpContext) {
|
|
33
|
+
return ctx.noContent()
|
|
34
|
+
}
|
|
35
|
+
}
|
package/stubs/event.stub
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineInterceptor } from '@hazejs/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 出站拦截器:拿到最终的 Response(统一格式与异常渲染之后),
|
|
5
|
+
* 可修改响应头,或返回新的 Response 替换;app/interceptor 下按文件名顺序执行。
|
|
6
|
+
*/
|
|
7
|
+
export default defineInterceptor((response, ctx) => {
|
|
8
|
+
response.headers.set('x-response-time', `${Math.round(performance.now() - ctx.startedAt)}ms`)
|
|
9
|
+
})
|
package/stubs/job.stub
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineJob } from '@hazejs/core'
|
|
2
|
+
|
|
3
|
+
export interface {{name}}Payload {
|
|
4
|
+
id: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 队列任务(app/job 下自动注册,文件名即任务名):
|
|
9
|
+
* 投递 await {{name}}.dispatch({ id: 1 }, { delay: '10m' });消费 haze queue:work
|
|
10
|
+
*/
|
|
11
|
+
const {{name}} = defineJob({
|
|
12
|
+
// 最多执行次数与重试前的等待(秒 / '5m')
|
|
13
|
+
tries: 3,
|
|
14
|
+
backoff: [10, 60],
|
|
15
|
+
timeout: '1m',
|
|
16
|
+
async handle(payload: {{name}}Payload, job) {
|
|
17
|
+
job.logger.info(`{{name}} ${payload.id}`)
|
|
18
|
+
},
|
|
19
|
+
// 全部重试失败后调用
|
|
20
|
+
async failed(payload, error, job) {
|
|
21
|
+
job.logger.error(`{{name}} ${payload.id} failed`, error)
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export default {{name}}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineEvent, defineListener } from '@hazejs/core'
|
|
2
|
+
|
|
3
|
+
// 替换为要监听的事件:import UserRegistered from '~/app/event/UserRegistered'
|
|
4
|
+
const SomeEvent = defineEvent<{ id: number }>()
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 监听器(app/listener 下自动注册,按文件名顺序执行)。
|
|
8
|
+
* 返回 false 时不再执行后续监听器;耗时操作加 { queue: true }(或 { queue: { tries: 3 } })排队异步执行
|
|
9
|
+
*/
|
|
10
|
+
export default defineListener(SomeEvent, async (payload, ctx) => {
|
|
11
|
+
ctx.logger.info(`{{name}} handled ${ctx.event} ${payload.id}`)
|
|
12
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HttpContext, Middleware, Next } from '@hazejs/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 在 config/middleware.ts 中注册:alias: { {{camelName}}: {{className}} }
|
|
5
|
+
* 路由中使用:.middleware('{{camelName}}')
|
|
6
|
+
*/
|
|
7
|
+
export class {{className}} implements Middleware {
|
|
8
|
+
async handle(ctx: HttpContext, next: Next) {
|
|
9
|
+
ctx.logger.debug(`{{className}}: ${ctx.method} ${ctx.path}`)
|
|
10
|
+
const response = await next()
|
|
11
|
+
return response
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MongoModel } from '@hazejs/orm/mongo'
|
|
2
|
+
|
|
3
|
+
/** {{name}} 文档结构:MongoDB 没有固定表结构,在这里声明字段类型 */
|
|
4
|
+
export interface {{className}}Doc {
|
|
5
|
+
name: string
|
|
6
|
+
createdAt?: Date
|
|
7
|
+
updatedAt?: Date
|
|
8
|
+
deletedAt?: Date | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* {{name}} 模型(集合 {{tableName}}),API 与 SQL 模型一致:
|
|
13
|
+
* {{className}}.findOrFail(id)、{{className}}.where('name', 'Ada').paginate(1, 20)、{{className}}.create(data)
|
|
14
|
+
*/
|
|
15
|
+
export class {{className}} extends MongoModel.define<{{className}}Doc>('{{tableName}}') {
|
|
16
|
+
static override softDelete = {{softDelete}}
|
|
17
|
+
}
|
package/stubs/model.stub
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Model } from '@hazejs/orm'
|
|
2
|
+
import { {{tableVar}} } from '~/database/schema/{{tableName}}'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* {{name}} 模型:字段与类型来自 database/schema/{{tableName}}.ts,
|
|
6
|
+
* 修改表结构后运行 haze db:generate 生成迁移,再 haze db:migrate。
|
|
7
|
+
*/
|
|
8
|
+
export class {{className}} extends Model.define({{tableVar}}) {
|
|
9
|
+
static override softDelete = {{softDelete}}
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { allowGuest, definePolicy, deny } from '@hazejs/core'
|
|
2
|
+
import { {{name}} } from '~/app/model/{{name}}'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* {{name}} 的授权策略(app/policy 下自动注册):
|
|
6
|
+
* - 控制器:await ctx.authorize('update', {{camelName}}),或 if (await ctx.can('delete', {{camelName}}))
|
|
7
|
+
* - 路由:.middleware('can:update,{{name}},id')(按路由参数 id 加载并检查,实例存入 ctx.get('{{camelName}}'))
|
|
8
|
+
* 能力函数返回 true / false / deny('原因');未登录用户默认拒绝,用 allowGuest() 包装后才会调用。
|
|
9
|
+
*/
|
|
10
|
+
export default definePolicy({{name}}, {
|
|
11
|
+
// 先于所有能力执行:返回 true / false 直接决定,undefined 继续判断(如超级管理员)
|
|
12
|
+
// before: user => user?.role === 'admin' || undefined,
|
|
13
|
+
|
|
14
|
+
viewAny: allowGuest(() => true),
|
|
15
|
+
view: allowGuest(() => true),
|
|
16
|
+
create: () => true,
|
|
17
|
+
// 示例:只有作者可以修改
|
|
18
|
+
// update: (user, {{camelName}}) => {{camelName}}.userId === user.id || deny('只能修改自己的{{name}}'),
|
|
19
|
+
update: () => deny('尚未配置 {{name}} 的修改权限'),
|
|
20
|
+
delete: () => deny('尚未配置 {{name}} 的删除权限'),
|
|
21
|
+
})
|
package/stubs/route.stub
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { mysqlTable, serial, timestamp, varchar } from 'drizzle-orm/mysql-core'
|
|
2
|
+
|
|
3
|
+
// 表结构定义(drizzle):此文件只依赖 drizzle-orm,不要使用 ~ 别名(drizzle-kit 需要直接读取)
|
|
4
|
+
export const {{tableVar}} = mysqlTable('{{tableName}}', {
|
|
5
|
+
id: serial('id').primaryKey(),
|
|
6
|
+
name: varchar('name', { length: 255 }).notNull(),
|
|
7
|
+
createdAt: timestamp('created_at'),
|
|
8
|
+
updatedAt: timestamp('updated_at'),
|
|
9
|
+
deletedAt: timestamp('deleted_at'),
|
|
10
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core'
|
|
2
|
+
|
|
3
|
+
// 表结构定义(drizzle):此文件只依赖 drizzle-orm,不要使用 ~ 别名(drizzle-kit 需要直接读取)
|
|
4
|
+
export const {{tableVar}} = pgTable('{{tableName}}', {
|
|
5
|
+
id: serial('id').primaryKey(),
|
|
6
|
+
name: text('name').notNull(),
|
|
7
|
+
createdAt: timestamp('created_at'),
|
|
8
|
+
updatedAt: timestamp('updated_at'),
|
|
9
|
+
deletedAt: timestamp('deleted_at'),
|
|
10
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
|
2
|
+
|
|
3
|
+
// 表结构定义(drizzle):此文件只依赖 drizzle-orm,不要使用 ~ 别名(drizzle-kit 需要直接读取)
|
|
4
|
+
export const {{tableVar}} = sqliteTable('{{tableName}}', {
|
|
5
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
6
|
+
name: text('name').notNull(),
|
|
7
|
+
createdAt: integer('created_at', { mode: 'timestamp' }),
|
|
8
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }),
|
|
9
|
+
deletedAt: integer('deleted_at', { mode: 'timestamp' }),
|
|
10
|
+
})
|
package/stubs/test.stub
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useTestApp } from '@hazejs/testing'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
// 每个用例一个全新的应用实例
|
|
5
|
+
const t = useTestApp()
|
|
6
|
+
|
|
7
|
+
describe('{{kebabName}}', () => {
|
|
8
|
+
it('responds', async () => {
|
|
9
|
+
const res = await t.get('/')
|
|
10
|
+
expect(res.status).toBeLessThan(500)
|
|
11
|
+
})
|
|
12
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as v from 'valibot'
|
|
2
|
+
|
|
3
|
+
export const Create{{name}}Schema = v.object({
|
|
4
|
+
name: v.pipe(v.string('name 必填'), v.trim(), v.minLength(1, 'name 不能为空')),
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
export const Update{{name}}Schema = v.partial(Create{{name}}Schema)
|
|
8
|
+
|
|
9
|
+
export type Create{{name}}Input = v.InferOutput<typeof Create{{name}}Schema>
|
|
10
|
+
export type Update{{name}}Input = v.InferOutput<typeof Update{{name}}Schema>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const Create{{name}}Schema = z.object({
|
|
4
|
+
name: z.string({ error: 'name 必填' }).trim().min(1, 'name 不能为空'),
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
export const Update{{name}}Schema = Create{{name}}Schema.partial()
|
|
8
|
+
|
|
9
|
+
export type Create{{name}}Input = z.infer<typeof Create{{name}}Schema>
|
|
10
|
+
export type Update{{name}}Input = z.infer<typeof Update{{name}}Schema>
|