@goplusvn/core 0.1.59 → 0.1.60
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/PLATFORM.md +18 -0
- package/bin/goerp-init.mjs +141 -0
- package/package.json +4 -4
- package/src/cron/db-cron-manager.ts +3 -3
- package/src/cron/index.ts +2 -2
- package/src/{infrastructure/cron/cron-manager.ts → cron/simple-cron-job.ts} +1 -1
- package/src/infrastructure/__tests__/architecture-verification.spec.ts +13 -97
- package/src/infrastructure/index.ts +4 -7
- package/src/ui/management/index.ts +3 -2
- package/templates/starter-app/.dockerignore +41 -0
- package/templates/starter-app/.env.example +25 -0
- package/templates/starter-app/AGENTS.md +52 -0
- package/templates/starter-app/Dockerfile +74 -0
- package/templates/starter-app/README.md +141 -0
- package/templates/starter-app/gitignore +9 -0
- package/templates/starter-app/next.config.mjs +50 -0
- package/templates/starter-app/package.json +55 -0
- package/templates/starter-app/postcss.config.mjs +5 -0
- package/templates/starter-app/prisma/migrations/20260801000000_init/migration.sql +504 -0
- package/templates/starter-app/prisma/migrations/20260801091814_goerp_audit_logs_0001_init/migration.sql +33 -0
- package/templates/starter-app/prisma/migrations/20260801091815_goerp_background_tasks_0001_init/migration.sql +23 -0
- package/templates/starter-app/prisma/migrations/20260801091816_goerp_error_logs_0001_init/migration.sql +32 -0
- package/templates/starter-app/prisma/migrations/20260801091817_goerp_notifications_0001_init/migration.sql +59 -0
- package/templates/starter-app/prisma/migrations/20260801091818_goerp_system_jobs_0001_init/migration.sql +47 -0
- package/templates/starter-app/prisma/schema/auth.prisma +87 -0
- package/templates/starter-app/prisma/schema/domain.prisma +24 -0
- package/templates/starter-app/prisma/schema/goerp-audit-logs.prisma +23 -0
- package/templates/starter-app/prisma/schema/goerp-background-tasks.prisma +25 -0
- package/templates/starter-app/prisma/schema/goerp-error-logs.prisma +31 -0
- package/templates/starter-app/prisma/schema/goerp-notifications.prisma +49 -0
- package/templates/starter-app/prisma/schema/goerp-system-jobs.prisma +45 -0
- package/templates/starter-app/prisma/schema/organization.prisma +31 -0
- package/templates/starter-app/prisma/schema/rbac.prisma +100 -0
- package/templates/starter-app/prisma/schema/schema.prisma +8 -0
- package/templates/starter-app/prisma/schema/system.prisma +22 -0
- package/templates/starter-app/prisma/seed.ts +127 -0
- package/templates/starter-app/prisma.config.ts +20 -0
- package/templates/starter-app/public/.gitkeep +2 -0
- package/templates/starter-app/scripts/rbac-sync.ts +235 -0
- package/templates/starter-app/src/__tests__/architecture.test.ts +151 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/audit/page.tsx +24 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/error-logs/page.tsx +21 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/jobs/page.tsx +18 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/settings/page.tsx +22 -0
- package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +41 -0
- package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +13 -0
- package/templates/starter-app/src/app/[lang]/(main)/notifications/page.tsx +13 -0
- package/templates/starter-app/src/app/[lang]/(main)/page.tsx +130 -0
- package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +60 -0
- package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +41 -0
- package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +48 -0
- package/templates/starter-app/src/app/[lang]/(main)/tasks/page.tsx +13 -0
- package/templates/starter-app/src/app/[lang]/(plain)/layout.tsx +10 -0
- package/templates/starter-app/src/app/[lang]/(plain)/sign-in/page.tsx +39 -0
- package/templates/starter-app/src/app/[lang]/layout.tsx +21 -0
- package/templates/starter-app/src/app/api/admin/system/audit/route.ts +60 -0
- package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +39 -0
- package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +96 -0
- package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +17 -0
- package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +19 -0
- package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +20 -0
- package/templates/starter-app/src/app/api/admin/system/settings/route.ts +49 -0
- package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +28 -0
- package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +24 -0
- package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +23 -0
- package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +13 -0
- package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +13 -0
- package/templates/starter-app/src/app/api/crud/[entity]/route.ts +14 -0
- package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +23 -0
- package/templates/starter-app/src/app/api/error-logs/route.ts +124 -0
- package/templates/starter-app/src/app/api/files/[...key]/route.ts +25 -0
- package/templates/starter-app/src/app/api/notifications/read/route.ts +29 -0
- package/templates/starter-app/src/app/api/notifications/route.ts +26 -0
- package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +14 -0
- package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +11 -0
- package/templates/starter-app/src/app/api/roles/[id]/route.ts +14 -0
- package/templates/starter-app/src/app/api/roles/route.ts +18 -0
- package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +52 -0
- package/templates/starter-app/src/app/api/tasks/route.ts +37 -0
- package/templates/starter-app/src/app/api/upload/route.ts +15 -0
- package/templates/starter-app/src/app/globals.css +15 -0
- package/templates/starter-app/src/app/layout.tsx +16 -0
- package/templates/starter-app/src/app/page.tsx +8 -0
- package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +30 -0
- package/templates/starter-app/src/configs/entities/department.config.ts +24 -0
- package/templates/starter-app/src/configs/entities/index.ts +13 -0
- package/templates/starter-app/src/configs/i18n.ts +12 -0
- package/templates/starter-app/src/configs/permissions/index.ts +45 -0
- package/templates/starter-app/src/configs/permissions/master-data.permissions.ts +31 -0
- package/templates/starter-app/src/configs/permissions/system.permissions.ts +131 -0
- package/templates/starter-app/src/configs/permissions/types.ts +63 -0
- package/templates/starter-app/src/configs/tenant.ts +18 -0
- package/templates/starter-app/src/data/dictionary.ts +8 -0
- package/templates/starter-app/src/data/navigations.ts +61 -0
- package/templates/starter-app/src/instrumentation.ts +105 -0
- package/templates/starter-app/src/lib/api-handler.ts +157 -0
- package/templates/starter-app/src/lib/auth-client.ts +57 -0
- package/templates/starter-app/src/lib/auth.ts +62 -0
- package/templates/starter-app/src/lib/better-auth.ts +107 -0
- package/templates/starter-app/src/lib/branch-scope.ts +53 -0
- package/templates/starter-app/src/lib/cron/db-cron-manager.ts +42 -0
- package/templates/starter-app/src/lib/crud/index.ts +13 -0
- package/templates/starter-app/src/lib/errors/app-error.ts +2 -0
- package/templates/starter-app/src/lib/errors/error-handler.ts +5 -0
- package/templates/starter-app/src/lib/errors/log-server-error.ts +15 -0
- package/templates/starter-app/src/lib/errors/server-error.ts +11 -0
- package/templates/starter-app/src/lib/logger.ts +30 -0
- package/templates/starter-app/src/lib/page-guard.ts +35 -0
- package/templates/starter-app/src/lib/prisma.ts +80 -0
- package/templates/starter-app/src/lib/rbac/access.ts +87 -0
- package/templates/starter-app/src/lib/storage.ts +28 -0
- package/templates/starter-app/src/providers/index.tsx +54 -0
- package/templates/starter-app/src/providers/mode-provider.tsx +31 -0
- package/templates/starter-app/src/providers/theme-provider.tsx +21 -0
- package/templates/starter-app/src/proxy.ts +45 -0
- package/templates/starter-app/src/server/services/notification-service.ts +31 -0
- package/templates/starter-app/src/server/services/system-config-service.ts +163 -0
- package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +58 -0
- package/templates/starter-app/src/server/tasks/index.ts +16 -0
- package/templates/starter-app/src/server/tasks/task-runner.ts +40 -0
- package/templates/starter-app/src/types/session.ts +29 -0
- package/templates/starter-app/tsconfig.json +47 -0
- package/templates/starter-app/vitest.config.ts +17 -0
- package/src/infrastructure/cron/index.ts +0 -6
- package/src/infrastructure/event-bus/event-bus.ts +0 -145
- package/src/infrastructure/event-bus/index.ts +0 -2
- package/src/infrastructure/event-bus/types.ts +0 -22
- package/src/infrastructure/lock/decorators.ts +0 -67
- package/src/infrastructure/lock/index.ts +0 -2
- package/src/infrastructure/lock/lock-manager.ts +0 -33
- package/src/plugin/apps-registry.ts +0 -97
- package/src/plugin/index.ts +0 -5
- package/src/plugin/types.ts +0 -41
- package/src/ui/management/audit-log-page.tsx +0 -14
- package/src/ui/management/job-management.tsx +0 -308
- package/src/workflow/activity-timeline.tsx +0 -412
- package/src/workflow/approval-workflow.tsx +0 -31
- package/src/workflow/index.ts +0 -2
- /package/src/{infrastructure/cron → cron}/types.ts +0 -0
package/PLATFORM.md
CHANGED
|
@@ -10,6 +10,24 @@ implementation of every pattern here.
|
|
|
10
10
|
> mechanism, or a CAS update into an app, stop — it belongs in core. Promote it
|
|
11
11
|
> and re-export a thin shim from the app so call sites don't change.
|
|
12
12
|
|
|
13
|
+
## Starting a new app
|
|
14
|
+
|
|
15
|
+
This package ships the starter app it documents, plus the scaffolder:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx -p @goplusvn/core goerp-init my-app --port 3010
|
|
19
|
+
cd my-app && pnpm install
|
|
20
|
+
# edit DATABASE_URL in the generated .env
|
|
21
|
+
pnpm prisma:deploy && pnpm seed && pnpm rbac-sync && pnpm dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`goerp-init` fills in the app name, port, branding (`src/configs/tenant.ts`) and
|
|
25
|
+
a random `BETTER_AUTH_SECRET`; it never touches a database. `seed` must run
|
|
26
|
+
**before** `rbac-sync` — see the generated `README.md` for why. What you get is
|
|
27
|
+
a working login, RBAC, CRUD, audit/error logs, notifications, background tasks
|
|
28
|
+
and cron — all from core, not copied code. Do not copy the template directory by
|
|
29
|
+
hand: the rename steps are the whole point of the command.
|
|
30
|
+
|
|
13
31
|
## Design system (styles)
|
|
14
32
|
|
|
15
33
|
One import gives the whole token system; override only your brand.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Khởi tạo một app mới từ template đi kèm gói này.
|
|
3
|
+
//
|
|
4
|
+
// npx -p @goplusvn/core goerp-init <ten-app> [--dir <đường-dẫn>] [--port 3010]
|
|
5
|
+
// pnpm init-app <ten-app> ... (từ trong repo goerp-core)
|
|
6
|
+
//
|
|
7
|
+
// Template nằm trong chính gói npm (packages/core/templates/starter-app) nên
|
|
8
|
+
// lệnh này chạy được ở máy trắng, không cần clone goerp-core.
|
|
9
|
+
//
|
|
10
|
+
// App mới là repo ĐỘC LẬP, không phải thành viên workspace của core: nó cài
|
|
11
|
+
// @goerp/core từ npm như mọi app thật, nên lỗi kiểu "chạy được nhờ workspace"
|
|
12
|
+
// không lọt qua được.
|
|
13
|
+
//
|
|
14
|
+
// Script chỉ chép + thay tên/cổng + sinh secret. Không chạy pnpm install,
|
|
15
|
+
// không đụng DB — những việc đó in ra cho người dùng tự chạy.
|
|
16
|
+
|
|
17
|
+
import { randomBytes } from "node:crypto";
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
23
|
+
const templateDir = path.join(packageRoot, "templates", "starter-app");
|
|
24
|
+
|
|
25
|
+
// Thứ sinh ra lúc build/cài: chép sang chỉ tổ làm app mới hỏng theo cách khó hiểu.
|
|
26
|
+
// `.env` cũng bỏ — nó là bí mật của máy đang chép, app mới sinh secret riêng.
|
|
27
|
+
const SKIP = new Set([
|
|
28
|
+
"node_modules",
|
|
29
|
+
".next",
|
|
30
|
+
".turbo",
|
|
31
|
+
".git",
|
|
32
|
+
".env",
|
|
33
|
+
"tsconfig.tsbuildinfo",
|
|
34
|
+
"next-env.d.ts",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
function fail(message) {
|
|
38
|
+
console.error(`✗ ${message}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Tham số ──────────────────────────────────────────────────────────────────
|
|
43
|
+
const argv = process.argv.slice(2);
|
|
44
|
+
const name = argv.find((a) => !a.startsWith("-"));
|
|
45
|
+
if (!name) fail("Thiếu tên app. Dùng: goerp-init <ten-app> [--dir <path>] [--port 3010]");
|
|
46
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
47
|
+
fail(`Tên "${name}" không hợp lệ — dùng kebab-case (chữ thường, số, gạch ngang).`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const flag = (key, fallback) => {
|
|
51
|
+
const i = argv.indexOf(`--${key}`);
|
|
52
|
+
return i >= 0 && argv[i + 1] ? argv[i + 1] : fallback;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const port = Number(flag("port", "3010"));
|
|
56
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) fail(`Cổng không hợp lệ: ${port}`);
|
|
57
|
+
|
|
58
|
+
const targetDir = path.resolve(flag("dir", path.join(process.cwd(), name)));
|
|
59
|
+
|
|
60
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
61
|
+
fail(`${targetDir} đã tồn tại và không rỗng — chọn thư mục khác cho chắc.`);
|
|
62
|
+
}
|
|
63
|
+
if (!fs.existsSync(templateDir)) fail(`Không thấy template ở ${templateDir}`);
|
|
64
|
+
|
|
65
|
+
// ── Chép ─────────────────────────────────────────────────────────────────────
|
|
66
|
+
function copyDir(from, to) {
|
|
67
|
+
fs.mkdirSync(to, { recursive: true });
|
|
68
|
+
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
69
|
+
if (SKIP.has(entry.name)) continue;
|
|
70
|
+
const src = path.join(from, entry.name);
|
|
71
|
+
const dst = path.join(to, entry.name);
|
|
72
|
+
if (entry.isDirectory()) copyDir(src, dst);
|
|
73
|
+
else fs.copyFileSync(src, dst);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
copyDir(templateDir, targetDir);
|
|
77
|
+
|
|
78
|
+
const read = (p) => fs.readFileSync(path.join(targetDir, p), "utf8");
|
|
79
|
+
const write = (p, content) => fs.writeFileSync(path.join(targetDir, p), content);
|
|
80
|
+
|
|
81
|
+
// npm/pnpm KHÔNG đóng gói file tên `.gitignore` (nuốt hoặc đổi thành
|
|
82
|
+
// `.npmignore` khi cài). Template ship nó dưới tên `gitignore`, trả lại dấu
|
|
83
|
+
// chấm ở đây — thiếu bước này thì `git add .` đầu tiên của app mới commit luôn
|
|
84
|
+
// `.env` vừa sinh (kèm BETTER_AUTH_SECRET) và cả node_modules.
|
|
85
|
+
fs.renameSync(path.join(targetDir, "gitignore"), path.join(targetDir, ".gitignore"));
|
|
86
|
+
|
|
87
|
+
// "quan-ly-kho" → "Quan Ly Kho": nhãn hiển thị mặc định, chủ app sửa lại sau.
|
|
88
|
+
const title = name
|
|
89
|
+
.split("-")
|
|
90
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
91
|
+
.join(" ");
|
|
92
|
+
|
|
93
|
+
// ── package.json: tên + cổng ─────────────────────────────────────────────────
|
|
94
|
+
const pkg = JSON.parse(read("package.json"));
|
|
95
|
+
pkg.name = name;
|
|
96
|
+
pkg.version = "0.1.0";
|
|
97
|
+
for (const key of ["dev", "start"]) {
|
|
98
|
+
if (pkg.scripts?.[key]) pkg.scripts[key] = pkg.scripts[key].replace(/--port \d+/, `--port ${port}`);
|
|
99
|
+
}
|
|
100
|
+
write("package.json", `${JSON.stringify(pkg, null, 2)}\n`);
|
|
101
|
+
|
|
102
|
+
// ── .env: điền sẵn thứ điền được, để trống thứ chỉ người dùng biết ───────────
|
|
103
|
+
// BETTER_AUTH_SECRET sinh ngay tại đây: bỏ trống thì lần đăng nhập đầu tiên đổ
|
|
104
|
+
// lỗi khó đoán, mà nhét placeholder thì có ngày nó lên production.
|
|
105
|
+
const dbName = name.replace(/-/g, "_");
|
|
106
|
+
const env = read(".env.example")
|
|
107
|
+
.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL="postgresql://postgres:postgres@localhost:5432/${dbName}"`)
|
|
108
|
+
.replace(/^BETTER_AUTH_SECRET=.*$/m, `BETTER_AUTH_SECRET="${randomBytes(32).toString("base64")}"`)
|
|
109
|
+
.replace(/^BETTER_AUTH_URL=.*$/m, `BETTER_AUTH_URL="http://localhost:${port}"`);
|
|
110
|
+
write(".env", env);
|
|
111
|
+
|
|
112
|
+
// ── Thương hiệu: một chỗ duy nhất đổi tên hiển thị toàn app ──────────────────
|
|
113
|
+
// Bỏ qua bước này thì app mới chạy lên vẫn đề "GoERP Starter" trên sidebar,
|
|
114
|
+
// trang đăng nhập và tiêu đề tab — người dựng app phải đi tìm mới sửa được.
|
|
115
|
+
const tenantPath = "src/configs/tenant.ts";
|
|
116
|
+
write(
|
|
117
|
+
tenantPath,
|
|
118
|
+
read(tenantPath)
|
|
119
|
+
.replace(/(\bid:\s*)"[^"]*"/, `$1"${name}"`)
|
|
120
|
+
.replace(/(\bname:\s*)"[^"]*"/, `$1"${title}"`)
|
|
121
|
+
.replace(/(\bcompanyName:\s*)"[^"]*"/, `$1"${title}"`),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// ── Tài liệu: điền tên app ───────────────────────────────────────────────────
|
|
125
|
+
write("AGENTS.md", read("AGENTS.md").replace(/<APP-NAME>/g, name));
|
|
126
|
+
write("README.md", read("README.md").replace(/^# GoERP starter app$/m, `# ${title}`));
|
|
127
|
+
|
|
128
|
+
// ── Xong ─────────────────────────────────────────────────────────────────────
|
|
129
|
+
const relative = path.relative(process.cwd(), targetDir);
|
|
130
|
+
// Đường dẫn tương đối chỉ dễ đọc khi app nằm gần; xa quá thì in tuyệt đối.
|
|
131
|
+
const rel = relative && relative.length < targetDir.length ? relative : targetDir;
|
|
132
|
+
console.log(`✓ Đã tạo ${name} tại ${targetDir}\n`);
|
|
133
|
+
console.log("Tiếp theo:\n");
|
|
134
|
+
console.log(` cd ${rel}`);
|
|
135
|
+
console.log(" pnpm install");
|
|
136
|
+
console.log(" # sửa DATABASE_URL trong .env cho trỏ đúng Postgres của bạn");
|
|
137
|
+
console.log(" pnpm prisma:deploy # dựng bảng (migration _init có sẵn)");
|
|
138
|
+
console.log(" pnpm seed # vai trò + tài khoản admin");
|
|
139
|
+
console.log(" pnpm rbac-sync # PHẢI chạy SAU seed, xem README");
|
|
140
|
+
console.log(` pnpm dev # http://localhost:${port}`);
|
|
141
|
+
console.log("\n.env đã có sẵn BETTER_AUTH_SECRET sinh ngẫu nhiên — đừng commit file này.");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goplusvn/core",
|
|
3
3
|
"description": "GoPlusVN Platform Kit - ERP kernel: layout, RBAC, CRUD, multi-tenant, system pages",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.60",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"registry": "https://registry.npmjs.org",
|
|
@@ -12,13 +12,15 @@
|
|
|
12
12
|
"types": "./src/index.ts",
|
|
13
13
|
"sideEffects": false,
|
|
14
14
|
"bin": {
|
|
15
|
-
"goerp-features": "./bin/goerp-features.mjs"
|
|
15
|
+
"goerp-features": "./bin/goerp-features.mjs",
|
|
16
|
+
"goerp-init": "./bin/goerp-init.mjs"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"src",
|
|
19
20
|
"bin",
|
|
20
21
|
"scripts",
|
|
21
22
|
"features",
|
|
23
|
+
"templates",
|
|
22
24
|
"README.md",
|
|
23
25
|
"CHANGELOG.md",
|
|
24
26
|
"PLATFORM.md"
|
|
@@ -88,8 +90,6 @@
|
|
|
88
90
|
"./audit": "./src/audit/index.ts",
|
|
89
91
|
"./rbac/role-service": "./src/rbac/role-service.ts",
|
|
90
92
|
"./rbac/resource-service": "./src/rbac/resource-service.ts",
|
|
91
|
-
"./infrastructure/cron/cron-manager": "./src/infrastructure/cron/cron-manager.ts",
|
|
92
|
-
"./infrastructure/cron/types": "./src/infrastructure/cron/types.ts",
|
|
93
93
|
"./crud/pages/entity-crud-page": "./src/crud/pages/entity-crud-page.tsx",
|
|
94
94
|
"./auth/auth-service": "./src/auth/auth-service.ts",
|
|
95
95
|
"./package.json": "./package.json",
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { SimpleCronJob } from "
|
|
1
|
+
import { SimpleCronJob } from "./simple-cron-job";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
4
|
CronJob,
|
|
5
5
|
CronJobManager,
|
|
6
6
|
CronJobOptions,
|
|
7
|
-
} from "
|
|
7
|
+
} from "./types";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Cron CÓ TRẠNG THÁI TRONG DB — engine dùng chung mọi app goerp (bảng
|
|
11
11
|
* `system_jobs` + `job_execution_logs` ship qua `goerp-features sync`, feature
|
|
12
|
-
* system-jobs). Bản `
|
|
12
|
+
* system-jobs). Bản `SimpleCronJob` cạnh đây chỉ hẹn giờ trong RAM: restart là
|
|
13
13
|
* mất bật/tắt, không có lịch sử chạy, admin không nhìn thấy gì.
|
|
14
14
|
*
|
|
15
15
|
* Vai trò từng lớp:
|
package/src/cron/index.ts
CHANGED
|
@@ -16,9 +16,9 @@ export {
|
|
|
16
16
|
cronJobManager,
|
|
17
17
|
CronJobManagerImpl,
|
|
18
18
|
SimpleCronJob,
|
|
19
|
-
} from "
|
|
19
|
+
} from "./simple-cron-job";
|
|
20
20
|
export type {
|
|
21
21
|
CronJob,
|
|
22
22
|
CronJobManager,
|
|
23
23
|
CronJobOptions,
|
|
24
|
-
} from "
|
|
24
|
+
} from "./types";
|
|
@@ -1,103 +1,19 @@
|
|
|
1
|
-
import { describe, it, expect
|
|
2
|
-
import { LockManager } from "../lock/lock-manager";
|
|
3
|
-
import { WithLock } from "../lock/decorators";
|
|
4
|
-
import type { CacheOptions, Cache } from "../cache/types";
|
|
5
|
-
import { CacheNamespace } from "../cache/types";
|
|
6
|
-
|
|
7
|
-
// Mock Cache Implementation for Testing
|
|
8
|
-
class MockCache implements Cache {
|
|
9
|
-
private store = new Map<string, any>();
|
|
10
|
-
async get<T>(key: string): Promise<T | undefined> {
|
|
11
|
-
return this.store.get(key);
|
|
12
|
-
}
|
|
13
|
-
async set<T>(key: string, value: T): Promise<void> {
|
|
14
|
-
this.store.set(key, value);
|
|
15
|
-
}
|
|
16
|
-
async del(key: string): Promise<void> {
|
|
17
|
-
this.store.delete(key);
|
|
18
|
-
}
|
|
19
|
-
async has(key: string): Promise<boolean> {
|
|
20
|
-
return this.store.has(key);
|
|
21
|
-
}
|
|
22
|
-
async reset(): Promise<void> {
|
|
23
|
-
this.store.clear();
|
|
24
|
-
}
|
|
25
|
-
async keys(): Promise<string[]> {
|
|
26
|
-
return Array.from(this.store.keys());
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe("Architecture Verification: Phase 2 Enhancements", () => {
|
|
31
|
-
describe("2.2 Concurrency Control (LockManager)", () => {
|
|
32
|
-
it("should acquire and release lock successfully", async () => {
|
|
33
|
-
const cache = new MockCache();
|
|
34
|
-
const lockManager = new LockManager(cache);
|
|
35
|
-
const resourceId = "order-123";
|
|
36
|
-
|
|
37
|
-
const acquired = await lockManager.acquire(resourceId, { ttl: 1000 });
|
|
38
|
-
expect(acquired).toBe(true);
|
|
39
|
-
|
|
40
|
-
const isLocked = await cache.has(resourceId);
|
|
41
|
-
expect(isLocked).toBe(true);
|
|
42
|
-
|
|
43
|
-
await lockManager.release(resourceId);
|
|
44
|
-
const isLockedAfterRelease = await cache.has(resourceId);
|
|
45
|
-
expect(isLockedAfterRelease).toBe(false);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("should fail to acquire lock if already locked", async () => {
|
|
49
|
-
const cache = new MockCache();
|
|
50
|
-
const lockManager = new LockManager(cache);
|
|
51
|
-
const resourceId = "order-456";
|
|
52
|
-
|
|
53
|
-
await lockManager.acquire(resourceId, { ttl: 5000 }); // Lock 1
|
|
54
|
-
const acquiredAgain = await lockManager.acquire(resourceId, {
|
|
55
|
-
ttl: 1000,
|
|
56
|
-
}); // Lock 2
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
57
2
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe("2.2 Decorator Usage (@WithLock)", () => {
|
|
63
|
-
it("should execute method with lock", async () => {
|
|
64
|
-
const cache = new MockCache();
|
|
65
|
-
const lockManager = new LockManager(cache);
|
|
66
|
-
|
|
67
|
-
class OrderService {
|
|
68
|
-
lockManager = lockManager;
|
|
69
|
-
executionCount = 0;
|
|
70
|
-
|
|
71
|
-
@WithLock("{0}")
|
|
72
|
-
async processOrder(orderId: string) {
|
|
73
|
-
this.executionCount++;
|
|
74
|
-
return `Processed ${orderId}`;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const service = new OrderService();
|
|
79
|
-
const result = await service.processOrder("100");
|
|
3
|
+
import type { CacheOptions } from "../cache/types";
|
|
4
|
+
import { CacheNamespace } from "../cache/types";
|
|
80
5
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
});
|
|
6
|
+
describe("Cache: chiến lược namespace", () => {
|
|
7
|
+
it("CacheNamespace khai đúng giá trị", () => {
|
|
8
|
+
expect(CacheNamespace.Auth).toBe("auth");
|
|
9
|
+
expect(CacheNamespace.TenantConfig).toBe("tenant-config");
|
|
86
10
|
});
|
|
87
11
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
it("should allow configuring cache with namespace (Simulation)", () => {
|
|
95
|
-
// This tests the TYPE definition and intent, as actual implementation depends on the Factory we haven't fully refactored yet.
|
|
96
|
-
const options: CacheOptions = {
|
|
97
|
-
name: "redis",
|
|
98
|
-
prefix: CacheNamespace.Auth,
|
|
99
|
-
};
|
|
100
|
-
expect(options.prefix).toBe("auth");
|
|
101
|
-
});
|
|
12
|
+
it("prefix nhận namespace", () => {
|
|
13
|
+
const options: CacheOptions = {
|
|
14
|
+
name: "redis",
|
|
15
|
+
prefix: CacheNamespace.Auth,
|
|
16
|
+
};
|
|
17
|
+
expect(options.prefix).toBe("auth");
|
|
102
18
|
});
|
|
103
19
|
});
|
|
@@ -14,13 +14,10 @@ export type {
|
|
|
14
14
|
CacheManagerOptions,
|
|
15
15
|
} from "./cache";
|
|
16
16
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// CronJob
|
|
22
|
-
export { cronJobManager, CronJobManagerImpl, SimpleCronJob } from "./cron";
|
|
23
|
-
export type { CronJob, CronJobOptions, CronJobManager } from "./cron";
|
|
17
|
+
// Lịch chạy nền KHÔNG ở đây: dùng `@goerp/core/cron` (DbCronManager, đọc bảng
|
|
18
|
+
// system_jobs). Bản in-memory cũ đã bỏ — mất hết job khi process restart.
|
|
19
|
+
// Event-bus và lock in-memory cũng đã bỏ: chỉ đúng trong 1 tiến trình, mà app
|
|
20
|
+
// thật chạy nhiều instance — khoá thì dùng advisory lock của Postgres.
|
|
24
21
|
|
|
25
22
|
// API Service Layer (Inspired by Plane's Abstract APIService pattern)
|
|
26
23
|
export { APIService, defaultClientOptions, isAPIError } from "./api-service";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Trang quản trị bộ nhớ đệm. Nhật ký hoạt động nằm ở
|
|
2
|
+
// `@goerp/core/system/pages/system-audit-page` (SystemAuditPage) — bản
|
|
3
|
+
// AuditLogPage cũ đọc RAM trong client component nên luôn rỗng, đã bỏ.
|
|
3
4
|
export * from "./cache-management";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Git & IDE
|
|
2
|
+
.git
|
|
3
|
+
.gitignore
|
|
4
|
+
**/.DS_Store
|
|
5
|
+
**/.idea
|
|
6
|
+
**/.vscode
|
|
7
|
+
|
|
8
|
+
# Kết quả build — dựng lại bên trong Docker
|
|
9
|
+
**/.next
|
|
10
|
+
**/dist
|
|
11
|
+
**/build
|
|
12
|
+
**/.turbo
|
|
13
|
+
|
|
14
|
+
# Cài lại bằng pnpm trong image; giữ context nhỏ
|
|
15
|
+
node_modules
|
|
16
|
+
**/node_modules
|
|
17
|
+
|
|
18
|
+
# Test
|
|
19
|
+
**/__tests__
|
|
20
|
+
**/*.test.ts
|
|
21
|
+
**/*.test.tsx
|
|
22
|
+
**/coverage
|
|
23
|
+
|
|
24
|
+
# Log
|
|
25
|
+
*.log
|
|
26
|
+
**/logs
|
|
27
|
+
|
|
28
|
+
# Env — tuyệt đối không đưa secret vào image
|
|
29
|
+
.env
|
|
30
|
+
**/.env.local
|
|
31
|
+
**/.env.*.local
|
|
32
|
+
|
|
33
|
+
# Tài liệu
|
|
34
|
+
**/docs
|
|
35
|
+
**/*.md
|
|
36
|
+
!**/README.md
|
|
37
|
+
|
|
38
|
+
# ⚠️ Đừng thêm glob trần kiểu **/check-*.ts hay **/seed*.ts: chúng khớp CẢ mã
|
|
39
|
+
# nguồn app (một route handler tên check-*.ts chẳng hạn) → build local xanh
|
|
40
|
+
# (local không đọc .dockerignore) nhưng production 500 "Module not found".
|
|
41
|
+
# Muốn loại script dev thì neo theo thư mục: **/scripts/seed*.ts
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# Sao chép thành .env rồi điền. Ba biến đầu là BẮT BUỘC.
|
|
3
|
+
# ============================================================
|
|
4
|
+
|
|
5
|
+
# Postgres. Prisma 7 đọc qua prisma.config.ts (driver adapter), app đọc trong
|
|
6
|
+
# src/lib/prisma.ts. Cùng một biến, không tách hai.
|
|
7
|
+
DATABASE_URL="postgresql://user:password@localhost:5432/starter_app"
|
|
8
|
+
|
|
9
|
+
# Better Auth. SECRET ký cookie phiên — đổi giá trị này là mọi người bị đăng
|
|
10
|
+
# xuất. Sinh bằng: openssl rand -base64 32
|
|
11
|
+
BETTER_AUTH_SECRET="generate-with: openssl rand -base64 32"
|
|
12
|
+
# URL gốc THẬT của app. Better Auth dùng nó để kiểm Origin của các POST
|
|
13
|
+
# (chống CSRF) — sai giá trị thì đăng nhập trả 403 dù mật khẩu đúng.
|
|
14
|
+
BETTER_AUTH_URL="http://localhost:3010"
|
|
15
|
+
|
|
16
|
+
# Trang chủ sau khi đăng nhập = "/vi" (src/proxy.ts: homePath). Muốn đổi thì
|
|
17
|
+
# sửa thẳng ở đó — đường dẫn PHẢI có locale, để "/" thì proxy localize lại và
|
|
18
|
+
# tạo vòng lặp chuyển hướng.
|
|
19
|
+
|
|
20
|
+
# ---- Tùy chọn ----
|
|
21
|
+
|
|
22
|
+
# Chỉ DEV: dựng vỏ app mà KHÔNG cần đăng nhập (phiên admin giả). Tiện lúc mới
|
|
23
|
+
# clone; bỏ đi khi đã seed tài khoản thật. Đặt trên production cũng vô hiệu —
|
|
24
|
+
# cả app lẫn core đều chặn cứng khi NODE_ENV=production.
|
|
25
|
+
# BYPASS_AUTH=1
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# <APP-NAME> — Quy tắc cho AI
|
|
2
|
+
|
|
3
|
+
> **Đọc trước:** `goerp-core/AGENTS.md` (nếu goerp-core là repo sibling) — quy tắc
|
|
4
|
+
> nền cho mọi app dựng trên `@goerp/core`. File này chỉ nói phần riêng của app.
|
|
5
|
+
|
|
6
|
+
## App này (điền khi khởi tạo)
|
|
7
|
+
|
|
8
|
+
- **Nghiệp vụ:** <mô tả domain>.
|
|
9
|
+
- **DB:** `DATABASE_URL` trong `.env`. Nếu đi qua SSH tunnel thì kiểm bằng
|
|
10
|
+
`nc -z localhost <port>` TRƯỚC khi chạy bất cứ lệnh Prisma nào.
|
|
11
|
+
- **Cổng dev:** 3010 (đổi trong `package.json`).
|
|
12
|
+
|
|
13
|
+
## Luật cứng
|
|
14
|
+
|
|
15
|
+
**1. Tái dùng core, đừng chế lại.** Trước khi viết một màn hình/lớp hạ tầng, tìm
|
|
16
|
+
trong `@goerp/core` xem đã có chưa: trang RBAC, trang cấu hình/nhật ký/tác vụ,
|
|
17
|
+
CRUD engine, export, print, task runner, notification, cron. Cần thêm chức năng
|
|
18
|
+
generic → **bổ sung vào core** (additive) rồi dùng, đừng fork vào app.
|
|
19
|
+
|
|
20
|
+
**2. Không route nào tự gác.** Mọi handler đi qua `apiHandler` ở
|
|
21
|
+
`src/lib/api-handler.ts`; mọi trang cần quyền gọi `requirePageAccess`. Không tự
|
|
22
|
+
viết chuỗi `getSession → 401 → checkPermission → 403`.
|
|
23
|
+
|
|
24
|
+
**3. Quyền phải khai trước khi dùng.** Chuỗi `resource` trong route/menu phải tồn
|
|
25
|
+
tại trong `src/configs/permissions/`. Khai xong chạy `pnpm rbac-sync`. Gõ sai một
|
|
26
|
+
ký tự = nút biến mất vĩnh viễn, không có thông báo. `pnpm test` bắt lỗi này.
|
|
27
|
+
|
|
28
|
+
**4. DB có dữ liệu thật thì chỉ `migrate deploy`.** `prisma migrate dev`,
|
|
29
|
+
`db push`, `migrate reset` (và các tool MCP tương ứng) chỉ dành cho DB trống lúc
|
|
30
|
+
khởi tạo. Với DB đang chạy: viết SQL tay vào `prisma/migrations/<ts>_<tên>/` rồi
|
|
31
|
+
`pnpm prisma:deploy`.
|
|
32
|
+
|
|
33
|
+
**5. Mỗi lần sửa xong: `pnpm type-check` + `pnpm test`.** Không dồn kiểm tra về
|
|
34
|
+
cuối. Ratchet trong `src/__tests__/architecture.test.ts` chỉ được siết chặt thêm,
|
|
35
|
+
không được nới ra.
|
|
36
|
+
|
|
37
|
+
**6. Lỗi phải đi vào `error_logs`.** Route dùng `serverError(error, req)` trong
|
|
38
|
+
catch; đừng nuốt lỗi bằng `console.log` rồi trả 200.
|
|
39
|
+
|
|
40
|
+
## Bẫy đã trả giá
|
|
41
|
+
|
|
42
|
+
- Đổi schema xong phải **restart `next dev`**. KHÔNG `rm -rf .next` khi dev chạy.
|
|
43
|
+
- Cron của core = `setInterval` tính từ lúc boot, không phải lịch theo giờ. Muốn
|
|
44
|
+
chạy đúng giờ: hẹn mỗi giờ + chặn bằng `isLocalHour()` (`src/instrumentation.ts`).
|
|
45
|
+
- Handler tác vụ nền phải được import qua `src/server/tasks/index.ts` (side-effect
|
|
46
|
+
đăng ký) trước khi có ai enqueue, nếu không tác vụ nằm mãi ở `pending`.
|
|
47
|
+
- Mọi đường ghi cấu hình phải `revalidateTag("system-settings", "max")`.
|
|
48
|
+
- Ma trận quyền của core đọc chuỗi `"action:resource"` — đảo thứ tự thì không ô
|
|
49
|
+
nào tick và cũng chẳng có lỗi nào hiện ra.
|
|
50
|
+
- Refactor lớn: chạy `pnpm build` thật trước khi deploy, và soi lại
|
|
51
|
+
`.dockerignore` — glob trần kiểu `**/check-*.ts` khớp cả mã nguồn app, làm build
|
|
52
|
+
local xanh nhưng production 500 "Module not found".
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
# App độc lập trên @goerp/core — một app Next.js, không monorepo/turbo.
|
|
3
|
+
# Output `standalone` (khai trong next.config.mjs) nên image chỉ chứa server.js
|
|
4
|
+
# + đúng những node_modules đã được trace, không phải cả cây phụ thuộc.
|
|
5
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
# ── Stage 1: base ────────────────────────────────────────────────────────────
|
|
8
|
+
# Node ≥24.15 BẮT BUỘC: vá race TransformStream (nodejs/node#62040) — khách hủy
|
|
9
|
+
# request giữa lúc SSR streaming làm nổ "transformAlgorithm is not a function"
|
|
10
|
+
# trên node:22, và lỗi đó rơi thẳng vào error_logs.
|
|
11
|
+
FROM node:24-alpine AS base
|
|
12
|
+
ENV PNPM_HOME="/pnpm"
|
|
13
|
+
ENV PATH="$PNPM_HOME:$PATH"
|
|
14
|
+
RUN npm install -g pnpm@10.8.1
|
|
15
|
+
# libc6-compat + openssl: engine Prisma cần trên Alpine.
|
|
16
|
+
# tzdata: cấp /usr/share/zoneinfo để biến TZ ở stage runner có tác dụng. Thiếu
|
|
17
|
+
# nó Alpine đứng nguyên UTC, và mọi phép "hôm nay" tính phía server lệch một
|
|
18
|
+
# ngày trong khung 00:00–07:00 giờ VN.
|
|
19
|
+
RUN apk add --no-cache libc6-compat openssl tzdata
|
|
20
|
+
|
|
21
|
+
# ── Stage 2: deps ────────────────────────────────────────────────────────────
|
|
22
|
+
FROM base AS deps
|
|
23
|
+
WORKDIR /app
|
|
24
|
+
COPY package.json pnpm-lock.yaml ./
|
|
25
|
+
RUN pnpm install --frozen-lockfile
|
|
26
|
+
|
|
27
|
+
# ── Stage 3: builder ─────────────────────────────────────────────────────────
|
|
28
|
+
FROM base AS builder
|
|
29
|
+
WORKDIR /app
|
|
30
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
31
|
+
COPY . .
|
|
32
|
+
|
|
33
|
+
# `prisma generate` đòi DATABASE_URL lúc build. Giá trị giả là đủ để sinh
|
|
34
|
+
# client; URL thật tiêm lúc chạy.
|
|
35
|
+
ARG DATABASE_URL
|
|
36
|
+
ENV DATABASE_URL=${DATABASE_URL:-"postgresql://dummy:dummy@localhost:5432/dummy"}
|
|
37
|
+
|
|
38
|
+
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
|
39
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
40
|
+
|
|
41
|
+
RUN pnpm run build
|
|
42
|
+
|
|
43
|
+
# ── Stage 4: runner ──────────────────────────────────────────────────────────
|
|
44
|
+
FROM base AS runner
|
|
45
|
+
WORKDIR /app
|
|
46
|
+
|
|
47
|
+
RUN addgroup --system --gid 1001 nodejs && \
|
|
48
|
+
adduser --system --uid 1001 nextjs
|
|
49
|
+
|
|
50
|
+
ENV NODE_ENV=production
|
|
51
|
+
ENV PORT=3000
|
|
52
|
+
ENV HOSTNAME="0.0.0.0"
|
|
53
|
+
# Đồng hồ container theo giờ VN để `new Date()` và các phép cắt ngày phía server
|
|
54
|
+
# khớp GMT+7. Dữ liệu đã lưu (created_at) không đổi.
|
|
55
|
+
ENV TZ=Asia/Ho_Chi_Minh
|
|
56
|
+
|
|
57
|
+
# standalone: server.js + node_modules đã trace nằm ngay gốc.
|
|
58
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
59
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
60
|
+
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
|
61
|
+
|
|
62
|
+
# File tác vụ nền ghi vào <cwd>/storage khi chưa cấu hình S3/MinIO. Tạo sẵn +
|
|
63
|
+
# cấp quyền, nếu không tác vụ xuất đầu tiên chết vì EACCES. Muốn file sống qua
|
|
64
|
+
# lần deploy sau thì mount volume vào đúng đường dẫn này.
|
|
65
|
+
RUN mkdir -p /app/storage && chown -R nextjs:nodejs /app/storage
|
|
66
|
+
|
|
67
|
+
USER nextjs
|
|
68
|
+
EXPOSE 3000
|
|
69
|
+
|
|
70
|
+
# Di trú KHÔNG chạy ở đây: image standalone không có Prisma CLI. Chạy
|
|
71
|
+
# `pnpm prisma migrate deploy` từ CI (hoặc một container one-shot dựng từ stage
|
|
72
|
+
# `builder`) TRƯỚC khi đổi sang bản mới — như vậy migration hỏng thì dừng ở đó,
|
|
73
|
+
# chứ không làm container khởi động lại vô hạn.
|
|
74
|
+
CMD ["node", "server.js"]
|