@goplusvn/core 0.1.58 → 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.
Files changed (145) hide show
  1. package/PLATFORM.md +83 -0
  2. package/bin/goerp-init.mjs +141 -0
  3. package/package.json +4 -4
  4. package/src/branch-scope/__tests__/branch-scope.test.ts +288 -0
  5. package/src/branch-scope/context.ts +66 -0
  6. package/src/branch-scope/guard.ts +100 -0
  7. package/src/branch-scope/index.ts +42 -0
  8. package/src/branch-scope/scope.ts +149 -0
  9. package/src/branch-scope/types.ts +57 -0
  10. package/src/cron/db-cron-manager.ts +3 -3
  11. package/src/cron/index.ts +2 -2
  12. package/src/{infrastructure/cron/cron-manager.ts → cron/simple-cron-job.ts} +1 -1
  13. package/src/infrastructure/__tests__/architecture-verification.spec.ts +13 -97
  14. package/src/infrastructure/index.ts +4 -7
  15. package/src/ui/management/index.ts +3 -2
  16. package/templates/starter-app/.dockerignore +41 -0
  17. package/templates/starter-app/.env.example +25 -0
  18. package/templates/starter-app/AGENTS.md +52 -0
  19. package/templates/starter-app/Dockerfile +74 -0
  20. package/templates/starter-app/README.md +141 -0
  21. package/templates/starter-app/gitignore +9 -0
  22. package/templates/starter-app/next.config.mjs +50 -0
  23. package/templates/starter-app/package.json +55 -0
  24. package/templates/starter-app/postcss.config.mjs +5 -0
  25. package/templates/starter-app/prisma/migrations/20260801000000_init/migration.sql +504 -0
  26. package/templates/starter-app/prisma/migrations/20260801091814_goerp_audit_logs_0001_init/migration.sql +33 -0
  27. package/templates/starter-app/prisma/migrations/20260801091815_goerp_background_tasks_0001_init/migration.sql +23 -0
  28. package/templates/starter-app/prisma/migrations/20260801091816_goerp_error_logs_0001_init/migration.sql +32 -0
  29. package/templates/starter-app/prisma/migrations/20260801091817_goerp_notifications_0001_init/migration.sql +59 -0
  30. package/templates/starter-app/prisma/migrations/20260801091818_goerp_system_jobs_0001_init/migration.sql +47 -0
  31. package/templates/starter-app/prisma/schema/auth.prisma +87 -0
  32. package/templates/starter-app/prisma/schema/domain.prisma +24 -0
  33. package/templates/starter-app/prisma/schema/goerp-audit-logs.prisma +23 -0
  34. package/templates/starter-app/prisma/schema/goerp-background-tasks.prisma +25 -0
  35. package/templates/starter-app/prisma/schema/goerp-error-logs.prisma +31 -0
  36. package/templates/starter-app/prisma/schema/goerp-notifications.prisma +49 -0
  37. package/templates/starter-app/prisma/schema/goerp-system-jobs.prisma +45 -0
  38. package/templates/starter-app/prisma/schema/organization.prisma +31 -0
  39. package/templates/starter-app/prisma/schema/rbac.prisma +100 -0
  40. package/templates/starter-app/prisma/schema/schema.prisma +8 -0
  41. package/templates/starter-app/prisma/schema/system.prisma +22 -0
  42. package/templates/starter-app/prisma/seed.ts +127 -0
  43. package/templates/starter-app/prisma.config.ts +20 -0
  44. package/templates/starter-app/public/.gitkeep +2 -0
  45. package/templates/starter-app/scripts/rbac-sync.ts +235 -0
  46. package/templates/starter-app/src/__tests__/architecture.test.ts +151 -0
  47. package/templates/starter-app/src/app/[lang]/(main)/admin/system/audit/page.tsx +24 -0
  48. package/templates/starter-app/src/app/[lang]/(main)/admin/system/error-logs/page.tsx +21 -0
  49. package/templates/starter-app/src/app/[lang]/(main)/admin/system/jobs/page.tsx +18 -0
  50. package/templates/starter-app/src/app/[lang]/(main)/admin/system/settings/page.tsx +22 -0
  51. package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +41 -0
  52. package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +13 -0
  53. package/templates/starter-app/src/app/[lang]/(main)/notifications/page.tsx +13 -0
  54. package/templates/starter-app/src/app/[lang]/(main)/page.tsx +130 -0
  55. package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +60 -0
  56. package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +41 -0
  57. package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +48 -0
  58. package/templates/starter-app/src/app/[lang]/(main)/tasks/page.tsx +13 -0
  59. package/templates/starter-app/src/app/[lang]/(plain)/layout.tsx +10 -0
  60. package/templates/starter-app/src/app/[lang]/(plain)/sign-in/page.tsx +39 -0
  61. package/templates/starter-app/src/app/[lang]/layout.tsx +21 -0
  62. package/templates/starter-app/src/app/api/admin/system/audit/route.ts +60 -0
  63. package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +39 -0
  64. package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +96 -0
  65. package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +17 -0
  66. package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +19 -0
  67. package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +20 -0
  68. package/templates/starter-app/src/app/api/admin/system/settings/route.ts +49 -0
  69. package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +28 -0
  70. package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +24 -0
  71. package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +23 -0
  72. package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +13 -0
  73. package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +13 -0
  74. package/templates/starter-app/src/app/api/crud/[entity]/route.ts +14 -0
  75. package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +23 -0
  76. package/templates/starter-app/src/app/api/error-logs/route.ts +124 -0
  77. package/templates/starter-app/src/app/api/files/[...key]/route.ts +25 -0
  78. package/templates/starter-app/src/app/api/notifications/read/route.ts +29 -0
  79. package/templates/starter-app/src/app/api/notifications/route.ts +26 -0
  80. package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +14 -0
  81. package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +11 -0
  82. package/templates/starter-app/src/app/api/roles/[id]/route.ts +14 -0
  83. package/templates/starter-app/src/app/api/roles/route.ts +18 -0
  84. package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +52 -0
  85. package/templates/starter-app/src/app/api/tasks/route.ts +37 -0
  86. package/templates/starter-app/src/app/api/upload/route.ts +15 -0
  87. package/templates/starter-app/src/app/globals.css +15 -0
  88. package/templates/starter-app/src/app/layout.tsx +16 -0
  89. package/templates/starter-app/src/app/page.tsx +8 -0
  90. package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +30 -0
  91. package/templates/starter-app/src/configs/entities/department.config.ts +24 -0
  92. package/templates/starter-app/src/configs/entities/index.ts +13 -0
  93. package/templates/starter-app/src/configs/i18n.ts +12 -0
  94. package/templates/starter-app/src/configs/permissions/index.ts +45 -0
  95. package/templates/starter-app/src/configs/permissions/master-data.permissions.ts +31 -0
  96. package/templates/starter-app/src/configs/permissions/system.permissions.ts +131 -0
  97. package/templates/starter-app/src/configs/permissions/types.ts +63 -0
  98. package/templates/starter-app/src/configs/tenant.ts +18 -0
  99. package/templates/starter-app/src/data/dictionary.ts +8 -0
  100. package/templates/starter-app/src/data/navigations.ts +61 -0
  101. package/templates/starter-app/src/instrumentation.ts +105 -0
  102. package/templates/starter-app/src/lib/api-handler.ts +157 -0
  103. package/templates/starter-app/src/lib/auth-client.ts +57 -0
  104. package/templates/starter-app/src/lib/auth.ts +62 -0
  105. package/templates/starter-app/src/lib/better-auth.ts +107 -0
  106. package/templates/starter-app/src/lib/branch-scope.ts +53 -0
  107. package/templates/starter-app/src/lib/cron/db-cron-manager.ts +42 -0
  108. package/templates/starter-app/src/lib/crud/index.ts +13 -0
  109. package/templates/starter-app/src/lib/errors/app-error.ts +2 -0
  110. package/templates/starter-app/src/lib/errors/error-handler.ts +5 -0
  111. package/templates/starter-app/src/lib/errors/log-server-error.ts +15 -0
  112. package/templates/starter-app/src/lib/errors/server-error.ts +11 -0
  113. package/templates/starter-app/src/lib/logger.ts +30 -0
  114. package/templates/starter-app/src/lib/page-guard.ts +35 -0
  115. package/templates/starter-app/src/lib/prisma.ts +80 -0
  116. package/templates/starter-app/src/lib/rbac/access.ts +87 -0
  117. package/templates/starter-app/src/lib/storage.ts +28 -0
  118. package/templates/starter-app/src/providers/index.tsx +54 -0
  119. package/templates/starter-app/src/providers/mode-provider.tsx +31 -0
  120. package/templates/starter-app/src/providers/theme-provider.tsx +21 -0
  121. package/templates/starter-app/src/proxy.ts +45 -0
  122. package/templates/starter-app/src/server/services/notification-service.ts +31 -0
  123. package/templates/starter-app/src/server/services/system-config-service.ts +163 -0
  124. package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +58 -0
  125. package/templates/starter-app/src/server/tasks/index.ts +16 -0
  126. package/templates/starter-app/src/server/tasks/task-runner.ts +40 -0
  127. package/templates/starter-app/src/types/session.ts +29 -0
  128. package/templates/starter-app/tsconfig.json +47 -0
  129. package/templates/starter-app/vitest.config.ts +17 -0
  130. package/src/infrastructure/cron/index.ts +0 -6
  131. package/src/infrastructure/event-bus/event-bus.ts +0 -145
  132. package/src/infrastructure/event-bus/index.ts +0 -2
  133. package/src/infrastructure/event-bus/types.ts +0 -22
  134. package/src/infrastructure/lock/decorators.ts +0 -67
  135. package/src/infrastructure/lock/index.ts +0 -2
  136. package/src/infrastructure/lock/lock-manager.ts +0 -33
  137. package/src/plugin/apps-registry.ts +0 -97
  138. package/src/plugin/index.ts +0 -5
  139. package/src/plugin/types.ts +0 -41
  140. package/src/ui/management/audit-log-page.tsx +0 -14
  141. package/src/ui/management/job-management.tsx +0 -308
  142. package/src/workflow/activity-timeline.tsx +0 -412
  143. package/src/workflow/approval-workflow.tsx +0 -31
  144. package/src/workflow/index.ts +0 -2
  145. /package/src/{infrastructure/cron → cron}/types.ts +0 -0
@@ -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"]
@@ -0,0 +1,141 @@
1
+ # GoERP starter app
2
+
3
+ Bộ khung **chạy được ngay** cho một ứng dụng ERP trên `@goerp/core`: Next.js 16 +
4
+ Prisma 7 + Better Auth. Cài, seed, `pnpm dev` là có đăng nhập thật, phân quyền,
5
+ CRUD, nhật ký lỗi, nhật ký hoạt động, thông báo, tác vụ nền và tác vụ định kỳ —
6
+ tất cả lấy từ core, không phải mã copy.
7
+
8
+ Đây là **điểm xuất phát** của một app mới, không phải bản demo để đọc.
9
+
10
+ ---
11
+
12
+ ## Bắt đầu
13
+
14
+ ```bash
15
+ # tạo app mới (không cần clone goerp-core):
16
+ npx -p @goplusvn/core goerp-init my-app --port 3010 && cd my-app
17
+ # trong repo goerp-core thì dùng: pnpm init-app my-app --dir ../my-app
18
+
19
+ pnpm install # postinstall tự chạy `prisma generate`
20
+ # `.env` đã sinh sẵn lúc init (BETTER_AUTH_SECRET ngẫu nhiên) — sửa DATABASE_URL
21
+ # cho trỏ đúng Postgres của bạn
22
+
23
+ pnpm prisma:deploy # dựng toàn bộ bảng (migration `_init` có sẵn)
24
+ pnpm seed # vai trò + tài khoản admin + dữ liệu mẫu
25
+ pnpm rbac-sync # nạp resource/action/quyền từ registry
26
+ pnpm dev # http://localhost:3010
27
+ ```
28
+
29
+ > ⚠️ **Thứ tự `seed` → `rbac-sync` không đảo được.** `rbac-sync` chỉ cấp quyền
30
+ > mặc định cho resource nó vừa TẠO MỚI (để không đè chỉnh sửa của quản trị
31
+ > viên trên production). Chạy nó trước `seed` thì vai trò chưa tồn tại, grant
32
+ > bị bỏ, và lần chạy sau resource không còn "mới" nên im lặng bỏ qua mãi —
33
+ > app dựng xong mà không ai vào được màn hình nào. Lỡ đảo rồi thì chữa bằng
34
+ > `pnpm rbac-sync --force-grants`.
35
+ >
36
+ > ⚠️ Đừng dùng `prisma migrate dev` / `db push` / `migrate reset` — chúng có thể
37
+ > reset sạch DB khi phát hiện drift. Template đã có sẵn migration khởi tạo nên
38
+ > `pnpm prisma:deploy` là lệnh duy nhất cần, cả lúc dựng lẫn về sau.
39
+
40
+ Chỉ muốn xem giao diện, chưa có DB? Đặt `BYPASS_AUTH=1` trong `.env` — app boot
41
+ với phiên admin giả, bỏ qua đăng nhập. Màn hình nào đọc DB thì vẫn cần Postgres.
42
+
43
+ ---
44
+
45
+ ## Có sẵn những gì
46
+
47
+ | Đường dẫn | Nội dung |
48
+ | --- | --- |
49
+ | `/sign-in` | Đăng nhập thật (Better Auth, mật khẩu bcrypt trên bảng `Account`). |
50
+ | `/vi` | Trang chủ mẫu. |
51
+ | `/vi/crud/departments` | CRUD đầy đủ (bảng + form + lọc + nhập/xuất) sinh từ **một** `EntityConfig`. |
52
+ | `/vi/roles` | Vai trò + ma trận quyền. Trang của core, dữ liệu do app nạp. |
53
+ | `/vi/admin/system/settings` | Cấu hình hệ thống (key-value + nhóm cấu hình có kiểu). |
54
+ | `/vi/admin/system/audit` | Nhật ký thay đổi dữ liệu (extension Prisma tự ghi). |
55
+ | `/vi/admin/system/error-logs` | Nhật ký lỗi server kèm stack. |
56
+ | `/vi/admin/system/jobs` | Tác vụ định kỳ: bật/tắt, chạy tay, lịch sử chạy. |
57
+ | `/vi/notifications` | Hộp thư của người dùng + chuông trên thanh tiêu đề. |
58
+ | `/vi/tasks` | Tác vụ nền của người dùng + tải file kết quả. |
59
+
60
+ ---
61
+
62
+ ## Bản đồ mã nguồn
63
+
64
+ | File | Vai trò |
65
+ | --- | --- |
66
+ | `src/proxy.ts` | **Cổng xác thực default-deny**. Quên gác một route mới cũng không thành lỗ hổng. |
67
+ | `src/lib/api-handler.ts` | **Cổng của tầng API**: session → quyền → audit context → ghi error_logs. Mọi route đi qua đây. |
68
+ | `src/lib/page-guard.ts` | `requirePageAccess(lang, resource)` — gác quyền ở tầng TRANG. |
69
+ | `src/lib/prisma.ts` | Prisma client DUY NHẤT + audit extension + nối dây `SettingsService`. |
70
+ | `src/lib/auth.ts` / `src/lib/better-auth.ts` | `getSession()` — nguồn phiên duy nhất phía server; cấu hình Better Auth. |
71
+ | `src/lib/rbac/access.ts` | Nạp vai trò + quyền + chi nhánh của user trong MỘT truy vấn cho session. |
72
+ | `src/configs/permissions/` | **Permission Registry** — dữ liệu thuần, mỗi phân hệ một file. `pnpm rbac-sync` đồng bộ xuống DB. |
73
+ | `src/configs/entities/` | `EntityConfig` cho CRUD engine. Thêm entity = thêm một file + một dòng registry. |
74
+ | `src/lib/crud/` + `src/app/api/crud/[entity]/` | Backend CRUD generic của core; app chỉ tiêm `prisma` + map entity → model. |
75
+ | `src/data/navigations.ts` | Menu sidebar. `resource` của mỗi mục nối với RBAC để tự ẩn khi thiếu quyền. |
76
+ | `src/instrumentation.ts` | Boot: khởi động cron, thu hồi tác vụ treo, `onRequestError` → error_logs. |
77
+ | `src/server/tasks/` | Tác vụ nền: `configureTaskRunner` + handler mẫu (xuất CSV theo trang). |
78
+ | `src/server/services/` | Dịch vụ dùng chung cho route (thông báo, cấu hình hệ thống). |
79
+ | `src/__tests__/architecture.test.ts` | **Ratchet** kiến trúc — mỗi kiểm tra là một lỗi im lặng đã từng mất thời gian. |
80
+
81
+ ---
82
+
83
+ ## Thêm một entity
84
+
85
+ 1. Thêm model vào `prisma/schema/domain.prisma` → viết migration → `pnpm prisma:deploy`.
86
+ 2. Tạo `src/configs/entities/<thing>.config.ts`, đăng ký ở `index.ts`, map khoá số
87
+ nhiều → model Prisma trong `src/lib/crud/index.ts`.
88
+ 3. Khai resource trong `src/configs/permissions/<feature>.permissions.ts` →
89
+ `pnpm rbac-sync`.
90
+ 4. Thêm mục menu trỏ `/crud/<key>`.
91
+
92
+ Xong. Không viết route, không viết engine.
93
+
94
+ ---
95
+
96
+ ## Lấy thêm tính năng từ core
97
+
98
+ ```bash
99
+ pnpm goerp-features list # các feature core có sẵn
100
+ pnpm goerp-features add <name> # chép schema + migration vào app
101
+ ```
102
+
103
+ Feature đã cắm sẵn trong template: `audit-logs`, `background-tasks`,
104
+ `error-logs`, `notifications`, `system-jobs`.
105
+
106
+ ---
107
+
108
+ ## Lệnh hay dùng
109
+
110
+ | Lệnh | Việc |
111
+ | --- | --- |
112
+ | `pnpm dev` | Chạy dev (cổng 3010). |
113
+ | `pnpm type-check` | `tsc --noEmit`. |
114
+ | `pnpm test` | Ratchet kiến trúc. Chạy trước mỗi lần commit. |
115
+ | `pnpm rbac-sync` | Đồng bộ Permission Registry → DB (idempotent, chạy lại vô hại). |
116
+ | `pnpm prisma:deploy` | Áp migration đang chờ. **Cách duy nhất** đụng vào DB có dữ liệu thật. |
117
+ | `pnpm seed` | Vai trò + tài khoản admin + dữ liệu mẫu. Chạy TRƯỚC `rbac-sync`. |
118
+
119
+ ---
120
+
121
+ ## Triển khai
122
+
123
+ `Dockerfile` dựng image standalone (node:24-alpine, TZ Việt Nam). Di trú DB
124
+ **không** chạy trong container: chạy `pnpm prisma migrate deploy` từ CI trước khi
125
+ đổi sang bản mới, để migration hỏng thì dừng ngay ở đó thay vì làm container
126
+ khởi động lại vô hạn.
127
+
128
+ Cần `pnpm-lock.yaml` trong build context (`pnpm install` sinh ra ở lần cài đầu).
129
+
130
+ ---
131
+
132
+ ## Bẫy đã gặp
133
+
134
+ - Sau khi đổi schema → **restart `next dev`**. Đừng `rm -rf .next` khi dev đang chạy.
135
+ - Mục menu trỏ tới resource chưa khai trong registry sẽ **ẩn vĩnh viễn với mọi
136
+ người, kể cả admin**, không có lỗi nào hiện ra. `pnpm test` bắt trường hợp này.
137
+ - Biểu thức cron của core thực chất là `setInterval` tính TỪ LÚC BOOT — `0 1 * * *`
138
+ nghĩa là "mỗi 24 giờ kể từ khi khởi động", không phải "1 giờ sáng". Muốn đúng
139
+ giờ thì hẹn theo giờ rồi chặn bằng `isLocalHour()` (xem `src/instrumentation.ts`).
140
+ - Viết thêm đường ghi cấu hình thì phải `revalidateTag("system-settings", "max")`,
141
+ nếu không giao diện đọc giá trị cũ từ cache.
@@ -0,0 +1,9 @@
1
+ node_modules/
2
+ .next/
3
+ out/
4
+ .env
5
+ .env*.local
6
+ *.tsbuildinfo
7
+
8
+ # Kho tập tin chế độ đĩa local (@goerp/core/storage) — dữ liệu, không phải mã nguồn.
9
+ /storage
@@ -0,0 +1,50 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+ import { fileURLToPath } from "url"
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
6
+
7
+ /**
8
+ * Khi lập trình core song song với app (`pnpm core:link`) hoặc khi app nằm
9
+ * trong một pnpm workspace, node_modules/@goerp/core là symlink trỏ RA NGOÀI
10
+ * thư mục app và turbopack báo "Symlink points out of the filesystem root".
11
+ *
12
+ * Nới root lên thư mục cha ĐẦU TIÊN chứa cả app lẫn core thật — đi ngược lên
13
+ * từng cấp chứ đừng đoán cứng ".." : store pnpm của workspace có thể nằm cách
14
+ * app 2–3 cấp, và chọn nhầm cấp thì Next không thấy được cả gói `next`.
15
+ *
16
+ * App độc lập (core cài từ npm, realpath nằm trong node_modules/.pnpm của
17
+ * chính app) giữ nguyên __dirname để `output: standalone` emit server.js đúng chỗ.
18
+ */
19
+ const workspaceRoot = (() => {
20
+ try {
21
+ const real = fs.realpathSync(path.join(__dirname, "node_modules/@goerp/core"))
22
+ if (real.startsWith(__dirname + path.sep)) return __dirname
23
+ let dir = path.dirname(__dirname)
24
+ while (dir !== path.dirname(dir)) {
25
+ if (real.startsWith(dir + path.sep)) return dir
26
+ dir = path.dirname(dir)
27
+ }
28
+ } catch {}
29
+ return __dirname
30
+ })()
31
+
32
+ /** @type {import('next').NextConfig} */
33
+ const nextConfig = {
34
+ // Core ship .tsx thô nên app phải transpile. Cần CẢ HAI tên: đường import là
35
+ // "@goerp/core" (giữ qua alias pnpm) còn package thật trên đĩa là "@goplusvn/core".
36
+ transpilePackages: ["@goerp/core", "@goplusvn/core"],
37
+
38
+ // Docker image gọn: chỉ .next/standalone + static. Xem Dockerfile.
39
+ output: "standalone",
40
+ turbopack: { root: workspaceRoot },
41
+ // Next đòi outputFileTracingRoot trùng turbopack.root — nới cùng nhau.
42
+ outputFileTracingRoot: workspaceRoot,
43
+
44
+ // Không khai redirects() cho "/:lang": "/vi" CHÍNH LÀ trang chủ
45
+ // (src/app/[lang]/(main)/page.tsx). Nếu app tách riêng "/vi/home" thì mới
46
+ // thêm redirect, và nhớ rằng redirects() được nướng lúc BUILD — giá trị phải
47
+ // truyền bằng build arg, không phải env lúc chạy.
48
+ }
49
+
50
+ export default nextConfig
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@goerp/starter-app",
3
+ "version": "0.2.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "next dev --turbopack --port 3010",
8
+ "build": "prisma generate && next build",
9
+ "postinstall": "prisma generate",
10
+ "start": "next start --port 3010",
11
+ "type-check": "tsc --noEmit",
12
+ "test": "vitest run",
13
+ "prisma:generate": "prisma generate",
14
+ "prisma:deploy": "prisma migrate deploy",
15
+ "prisma:studio": "prisma studio",
16
+ "seed": "tsx prisma/seed.ts",
17
+ "rbac-sync": "tsx scripts/rbac-sync.ts",
18
+ "goerp-features": "goerp-features"
19
+ },
20
+ "dependencies": {
21
+ "@goerp/core": "npm:@goplusvn/core@^0.1.59",
22
+ "@prisma/adapter-pg": "^7.0.0",
23
+ "@prisma/client": "^7.0.0",
24
+ "bcryptjs": "^3.0.3",
25
+ "better-auth": "1.6.23",
26
+ "lucide-react": "0.446.0",
27
+ "next": "16.0.10",
28
+ "next-themes": "^0.2.1",
29
+ "pg": "^8.16.3",
30
+ "react": "19.2.0",
31
+ "react-dom": "19.2.0",
32
+ "server-only": "^0.0.1",
33
+ "sonner": "2.0.2",
34
+ "swr": "^2.3.6",
35
+ "zod": "3.23.8"
36
+ },
37
+ "devDependencies": {
38
+ "@tailwindcss/postcss": "4.0.17",
39
+ "@vitejs/plugin-react": "^5.0.4",
40
+ "@testing-library/react": "^16.3.0",
41
+ "@types/bcryptjs": "^2.4.6",
42
+ "@types/node": "22.9.0",
43
+ "@types/pg": "^8.16.0",
44
+ "@types/react": "19.0.12",
45
+ "@types/react-dom": "19.0.4",
46
+ "dotenv": "^16.4.7",
47
+ "jsdom": "^27.4.0",
48
+ "prisma": "^7.0.0",
49
+ "tailwindcss": "4.1.3",
50
+ "tsx": "^4.7.1",
51
+ "tw-animate-css": "1.2.5",
52
+ "typescript": "5.8.3",
53
+ "vitest": "^4.0.16"
54
+ }
55
+ }
@@ -0,0 +1,5 @@
1
+ const config = {
2
+ plugins: ["@tailwindcss/postcss"],
3
+ }
4
+
5
+ export default config