@gadmin2n/schematics 0.0.94 → 0.0.95

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.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * gadmin2 顶级"系统管理"page 的 code,跨实例项目可配。
3
+ *
4
+ * 背景:
5
+ * - gadmin2 模板默认惯例是 'admin'(其他实例项目走默认)
6
+ * - ERP 是历史特例,DB 里早期初始化为 'administration',所有环境都已存在
7
+ *
8
+ * 通过环境变量 `ADMIN_PAGE_CODE` 覆盖默认值,避免在 seed 代码里硬编码。
9
+ * ERP 在 server/.env.local 及各部署环境里设置 `ADMIN_PAGE_CODE=administration`。
10
+ *
11
+ * seed 脚本通过 `dotenv -e .env.local -e .env -- ts-node ...` 执行,
12
+ * 因此 process.env 已自动加载相应配置。
13
+ */
14
+ export const ADMIN_PAGE_CODE = process.env.ADMIN_PAGE_CODE ?? 'admin';
@@ -13,17 +13,29 @@ export const FULL_ACTIONS = ['createAny', 'readAny', 'updateAny', 'deleteAny'];
13
13
 
14
14
  /**
15
15
  * Upsert a Resource row.
16
- * update: {} preserves any manual edits on re-run.
16
+ *
17
+ * 历史数据中部分 t_resource 行存在 code/name 不一致(如 code='businesstrip',
18
+ * name='businessTrip')—— 这是早期 seed 用过 toLowerCase 留下的痕迹。`code` 和
19
+ * `name` 都是 @unique,简单按 `code` 做 upsert 会在 create 分支撞 name 唯一约束
20
+ * (P2002)。
21
+ *
22
+ * 因此先 findFirst by `code OR name`,命中即视为同一行返回;找不到才 create。
23
+ * 这样无论 fresh DB 还是带历史数据的 DB 都幂等。
24
+ *
25
+ * 不更新已存在行的 code/name/description 字段,避免改动正在生效的 RBAC 关键值
26
+ * (nest-access-control 通过 Resource.name 匹配 @UseRoles({ resource }))。
17
27
  */
18
28
  export async function upsertResource(
19
29
  prisma: PrismaClient,
20
30
  code: string,
21
31
  description = `${code} resource`,
22
32
  ) {
23
- return prisma.resource.upsert({
24
- where: { code },
25
- update: {},
26
- create: { code, name: code, description, isCommonResource: false },
33
+ const existing = await prisma.resource.findFirst({
34
+ where: { OR: [{ code }, { name: code }] },
35
+ });
36
+ if (existing) return existing;
37
+ return prisma.resource.create({
38
+ data: { code, name: code, description, isCommonResource: false },
27
39
  });
28
40
  }
29
41
 
@@ -0,0 +1,89 @@
1
+ /**
2
+ * resync-sequences.ts —— 把 PostgreSQL autoincrement 序列推到 MAX(id)
3
+ *
4
+ * 用途:seed 启动前调用一次,规避「序列落后于真实最大 id」造成的 P2002 on `id`。
5
+ *
6
+ * 触发场景:
7
+ * - 用 pg_dump --data-only 导入数据但没带序列状态
8
+ * - 灌库脚本用显式 id 做 INSERT(绕过 nextval)
9
+ * - 早期 seed 用过显式 id,后来切回 autoincrement
10
+ *
11
+ * 工作方式:
12
+ * 1) 通过 pg_class / pg_attribute 元数据,找出 current_schema() 下所有
13
+ * 绑定了序列的列(pg_get_serial_sequence 非 NULL)。
14
+ * 2) 对每列执行一条 SQL:取 MAX(col) 与序列 last_value,落后才 setval。
15
+ * 正常状态下是纯读,零写。
16
+ *
17
+ * 安全性:
18
+ * - 元数据来自 pg_catalog(DB 自身),不接受任何用户输入。
19
+ * - 标识符通过 pg_get_serial_sequence + format('%I') 来源,已被 PG 自身
20
+ * 按需引号化,可直接拼接进 SQL。
21
+ * - 不跨 schema:只处理 current_schema()(即连接串里 ?schema=xxx 指向的那个)。
22
+ */
23
+
24
+ import { PrismaClient } from '@prisma/client';
25
+
26
+ interface SeqRow {
27
+ table_name: string;
28
+ column_name: string;
29
+ seq_name: string; // 形如 "public"."t_page_id_seq",已被 PG 引号化
30
+ }
31
+
32
+ interface BumpResult {
33
+ bumped: boolean;
34
+ from_v: bigint;
35
+ to_v: bigint;
36
+ }
37
+
38
+ export async function resyncSequences(prisma: PrismaClient): Promise<void> {
39
+ const seqs = await prisma.$queryRaw<SeqRow[]>`
40
+ SELECT
41
+ c.relname AS table_name,
42
+ a.attname AS column_name,
43
+ pg_get_serial_sequence(format('%I.%I', n.nspname, c.relname), a.attname) AS seq_name
44
+ FROM pg_class c
45
+ JOIN pg_namespace n ON n.oid = c.relnamespace
46
+ JOIN pg_attribute a ON a.attrelid = c.oid
47
+ WHERE c.relkind = 'r'
48
+ AND n.nspname = current_schema()
49
+ AND a.attnum > 0
50
+ AND NOT a.attisdropped
51
+ AND pg_get_serial_sequence(format('%I.%I', n.nspname, c.relname), a.attname) IS NOT NULL
52
+ ORDER BY c.relname, a.attname
53
+ `;
54
+
55
+ let bumped = 0;
56
+ for (const s of seqs) {
57
+ // 单条 SQL 完成「比较 + 落后才 setval」,对健康序列是纯读:
58
+ // - cur: 序列当前 last_value
59
+ // - m : 表里 column 的 MAX 值
60
+ // - 仅当 m > cur 时执行 setval(seq, m),否则不写
61
+ const result = await prisma.$queryRawUnsafe<BumpResult[]>(`
62
+ WITH cur AS (SELECT last_value AS v FROM ${s.seq_name}),
63
+ m AS (SELECT COALESCE(MAX("${
64
+ s.column_name
65
+ }"), 0)::bigint AS v FROM "${s.table_name}")
66
+ SELECT
67
+ (m.v > cur.v) AS bumped,
68
+ cur.v AS from_v,
69
+ CASE WHEN m.v > cur.v
70
+ THEN setval('${s.seq_name.replace(/'/g, "''")}', m.v)
71
+ ELSE cur.v
72
+ END AS to_v
73
+ FROM cur, m
74
+ `);
75
+ const r = result[0];
76
+ if (r?.bumped) {
77
+ bumped++;
78
+ console.log(
79
+ `[resync-seq] ${s.table_name}.${s.column_name}: ${r.from_v} → ${r.to_v}`,
80
+ );
81
+ }
82
+ }
83
+
84
+ console.log(
85
+ bumped > 0
86
+ ? `[resync-seq] Bumped ${bumped} stale sequence(s) (of ${seqs.length} checked).`
87
+ : `[resync-seq] All ${seqs.length} sequences healthy.`,
88
+ );
89
+ }
@@ -1,4 +1,5 @@
1
1
  import { PrismaClient } from '@prisma/client';
2
+ import { ADMIN_PAGE_CODE } from '../scripts/lib/admin-code';
2
3
  import {
3
4
  bindResourceToPage,
4
5
  getPageResourceIds,
@@ -9,15 +10,15 @@ import {
9
10
  /**
10
11
  * Agenda 模块 seed
11
12
  *
12
- * 职责:agenda page (admin 子节点) + agenda resource + 自家 SYSTEM_ADMIN 授权。
13
- * 前置:seedBootstrap 已建好 admin 与 SYSTEM_ADMIN。
13
+ * 职责:agenda page (顶级 admin/administration 子节点) + agenda resource + 自家 SYSTEM_ADMIN 授权。
14
+ * 前置:seedBootstrap 已建好 顶级 page 与 SYSTEM_ADMIN。
14
15
  */
15
16
  export async function seedAgendaModule(prisma: PrismaClient): Promise<void> {
16
17
  const role = await prisma.role.findUniqueOrThrow({
17
18
  where: { name: 'SYSTEM_ADMIN' },
18
19
  });
19
20
  const admin = await prisma.page.findUniqueOrThrow({
20
- where: { code: 'admin' },
21
+ where: { code: ADMIN_PAGE_CODE },
21
22
  });
22
23
 
23
24
  const page = await upsertPage(prisma, {
@@ -1,4 +1,5 @@
1
1
  import { PrismaClient } from '@prisma/client';
2
+ import { ADMIN_PAGE_CODE } from '../scripts/lib/admin-code';
2
3
  import {
3
4
  bindResourceToPage,
4
5
  getPageResourceIds,
@@ -9,16 +10,16 @@ import {
9
10
  /**
10
11
  * Audit 模块 seed
11
12
  *
12
- * 职责:audits page (admin 子节点) + audit resource + 自家 SYSTEM_ADMIN 授权。
13
+ * 职责:audits page (顶级 admin/administration 子节点) + audit resource + 自家 SYSTEM_ADMIN 授权。
13
14
  * 注意:page.code 是 'audits',但 resource.code 是 'audit'(与 backend module 一致)。
14
- * 前置:seedBootstrap 已建好 admin 与 SYSTEM_ADMIN。
15
+ * 前置:seedBootstrap 已建好 顶级 page 与 SYSTEM_ADMIN。
15
16
  */
16
17
  export async function seedAuditModule(prisma: PrismaClient): Promise<void> {
17
18
  const role = await prisma.role.findUniqueOrThrow({
18
19
  where: { name: 'SYSTEM_ADMIN' },
19
20
  });
20
21
  const admin = await prisma.page.findUniqueOrThrow({
21
- where: { code: 'admin' },
22
+ where: { code: ADMIN_PAGE_CODE },
22
23
  });
23
24
 
24
25
  const page = await upsertPage(prisma, {
@@ -1,15 +1,17 @@
1
1
  import { PrismaClient } from '@prisma/client';
2
+ import { ADMIN_PAGE_CODE } from '../scripts/lib/admin-code';
2
3
  import { grantRoleAccess, upsertPage } from '../scripts/lib/page-helpers';
3
4
 
4
5
  /**
5
6
  * Bootstrap seed — 全模块共享的最小公共集
6
7
  *
7
8
  * 任何模块 seed 都假设以下 3 个对象已存在:
8
- * 1. SYSTEM_ADMIN 角色 —— 各模块 grant SYSTEM_ADMIN 时通过 name 查找
9
- * 2. admin 顶级 page —— 各业务模块 page 的 parentId
10
- * 3. dataMngt 分组 page —— 自动生成 CRUD 页面(data-mngt.seed.ts)的 parentId
9
+ * 1. SYSTEM_ADMIN 角色 —— 各模块 grant SYSTEM_ADMIN 时通过 name 查找
10
+ * 2. ADMIN_PAGE_CODE 顶级 page —— 各业务模块 page 的 parentId
11
+ * (默认 'admin';ERP 通过环境变量 ADMIN_PAGE_CODE=administration 覆盖)
12
+ * 3. dataMngt 分组 page —— 自动生成 CRUD 页面(data-mngt.seed.ts)的 parentId
11
13
  *
12
- * 同时把 SYSTEM_ADMIN 对 admin / dataMngt 这两个分组节点的 RolePages 也建好,
14
+ * 同时把 SYSTEM_ADMIN 对 顶级 / dataMngt 这两个分组节点的 RolePages 也建好,
13
15
  * 否则它们的子菜单父节点不会展开。
14
16
  *
15
17
  * 必须在所有模块 seed 之前运行(由 seed/index.ts 编排)。
@@ -27,14 +29,20 @@ export async function seedBootstrap(prisma: PrismaClient): Promise<void> {
27
29
  });
28
30
  console.log(`[bootstrap] Role: SYSTEM_ADMIN (id=${systemAdminRole.id})`);
29
31
 
30
- // ── admin 顶级分组 page ────────────────────────────────────────────────────
31
- const adminPage = await upsertPage(prisma, {
32
- code: 'admin',
33
- name: 'admin',
34
- zhName: '系统管理',
35
- enName: 'System Mgnt',
36
- sortOrder: 1000,
37
- });
32
+ // ── 顶级分组 page(admin 或 administration,由环境变量决定) ──────────────
33
+ // updateData 显式同步文案/排序,确保已存在的 administration 行(ERP 历史数据)
34
+ // 也能拿到正确的 zhName='系统管理'、enName='System Mgnt'、sortOrder=1000。
35
+ const adminPage = await upsertPage(
36
+ prisma,
37
+ {
38
+ code: ADMIN_PAGE_CODE,
39
+ name: ADMIN_PAGE_CODE,
40
+ zhName: '系统管理',
41
+ enName: 'System Mgnt',
42
+ sortOrder: 1000,
43
+ },
44
+ { zhName: '系统管理', enName: 'System Mgnt', sortOrder: 1000 },
45
+ );
38
46
  console.log(`[bootstrap] Page: ${adminPage.code} (id=${adminPage.id})`);
39
47
 
40
48
  // ── dataMngt 分组 page(自动 CRUD 菜单容器) ──────────────────────────────
@@ -52,5 +60,7 @@ export async function seedBootstrap(prisma: PrismaClient): Promise<void> {
52
60
  // 分组节点本身不绑定 resource,只为让侧边栏父节点对 SYSTEM_ADMIN 可见
53
61
  await grantRoleAccess(prisma, systemAdminRole.id, adminPage.id, []);
54
62
  await grantRoleAccess(prisma, systemAdminRole.id, dataMngtPage.id, []);
55
- console.log(`[bootstrap] RolePages: SYSTEM_ADMIN → admin, dataMngt`);
63
+ console.log(
64
+ `[bootstrap] RolePages: SYSTEM_ADMIN → ${adminPage.code}, dataMngt`,
65
+ );
56
66
  }
@@ -1,4 +1,5 @@
1
1
  import { PrismaClient } from '@prisma/client';
2
+ import { ADMIN_PAGE_CODE } from '../scripts/lib/admin-code';
2
3
  import {
3
4
  bindResourceToPage,
4
5
  getPageResourceIds,
@@ -9,15 +10,15 @@ import {
9
10
  /**
10
11
  * Canvas 模块 seed
11
12
  *
12
- * 职责:canvas page (admin 子节点) + canvas resource + 自家 SYSTEM_ADMIN 授权。
13
- * 前置:seedBootstrap 已建好 admin 与 SYSTEM_ADMIN。
13
+ * 职责:canvas page (顶级 admin/administration 子节点) + canvas resource + 自家 SYSTEM_ADMIN 授权。
14
+ * 前置:seedBootstrap 已建好 顶级 page 与 SYSTEM_ADMIN。
14
15
  */
15
16
  export async function seedCanvasModule(prisma: PrismaClient): Promise<void> {
16
17
  const role = await prisma.role.findUniqueOrThrow({
17
18
  where: { name: 'SYSTEM_ADMIN' },
18
19
  });
19
20
  const admin = await prisma.page.findUniqueOrThrow({
20
- where: { code: 'admin' },
21
+ where: { code: ADMIN_PAGE_CODE },
21
22
  });
22
23
 
23
24
  const page = await upsertPage(prisma, {
@@ -1,4 +1,5 @@
1
1
  import { PrismaClient } from '@prisma/client';
2
+ import { ADMIN_PAGE_CODE } from '../scripts/lib/admin-code';
2
3
  import {
3
4
  bindResourceToPage,
4
5
  getPageResourceIds,
@@ -9,15 +10,15 @@ import {
9
10
  /**
10
11
  * Game 模块 seed
11
12
  *
12
- * 职责:game page (admin 子节点) + game resource + 自家 SYSTEM_ADMIN 授权。
13
- * 前置:seedBootstrap 已建好 admin 与 SYSTEM_ADMIN。
13
+ * 职责:game page (顶级 admin/administration 子节点) + game resource + 自家 SYSTEM_ADMIN 授权。
14
+ * 前置:seedBootstrap 已建好 顶级 page 与 SYSTEM_ADMIN。
14
15
  */
15
16
  export async function seedGameModule(prisma: PrismaClient): Promise<void> {
16
17
  const role = await prisma.role.findUniqueOrThrow({
17
18
  where: { name: 'SYSTEM_ADMIN' },
18
19
  });
19
20
  const admin = await prisma.page.findUniqueOrThrow({
20
- where: { code: 'admin' },
21
+ where: { code: ADMIN_PAGE_CODE },
21
22
  });
22
23
 
23
24
  const page = await upsertPage(prisma, {
@@ -1,4 +1,5 @@
1
1
  import { PrismaClient } from '@prisma/client';
2
+ import { resyncSequences } from '../scripts/lib/resync-sequences';
2
3
  import { seedAgendaModule } from './agenda.seed';
3
4
  import { seedAuditModule } from './audit.seed';
4
5
  import { seedBootstrap } from './bootstrap';
@@ -15,6 +16,7 @@ const prisma = new PrismaClient();
15
16
  * Top-level seed orchestrator.
16
17
  *
17
18
  * 顺序:
19
+ * ⓪ resync-sequences —— 把 autoincrement 序列追到 MAX(id),规避 dump/import 留下的 P2002
18
20
  * ① bootstrap —— admin/dataMngt 根 page + SYSTEM_ADMIN role(全模块前置)
19
21
  * ② 各模块 seed —— 每个模块自管自家 page / resource / 授权 / 业务数据
20
22
  * ③ data-mngt 自动 CRUD —— 扫描 config/prisma/ 业务模型生成 page/resource/grants
@@ -23,6 +25,8 @@ const prisma = new PrismaClient();
23
25
  * 全部幂等,可重复运行。
24
26
  */
25
27
  export async function main() {
28
+ await resyncSequences(prisma);
29
+
26
30
  await prisma.user.createMany({ data: users, skipDuplicates: true });
27
31
 
28
32
  await seedBootstrap(prisma);
@@ -1,4 +1,5 @@
1
1
  import { PrismaClient } from '@prisma/client';
2
+ import { ADMIN_PAGE_CODE } from '../scripts/lib/admin-code';
2
3
  import {
3
4
  bindResourceToPage,
4
5
  getPageResourceIds,
@@ -10,13 +11,13 @@ import {
10
11
  * Permission 模块 seed
11
12
  *
12
13
  * 职责(仅本模块自家):
13
- * 1. permission 父分组 page (admin → permission)
14
+ * 1. permission 父分组 page (顶级 admin/administration → permission)
14
15
  * 2. 5 个 leaf page: permission_readme / user / role / page / resource
15
16
  * 3. 7 个 resource: user / role / rolePages / roleResource / page / pageResource / resource
16
17
  * 4. PageResource 绑定(page ↔ resource,多对多)
17
18
  * 5. SYSTEM_ADMIN 对上述全部 page + resource 的全量授权
18
19
  *
19
- * 前置:seedBootstrap 已建好 admin 顶级 page 和 SYSTEM_ADMIN role。
20
+ * 前置:seedBootstrap 已建好 顶级 page 和 SYSTEM_ADMIN role。
20
21
  *
21
22
  * 多 resource 共享同一 page 的情况(如 role page UI 同时调 role + rolePages + roleResource API)
22
23
  * 通过 resourceDefs[].pageCode 表达。
@@ -29,7 +30,7 @@ export async function seedPermissionModule(
29
30
  where: { name: 'SYSTEM_ADMIN' },
30
31
  });
31
32
  const adminPage = await prisma.page.findUniqueOrThrow({
32
- where: { code: 'admin' },
33
+ where: { code: ADMIN_PAGE_CODE },
33
34
  });
34
35
 
35
36
  // ── permission 父分组 ──────────────────────────────────────────────────────
@@ -72,7 +72,7 @@
72
72
  "lint-staged": "^13.0.0",
73
73
  "postcss": "^8.5.6",
74
74
  "postcss-nesting": "^14.0.0",
75
- "prettier": "^3.0.0",
75
+ "prettier": "^3.8.3",
76
76
  "typescript": "^5.4.2",
77
77
  "vite": "^6.2.0",
78
78
  "vite-tsconfig-paths": "^5.1.4"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gadmin2n/schematics",
3
- "version": "0.0.94",
3
+ "version": "0.0.95",
4
4
  "description": "Gadmin - modern, fast, powerful node.js web framework (@schematics)",
5
5
  "main": "dist/index.js",
6
6
  "files": [