@goplusvn/core 0.1.59 → 0.1.61
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/crud/lib/mutation-builder.ts +105 -0
- package/src/crud/lib/query-builder.ts +119 -0
- package/src/crud/server-service.ts +35 -163
- 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.61",
|
|
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";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { EntityConfig } from "../../types";
|
|
2
|
+
import type { ServerCrudLogger } from "../server-service";
|
|
3
|
+
|
|
4
|
+
export interface PrepareMutationDataOptions {
|
|
5
|
+
data: any;
|
|
6
|
+
config?: EntityConfig;
|
|
7
|
+
mode: "create" | "update";
|
|
8
|
+
systemFields: string[];
|
|
9
|
+
relationFieldSkip?: string[];
|
|
10
|
+
logger: ServerCrudLogger;
|
|
11
|
+
touchUpdatedAtOnCreate?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function prepareMutationData(options: PrepareMutationDataOptions) {
|
|
15
|
+
const { data, config, mode, systemFields, relationFieldSkip, logger, touchUpdatedAtOnCreate } = options;
|
|
16
|
+
|
|
17
|
+
let filtered = data;
|
|
18
|
+
|
|
19
|
+
if (config) {
|
|
20
|
+
// 1. Filter valid fields
|
|
21
|
+
const valid = new Set(config.fields.filter((f) => !(f as any).isDisplayOnly).map((f) => f.name));
|
|
22
|
+
for (const f of systemFields) valid.add(f);
|
|
23
|
+
filtered = {};
|
|
24
|
+
for (const [k, v] of Object.entries(data)) {
|
|
25
|
+
if (valid.has(k)) {
|
|
26
|
+
filtered[k] = v;
|
|
27
|
+
} else {
|
|
28
|
+
logger.warn(`Filtering out invalid field "${k}" for entity "${config.name}"`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 2. Cast field values (boolean, number)
|
|
33
|
+
for (const field of config.fields) {
|
|
34
|
+
const value = filtered[field.name];
|
|
35
|
+
if (value === undefined || value === null) continue;
|
|
36
|
+
|
|
37
|
+
if (field.type === "boolean" || field.type === "switch") {
|
|
38
|
+
let isTrue: boolean;
|
|
39
|
+
if (typeof value === "string") {
|
|
40
|
+
const lv = value.toLowerCase();
|
|
41
|
+
isTrue = lv === "true" || lv === "active" || value === "1" || value === "on";
|
|
42
|
+
} else {
|
|
43
|
+
isTrue = Boolean(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (field.type === "switch" && field.options && field.options.length >= 2) {
|
|
47
|
+
filtered[field.name] = isTrue ? (field.options[0] as any).value : (field.options[1] as any).value;
|
|
48
|
+
} else {
|
|
49
|
+
filtered[field.name] = isTrue;
|
|
50
|
+
}
|
|
51
|
+
} else if (field.type === "number" || (field.type as string) === "integer") {
|
|
52
|
+
if (typeof value === "string") {
|
|
53
|
+
if (value.trim() === "") {
|
|
54
|
+
filtered[field.name] = null;
|
|
55
|
+
} else {
|
|
56
|
+
const num = Number(value);
|
|
57
|
+
if (!isNaN(num)) filtered[field.name] = num;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3. System defaults for create
|
|
65
|
+
if (mode === "create") {
|
|
66
|
+
if (!filtered.id) {
|
|
67
|
+
filtered.id = crypto.randomUUID();
|
|
68
|
+
}
|
|
69
|
+
if (touchUpdatedAtOnCreate && !filtered.updatedAt) {
|
|
70
|
+
filtered.updatedAt = new Date();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 4. Transform relation fields (*Id -> connect/disconnect)
|
|
75
|
+
const skip = new Set([
|
|
76
|
+
"id",
|
|
77
|
+
"createdBy",
|
|
78
|
+
"updatedBy",
|
|
79
|
+
"citizenId",
|
|
80
|
+
"targetId",
|
|
81
|
+
...(relationFieldSkip ?? []),
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const prismaData = { ...filtered };
|
|
85
|
+
for (const key of Object.keys(prismaData)) {
|
|
86
|
+
if (skip.has(key)) continue;
|
|
87
|
+
|
|
88
|
+
if (key.endsWith("Id") && key.length > 2) {
|
|
89
|
+
const rel = key.slice(0, -2);
|
|
90
|
+
const value = prismaData[key];
|
|
91
|
+
|
|
92
|
+
if (value && typeof value === "string" && value.trim() !== "") {
|
|
93
|
+
prismaData[rel] = { connect: { id: value } };
|
|
94
|
+
delete prismaData[key];
|
|
95
|
+
} else if (value === null || value === undefined || value === "") {
|
|
96
|
+
if (mode === "update") {
|
|
97
|
+
prismaData[rel] = { disconnect: true };
|
|
98
|
+
}
|
|
99
|
+
delete prismaData[key];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return prismaData;
|
|
105
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { compileFilterTree } from "./filter-tree";
|
|
2
|
+
import type { CrudQueryParams, EntityConfig } from "../../types";
|
|
3
|
+
|
|
4
|
+
export interface BuildListQueryOptions {
|
|
5
|
+
entity: string;
|
|
6
|
+
config: EntityConfig;
|
|
7
|
+
params: CrudQueryParams;
|
|
8
|
+
scopeWhere?: Record<string, unknown>;
|
|
9
|
+
onDisallowedFilter?: (field: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildListQuery({
|
|
13
|
+
entity,
|
|
14
|
+
config,
|
|
15
|
+
params,
|
|
16
|
+
scopeWhere,
|
|
17
|
+
onDisallowedFilter,
|
|
18
|
+
}: BuildListQueryOptions) {
|
|
19
|
+
const { page = 1, pageSize = 10, search, sort, filters, filterTree } = params;
|
|
20
|
+
const MAX_PAGE_SIZE = 200;
|
|
21
|
+
|
|
22
|
+
const safePage = Math.max(1, Number(page) || 1);
|
|
23
|
+
const safePageSize = Math.min(Math.max(1, Number(pageSize) || 10), MAX_PAGE_SIZE);
|
|
24
|
+
const skip = (safePage - 1) * safePageSize;
|
|
25
|
+
const take = safePageSize;
|
|
26
|
+
|
|
27
|
+
const allowedFields = new Set<string>([
|
|
28
|
+
...config.fields.map((f) => f.name),
|
|
29
|
+
"id", "createdAt", "updatedAt", "createdBy", "updatedBy", config.idField || "id",
|
|
30
|
+
]);
|
|
31
|
+
const allowedRelations = new Set<string>(config.include || []);
|
|
32
|
+
const isAllowed = (name: string) => {
|
|
33
|
+
if (!name) return false;
|
|
34
|
+
if (name.includes(".")) return allowedRelations.has(name.split(".")[0]);
|
|
35
|
+
return allowedFields.has(name);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const where: any = { ...(scopeWhere ?? {}) };
|
|
39
|
+
if (search && search.trim()) {
|
|
40
|
+
const term = search.trim();
|
|
41
|
+
const searchFields = config.fields.filter((f) => f.filterable && f.type === "text").map((f) => f.name);
|
|
42
|
+
if (searchFields.length) where.OR = searchFields.map((f) => ({ [f]: { contains: term, mode: "insensitive" } }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (filters && filters.length) {
|
|
46
|
+
for (const filter of filters) {
|
|
47
|
+
const { name, value, operator } = filter as any;
|
|
48
|
+
if (value === undefined || value === null || (Array.isArray(value) && value.length === 0)) continue;
|
|
49
|
+
if (!isAllowed(name)) {
|
|
50
|
+
if (onDisallowedFilter) onDisallowedFilter(name);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
let target = where;
|
|
54
|
+
let key = name;
|
|
55
|
+
if (name.includes(".")) {
|
|
56
|
+
const parts = name.split(".");
|
|
57
|
+
key = parts.pop()!;
|
|
58
|
+
for (const p of parts) { if (!target[p]) target[p] = {}; target = target[p]; }
|
|
59
|
+
}
|
|
60
|
+
const op = operator as string;
|
|
61
|
+
if (op === "contains") target[key] = { contains: value, mode: "insensitive" };
|
|
62
|
+
else if (op === "in") target[key] = { in: value };
|
|
63
|
+
else if (op === "notIn") target[key] = { notIn: value };
|
|
64
|
+
else if (op === "eq") target[key] = value;
|
|
65
|
+
else if (op === "ne") target[key] = { not: value };
|
|
66
|
+
else if (op === "gt") target[key] = { gt: value };
|
|
67
|
+
else if (op === "gte") target[key] = { gte: value };
|
|
68
|
+
else if (op === "lt") target[key] = { lt: value };
|
|
69
|
+
else if (op === "lte") target[key] = { lte: value };
|
|
70
|
+
else if (op === "startsWith") target[key] = { startsWith: value, mode: "insensitive" };
|
|
71
|
+
else if (op === "endsWith") target[key] = { endsWith: value, mode: "insensitive" };
|
|
72
|
+
else if (op === "isNull") target[key] = null;
|
|
73
|
+
else if (op === "isNotNull") target[key] = { not: null };
|
|
74
|
+
else target[key] = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (filterTree) {
|
|
79
|
+
const compiled = compileFilterTree(filterTree, {
|
|
80
|
+
isAllowed,
|
|
81
|
+
onDisallowed: (field) => {
|
|
82
|
+
if (onDisallowedFilter) onDisallowedFilter(field);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
if (compiled) {
|
|
86
|
+
where.AND = [...(Array.isArray(where.AND) ? where.AND : where.AND ? [where.AND] : []), compiled];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const orderBy: any = {};
|
|
91
|
+
const applySort = (field: string, direction: any) => {
|
|
92
|
+
if (field.includes(".")) {
|
|
93
|
+
const parts = field.split(".");
|
|
94
|
+
const leaf = parts.pop()!;
|
|
95
|
+
let t = orderBy;
|
|
96
|
+
for (const p of parts) { t[p] = t[p] || {}; t = t[p]; }
|
|
97
|
+
t[leaf] = direction;
|
|
98
|
+
} else orderBy[field] = direction;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (sort && isAllowed(sort.field)) applySort(sort.field, sort.direction);
|
|
102
|
+
else if (config.defaultSort) applySort(config.defaultSort.field, config.defaultSort.direction);
|
|
103
|
+
else if (config.fields.some((f) => f.name === "createdAt")) orderBy.createdAt = "desc";
|
|
104
|
+
else orderBy[config.idField || "id"] = "desc";
|
|
105
|
+
|
|
106
|
+
const include: any = {};
|
|
107
|
+
if (config.include?.length) config.include.forEach((inc) => (include[inc] = true));
|
|
108
|
+
const includeOption = Object.keys(include).length ? { include } : {};
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
where,
|
|
112
|
+
orderBy,
|
|
113
|
+
includeOption,
|
|
114
|
+
skip,
|
|
115
|
+
take,
|
|
116
|
+
safePage,
|
|
117
|
+
safePageSize,
|
|
118
|
+
};
|
|
119
|
+
}
|