背景:A4 需要字典三层合并、配置单一来源与阈值同源。\n改动:新增 catalog 模块(类型/条目/三层合并/编码不可改/快照工具);platform 新增 SystemConfigService/Controller、NamingService 与配置版本历史;roles/users/customers/customer-fields 改为读 system_configs 上限。\n影响面:新增 13 个 REST 端点与 4 个模块注入;system_configs 写入走事务/乐观锁/版本历史;OpenAPI 快照更新。\n回滚:回滚本提交并恢复 shared 契约;新增表可保留。\n验证:api typecheck/build;单测 137;集成 58;OpenAPI diff 无破坏性变更。
134 lines
3.9 KiB
TypeScript
134 lines
3.9 KiB
TypeScript
import { dictItemSnapshotSchema } from '@lingxi/shared';
|
||
import { describe, expect, it } from 'vitest';
|
||
|
||
import {
|
||
mapDictGroup,
|
||
mapDictKind,
|
||
mapDictStatus,
|
||
toDictItem,
|
||
toDictSnapshot,
|
||
toDictTypeSummary,
|
||
} from './dict.mapper';
|
||
|
||
describe('dict.mapper · Prisma 行 -> 共享 DTO', () => {
|
||
it('类型统计与枚举转换:group / kind / status 映射为契约小写值', () => {
|
||
const summary = toDictTypeSummary(
|
||
{
|
||
code: 'script_direction',
|
||
name: '脚本方向',
|
||
groupKey: 'content',
|
||
kind: 'LIST',
|
||
description: '脚本与素材的结构化方向',
|
||
industryScoped: true,
|
||
builtin: true,
|
||
status: 'ACTIVE',
|
||
sortOrder: 60,
|
||
updatedAt: new Date('2026-11-04T02:00:00.000Z'),
|
||
updatedBy: '江岚',
|
||
},
|
||
{ itemCount: 11, activeItemCount: 10, industryCount: 2 },
|
||
);
|
||
expect(summary).toEqual({
|
||
code: 'script_direction',
|
||
name: '脚本方向',
|
||
group: 'content',
|
||
kind: 'list',
|
||
description: '脚本与素材的结构化方向',
|
||
industryScoped: true,
|
||
builtin: true,
|
||
status: 'active',
|
||
sortOrder: 60,
|
||
itemCount: 11,
|
||
activeItemCount: 10,
|
||
industryCount: 2,
|
||
updatedAt: '2026-11-04T02:00:00.000Z',
|
||
updatedBy: '江岚',
|
||
});
|
||
});
|
||
|
||
it('库内异常分组回退 content;kind / status 只映射两个分支', () => {
|
||
expect(mapDictGroup('unknown_group')).toBe('content');
|
||
expect(mapDictGroup('quality')).toBe('quality');
|
||
expect(mapDictKind('TREE')).toBe('tree');
|
||
expect(mapDictKind('LIST')).toBe('list');
|
||
expect(mapDictStatus('ACTIVE')).toBe('active');
|
||
expect(mapDictStatus('DISABLED')).toBe('disabled');
|
||
});
|
||
|
||
it('条目映射:source / overridden / parentName / industryPath / refCount=0', () => {
|
||
const item = toDictItem(
|
||
{
|
||
id: '01HZZZZZZZZZZZZZZZZZZZZZZZ',
|
||
code: 'seeding',
|
||
name: '种草短视频',
|
||
parentCode: null,
|
||
industryId: 'ec',
|
||
sortOrder: 40,
|
||
status: 'ACTIVE',
|
||
remark: '父行业覆盖通用层',
|
||
updatedAt: new Date('2026-11-04T02:00:00.000Z'),
|
||
updatedBy: '江岚',
|
||
},
|
||
{
|
||
typeCode: 'script_direction',
|
||
source: 'parent',
|
||
overridden: true,
|
||
parentName: null,
|
||
industryPath: '电商',
|
||
},
|
||
);
|
||
expect(item).toMatchObject({
|
||
typeCode: 'script_direction',
|
||
code: 'seeding',
|
||
name: '种草短视频',
|
||
parentName: null,
|
||
industryId: 'ec',
|
||
industryPath: '电商',
|
||
status: 'active',
|
||
source: 'parent',
|
||
overridden: true,
|
||
refCount: 0,
|
||
updatedAt: '2026-11-04T02:00:00.000Z',
|
||
updatedBy: '江岚',
|
||
});
|
||
});
|
||
|
||
it('快照 R-10:先取快照再改字典名称,快照保留填写时名称', () => {
|
||
const item = toDictItem(
|
||
{
|
||
id: '01HZZZZZZZZZZZZZZZZZZZZZZZ',
|
||
code: 'oral',
|
||
name: '口播',
|
||
parentCode: null,
|
||
industryId: null,
|
||
sortOrder: 10,
|
||
status: 'ACTIVE',
|
||
remark: '',
|
||
updatedAt: new Date('2026-11-04T02:00:00.000Z'),
|
||
updatedBy: '江岚',
|
||
},
|
||
{
|
||
typeCode: 'script_direction',
|
||
source: 'generic',
|
||
overridden: false,
|
||
parentName: null,
|
||
industryPath: null,
|
||
},
|
||
);
|
||
const snapshot = toDictSnapshot(item, '2026-11-04T10:00:00.000Z');
|
||
// 模拟字典改名 / 停用(历史引用方不感知)
|
||
const renamed = { ...item, name: '口播(改名后)', status: 'disabled' as const };
|
||
expect(renamed.name).not.toBe(snapshot.name);
|
||
expect(snapshot.name).toBe('口播');
|
||
expect(snapshot).toEqual({
|
||
typeCode: 'script_direction',
|
||
code: 'oral',
|
||
name: '口播',
|
||
parentCode: null,
|
||
industryId: null,
|
||
capturedAt: '2026-11-04T10:00:00.000Z',
|
||
});
|
||
expect(dictItemSnapshotSchema.parse(snapshot)).toEqual(snapshot);
|
||
});
|
||
});
|