@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.
Files changed (142) hide show
  1. package/PLATFORM.md +18 -0
  2. package/bin/goerp-init.mjs +141 -0
  3. package/package.json +4 -4
  4. package/src/cron/db-cron-manager.ts +3 -3
  5. package/src/cron/index.ts +2 -2
  6. package/src/{infrastructure/cron/cron-manager.ts → cron/simple-cron-job.ts} +1 -1
  7. package/src/crud/lib/mutation-builder.ts +105 -0
  8. package/src/crud/lib/query-builder.ts +119 -0
  9. package/src/crud/server-service.ts +35 -163
  10. package/src/infrastructure/__tests__/architecture-verification.spec.ts +13 -97
  11. package/src/infrastructure/index.ts +4 -7
  12. package/src/ui/management/index.ts +3 -2
  13. package/templates/starter-app/.dockerignore +41 -0
  14. package/templates/starter-app/.env.example +25 -0
  15. package/templates/starter-app/AGENTS.md +52 -0
  16. package/templates/starter-app/Dockerfile +74 -0
  17. package/templates/starter-app/README.md +141 -0
  18. package/templates/starter-app/gitignore +9 -0
  19. package/templates/starter-app/next.config.mjs +50 -0
  20. package/templates/starter-app/package.json +55 -0
  21. package/templates/starter-app/postcss.config.mjs +5 -0
  22. package/templates/starter-app/prisma/migrations/20260801000000_init/migration.sql +504 -0
  23. package/templates/starter-app/prisma/migrations/20260801091814_goerp_audit_logs_0001_init/migration.sql +33 -0
  24. package/templates/starter-app/prisma/migrations/20260801091815_goerp_background_tasks_0001_init/migration.sql +23 -0
  25. package/templates/starter-app/prisma/migrations/20260801091816_goerp_error_logs_0001_init/migration.sql +32 -0
  26. package/templates/starter-app/prisma/migrations/20260801091817_goerp_notifications_0001_init/migration.sql +59 -0
  27. package/templates/starter-app/prisma/migrations/20260801091818_goerp_system_jobs_0001_init/migration.sql +47 -0
  28. package/templates/starter-app/prisma/schema/auth.prisma +87 -0
  29. package/templates/starter-app/prisma/schema/domain.prisma +24 -0
  30. package/templates/starter-app/prisma/schema/goerp-audit-logs.prisma +23 -0
  31. package/templates/starter-app/prisma/schema/goerp-background-tasks.prisma +25 -0
  32. package/templates/starter-app/prisma/schema/goerp-error-logs.prisma +31 -0
  33. package/templates/starter-app/prisma/schema/goerp-notifications.prisma +49 -0
  34. package/templates/starter-app/prisma/schema/goerp-system-jobs.prisma +45 -0
  35. package/templates/starter-app/prisma/schema/organization.prisma +31 -0
  36. package/templates/starter-app/prisma/schema/rbac.prisma +100 -0
  37. package/templates/starter-app/prisma/schema/schema.prisma +8 -0
  38. package/templates/starter-app/prisma/schema/system.prisma +22 -0
  39. package/templates/starter-app/prisma/seed.ts +127 -0
  40. package/templates/starter-app/prisma.config.ts +20 -0
  41. package/templates/starter-app/public/.gitkeep +2 -0
  42. package/templates/starter-app/scripts/rbac-sync.ts +235 -0
  43. package/templates/starter-app/src/__tests__/architecture.test.ts +151 -0
  44. package/templates/starter-app/src/app/[lang]/(main)/admin/system/audit/page.tsx +24 -0
  45. package/templates/starter-app/src/app/[lang]/(main)/admin/system/error-logs/page.tsx +21 -0
  46. package/templates/starter-app/src/app/[lang]/(main)/admin/system/jobs/page.tsx +18 -0
  47. package/templates/starter-app/src/app/[lang]/(main)/admin/system/settings/page.tsx +22 -0
  48. package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +41 -0
  49. package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +13 -0
  50. package/templates/starter-app/src/app/[lang]/(main)/notifications/page.tsx +13 -0
  51. package/templates/starter-app/src/app/[lang]/(main)/page.tsx +130 -0
  52. package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +60 -0
  53. package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +41 -0
  54. package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +48 -0
  55. package/templates/starter-app/src/app/[lang]/(main)/tasks/page.tsx +13 -0
  56. package/templates/starter-app/src/app/[lang]/(plain)/layout.tsx +10 -0
  57. package/templates/starter-app/src/app/[lang]/(plain)/sign-in/page.tsx +39 -0
  58. package/templates/starter-app/src/app/[lang]/layout.tsx +21 -0
  59. package/templates/starter-app/src/app/api/admin/system/audit/route.ts +60 -0
  60. package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +39 -0
  61. package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +96 -0
  62. package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +17 -0
  63. package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +19 -0
  64. package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +20 -0
  65. package/templates/starter-app/src/app/api/admin/system/settings/route.ts +49 -0
  66. package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +28 -0
  67. package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +24 -0
  68. package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +23 -0
  69. package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +13 -0
  70. package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +13 -0
  71. package/templates/starter-app/src/app/api/crud/[entity]/route.ts +14 -0
  72. package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +23 -0
  73. package/templates/starter-app/src/app/api/error-logs/route.ts +124 -0
  74. package/templates/starter-app/src/app/api/files/[...key]/route.ts +25 -0
  75. package/templates/starter-app/src/app/api/notifications/read/route.ts +29 -0
  76. package/templates/starter-app/src/app/api/notifications/route.ts +26 -0
  77. package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +14 -0
  78. package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +11 -0
  79. package/templates/starter-app/src/app/api/roles/[id]/route.ts +14 -0
  80. package/templates/starter-app/src/app/api/roles/route.ts +18 -0
  81. package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +52 -0
  82. package/templates/starter-app/src/app/api/tasks/route.ts +37 -0
  83. package/templates/starter-app/src/app/api/upload/route.ts +15 -0
  84. package/templates/starter-app/src/app/globals.css +15 -0
  85. package/templates/starter-app/src/app/layout.tsx +16 -0
  86. package/templates/starter-app/src/app/page.tsx +8 -0
  87. package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +30 -0
  88. package/templates/starter-app/src/configs/entities/department.config.ts +24 -0
  89. package/templates/starter-app/src/configs/entities/index.ts +13 -0
  90. package/templates/starter-app/src/configs/i18n.ts +12 -0
  91. package/templates/starter-app/src/configs/permissions/index.ts +45 -0
  92. package/templates/starter-app/src/configs/permissions/master-data.permissions.ts +31 -0
  93. package/templates/starter-app/src/configs/permissions/system.permissions.ts +131 -0
  94. package/templates/starter-app/src/configs/permissions/types.ts +63 -0
  95. package/templates/starter-app/src/configs/tenant.ts +18 -0
  96. package/templates/starter-app/src/data/dictionary.ts +8 -0
  97. package/templates/starter-app/src/data/navigations.ts +61 -0
  98. package/templates/starter-app/src/instrumentation.ts +105 -0
  99. package/templates/starter-app/src/lib/api-handler.ts +157 -0
  100. package/templates/starter-app/src/lib/auth-client.ts +57 -0
  101. package/templates/starter-app/src/lib/auth.ts +62 -0
  102. package/templates/starter-app/src/lib/better-auth.ts +107 -0
  103. package/templates/starter-app/src/lib/branch-scope.ts +53 -0
  104. package/templates/starter-app/src/lib/cron/db-cron-manager.ts +42 -0
  105. package/templates/starter-app/src/lib/crud/index.ts +13 -0
  106. package/templates/starter-app/src/lib/errors/app-error.ts +2 -0
  107. package/templates/starter-app/src/lib/errors/error-handler.ts +5 -0
  108. package/templates/starter-app/src/lib/errors/log-server-error.ts +15 -0
  109. package/templates/starter-app/src/lib/errors/server-error.ts +11 -0
  110. package/templates/starter-app/src/lib/logger.ts +30 -0
  111. package/templates/starter-app/src/lib/page-guard.ts +35 -0
  112. package/templates/starter-app/src/lib/prisma.ts +80 -0
  113. package/templates/starter-app/src/lib/rbac/access.ts +87 -0
  114. package/templates/starter-app/src/lib/storage.ts +28 -0
  115. package/templates/starter-app/src/providers/index.tsx +54 -0
  116. package/templates/starter-app/src/providers/mode-provider.tsx +31 -0
  117. package/templates/starter-app/src/providers/theme-provider.tsx +21 -0
  118. package/templates/starter-app/src/proxy.ts +45 -0
  119. package/templates/starter-app/src/server/services/notification-service.ts +31 -0
  120. package/templates/starter-app/src/server/services/system-config-service.ts +163 -0
  121. package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +58 -0
  122. package/templates/starter-app/src/server/tasks/index.ts +16 -0
  123. package/templates/starter-app/src/server/tasks/task-runner.ts +40 -0
  124. package/templates/starter-app/src/types/session.ts +29 -0
  125. package/templates/starter-app/tsconfig.json +47 -0
  126. package/templates/starter-app/vitest.config.ts +17 -0
  127. package/src/infrastructure/cron/index.ts +0 -6
  128. package/src/infrastructure/event-bus/event-bus.ts +0 -145
  129. package/src/infrastructure/event-bus/index.ts +0 -2
  130. package/src/infrastructure/event-bus/types.ts +0 -22
  131. package/src/infrastructure/lock/decorators.ts +0 -67
  132. package/src/infrastructure/lock/index.ts +0 -2
  133. package/src/infrastructure/lock/lock-manager.ts +0 -33
  134. package/src/plugin/apps-registry.ts +0 -97
  135. package/src/plugin/index.ts +0 -5
  136. package/src/plugin/types.ts +0 -41
  137. package/src/ui/management/audit-log-page.tsx +0 -14
  138. package/src/ui/management/job-management.tsx +0 -308
  139. package/src/workflow/activity-timeline.tsx +0 -412
  140. package/src/workflow/approval-workflow.tsx +0 -31
  141. package/src/workflow/index.ts +0 -2
  142. /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.59",
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 "../infrastructure/cron/cron-manager";
1
+ import { SimpleCronJob } from "./simple-cron-job";
2
2
 
3
3
  import type {
4
4
  CronJob,
5
5
  CronJobManager,
6
6
  CronJobOptions,
7
- } from "../infrastructure/cron/types";
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 `infrastructure/cron` chỉ hẹn giờ trong RAM: restart là
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 "../infrastructure/cron/cron-manager";
19
+ } from "./simple-cron-job";
20
20
  export type {
21
21
  CronJob,
22
22
  CronJobManager,
23
23
  CronJobOptions,
24
- } from "../infrastructure/cron/types";
24
+ } from "./types";
@@ -3,7 +3,7 @@ import type {
3
3
  CronJobManager as ICronJobManager,
4
4
  CronJobOptions,
5
5
  } from "./types";
6
- import { createLogger } from "../logger";
6
+ import { createLogger } from "../infrastructure/logger";
7
7
 
8
8
  const logger = createLogger("CronJobManager");
9
9
 
@@ -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
+ }