feat(v2.4): 完成领域主写迁移
This commit is contained in:
@@ -2,10 +2,14 @@ import { api } from './api';
|
||||
import type { Bug, BugSeverity, BugStatus } from './bug';
|
||||
import type { DevTask, DevTaskStatus, Reference } from './dev-task';
|
||||
import type { Priority } from './derive';
|
||||
import type { Member } from './members';
|
||||
import type { OvertimeRecord } from './overtime';
|
||||
import type { Requirement, RequirementStatus, SourceType } from './requirement';
|
||||
import type { TaskCategory } from './task-category';
|
||||
import type { TaskWorklog } from './task-worklog';
|
||||
import type { TestCase, TestCaseStatus } from './test-case';
|
||||
import type { VersionPlan, VersionPlanLog, VersionPlanRequirementCoverage } from './version-plan';
|
||||
import type { WorkActivity, WorkActivityCategory, WorkActivitySourceType } from './work-activity';
|
||||
import type { WorkActivity, WorkActivityCategory, WorkActivityDraft, WorkActivitySourceType } from './work-activity';
|
||||
|
||||
export interface RootProject {
|
||||
id: string;
|
||||
@@ -254,6 +258,51 @@ interface DomainWorkActivityRow {
|
||||
occurredAt?: string | Date | null;
|
||||
}
|
||||
|
||||
interface DomainMemberRow {
|
||||
id: string;
|
||||
name: string;
|
||||
username?: string | null;
|
||||
departmentId?: string | null;
|
||||
roleId?: string | null;
|
||||
phone?: string | null;
|
||||
email?: string | null;
|
||||
password?: string | null;
|
||||
isSystem?: boolean | null;
|
||||
createdAt?: string | Date | null;
|
||||
}
|
||||
|
||||
interface DomainTaskCategoryRow {
|
||||
id: string;
|
||||
name: string;
|
||||
code?: string | null;
|
||||
group: string;
|
||||
isSystem?: boolean | null;
|
||||
createdAt?: string | Date | null;
|
||||
}
|
||||
|
||||
interface DomainTaskWorklogRow {
|
||||
id: string;
|
||||
userId?: string | null;
|
||||
sourceId: string;
|
||||
workDate?: string | Date | null;
|
||||
hours?: number | null;
|
||||
content?: string | null;
|
||||
createdAt?: string | Date | null;
|
||||
}
|
||||
|
||||
interface DomainOvertimeRow {
|
||||
id: string;
|
||||
productId?: string | null;
|
||||
projectId?: string | null;
|
||||
versionId?: string | null;
|
||||
userId?: string | null;
|
||||
reason?: string | null;
|
||||
startAt?: string | Date | null;
|
||||
endAt?: string | Date | null;
|
||||
hours?: number | null;
|
||||
createdAt?: string | Date | null;
|
||||
}
|
||||
|
||||
interface DomainMutationResponse<T> {
|
||||
item: T;
|
||||
activities?: DomainWorkActivityRow[];
|
||||
@@ -436,6 +485,83 @@ export async function deleteBugByVersionId(versionId: string, bugId: string): Pr
|
||||
await api.delete(`/versions/${versionId}/bugs/${bugId}`);
|
||||
}
|
||||
|
||||
export async function listMembersDomain(): Promise<Member[]> {
|
||||
const rows = await api.get<DomainMemberRow[]>('/members');
|
||||
return rows.map(normalizeMember);
|
||||
}
|
||||
|
||||
export async function createMemberDomain(data: Partial<Member>, _existing?: Member[]): Promise<Member> {
|
||||
return normalizeMember(await api.post<DomainMemberRow>('/members', toMemberPayload(data)));
|
||||
}
|
||||
|
||||
export async function updateMemberDomain(memberId: string, data: Partial<Member>): Promise<Member> {
|
||||
return normalizeMember(await api.patch<DomainMemberRow>(`/members/${memberId}`, toMemberPayload(data)));
|
||||
}
|
||||
|
||||
export async function deleteMemberDomain(memberId: string): Promise<void> {
|
||||
await api.delete(`/members/${memberId}`);
|
||||
}
|
||||
|
||||
export async function listTaskCategoriesDomain(): Promise<TaskCategory[]> {
|
||||
const rows = await api.get<DomainTaskCategoryRow[]>('/task-categories');
|
||||
return rows.map((row, index) => normalizeTaskCategory(row, index));
|
||||
}
|
||||
|
||||
export async function createTaskCategoryDomain(data: Partial<TaskCategory>): Promise<TaskCategory> {
|
||||
return normalizeTaskCategory(await api.post<DomainTaskCategoryRow>('/task-categories', toTaskCategoryPayload(data)), 0);
|
||||
}
|
||||
|
||||
export async function updateTaskCategoryDomain(categoryId: string, data: Partial<TaskCategory>): Promise<TaskCategory> {
|
||||
return normalizeTaskCategory(await api.patch<DomainTaskCategoryRow>(`/task-categories/${categoryId}`, toTaskCategoryPayload(data)), 0);
|
||||
}
|
||||
|
||||
export async function deleteTaskCategoryDomain(categoryId: string): Promise<void> {
|
||||
await api.delete(`/task-categories/${categoryId}`);
|
||||
}
|
||||
|
||||
export async function listTaskWorklogsDomain(): Promise<TaskWorklog[]> {
|
||||
const rows = await api.get<DomainTaskWorklogRow[]>('/task-worklogs');
|
||||
return rows.map(normalizeTaskWorklog);
|
||||
}
|
||||
|
||||
export async function createTaskWorklogDomain(data: TaskWorklog): Promise<TaskWorklog> {
|
||||
return normalizeTaskWorklog(await api.post<DomainTaskWorklogRow>('/task-worklogs', toTaskWorklogPayload(data)));
|
||||
}
|
||||
|
||||
export async function deleteTaskWorklogDomain(worklogId: string): Promise<void> {
|
||||
await api.delete(`/task-worklogs/${worklogId}`);
|
||||
}
|
||||
|
||||
export async function listOvertimeRecordsDomain(): Promise<OvertimeRecord[]> {
|
||||
const rows = await api.get<DomainOvertimeRow[]>('/overtime');
|
||||
return rows.map(normalizeOvertimeRecord);
|
||||
}
|
||||
|
||||
export async function createOvertimeRecordDomain(data: OvertimeRecord): Promise<OvertimeRecord> {
|
||||
return normalizeOvertimeRecord(await api.post<DomainOvertimeRow>('/overtime', toOvertimePayload(data)));
|
||||
}
|
||||
|
||||
export async function updateOvertimeRecordDomain(recordId: string, data: Partial<OvertimeRecord>): Promise<void> {
|
||||
await api.patch(`/overtime/${recordId}`, toOvertimePayload(data));
|
||||
}
|
||||
|
||||
export async function deleteOvertimeRecordDomain(recordId: string): Promise<void> {
|
||||
await api.delete(`/overtime/${recordId}`);
|
||||
}
|
||||
|
||||
export async function listWorkActivitiesDomain(): Promise<WorkActivity[]> {
|
||||
const rows = await api.get<DomainWorkActivityRow[]>('/work-activities');
|
||||
return rows.map(normalizeWorkActivity);
|
||||
}
|
||||
|
||||
export async function createWorkActivityDomain(data: WorkActivity | WorkActivityDraft): Promise<WorkActivity> {
|
||||
return normalizeWorkActivity(await api.post<DomainWorkActivityRow>('/work-activities', toWorkActivityPayload(data)));
|
||||
}
|
||||
|
||||
export async function deleteWorkActivityDomain(activityId: string): Promise<void> {
|
||||
await api.delete(`/work-activities/${activityId}`);
|
||||
}
|
||||
|
||||
function normalizeProductRoot(product: RootProduct): RootProduct {
|
||||
return {
|
||||
...product,
|
||||
@@ -753,6 +879,126 @@ function toBugSeverity(value: string | null | undefined): BugSeverity {
|
||||
return 'minor';
|
||||
}
|
||||
|
||||
function toMemberPayload(data: Partial<Member>) {
|
||||
return {
|
||||
...(data.id !== undefined && { id: data.id }),
|
||||
...(data.name !== undefined && { name: data.name }),
|
||||
...(data.username !== undefined && { username: data.username }),
|
||||
...(data.departmentId !== undefined && { departmentId: data.departmentId }),
|
||||
...(data.roleId !== undefined && { roleId: data.roleId }),
|
||||
...(data.phone !== undefined && { phone: data.phone }),
|
||||
...(data.email !== undefined && { email: data.email }),
|
||||
...(data.password !== undefined && { password: data.password }),
|
||||
...(data.isSystem !== undefined && { isSystem: data.isSystem }),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeMember(row: DomainMemberRow): Member {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
username: row.username ?? undefined,
|
||||
departmentId: row.departmentId ?? '',
|
||||
roleId: row.roleId ?? 'member',
|
||||
phone: row.phone ?? '',
|
||||
email: row.email ?? '',
|
||||
password: row.password ?? '',
|
||||
createdAt: isoString(row.createdAt).slice(0, 10),
|
||||
isSystem: Boolean(row.isSystem),
|
||||
};
|
||||
}
|
||||
|
||||
function toTaskCategoryPayload(data: Partial<TaskCategory>) {
|
||||
return {
|
||||
...(data.name !== undefined && { name: data.name }),
|
||||
...(data.group !== undefined && { group: data.group }),
|
||||
...(data.code !== undefined && { code: data.code }),
|
||||
...(data.isSystem !== undefined && { isSystem: data.isSystem }),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTaskCategory(row: DomainTaskCategoryRow, index: number): TaskCategory {
|
||||
return {
|
||||
id: row.id,
|
||||
code: row.code ?? row.name,
|
||||
name: row.name,
|
||||
group: toCategoryGroup(row.group),
|
||||
sortOrder: index + 1,
|
||||
isSystem: Boolean(row.isSystem),
|
||||
};
|
||||
}
|
||||
|
||||
function toCategoryGroup(value: string): TaskCategory['group'] {
|
||||
if (value === 'development' || value === 'testing' || value === 'implementation') return value;
|
||||
return 'other';
|
||||
}
|
||||
|
||||
function toTaskWorklogPayload(data: TaskWorklog) {
|
||||
return {
|
||||
taskId: data.taskId,
|
||||
userId: data.userId,
|
||||
date: data.date,
|
||||
hours: data.hours,
|
||||
workContent: data.workContent,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTaskWorklog(row: DomainTaskWorklogRow): TaskWorklog {
|
||||
return {
|
||||
id: row.id,
|
||||
taskId: row.sourceId,
|
||||
userId: row.userId ?? '',
|
||||
date: isoString(row.workDate).slice(0, 10),
|
||||
hours: row.hours ?? 0,
|
||||
workContent: row.content ?? '',
|
||||
createdAt: isoString(row.createdAt),
|
||||
};
|
||||
}
|
||||
|
||||
function toOvertimePayload(data: Partial<OvertimeRecord>) {
|
||||
return {
|
||||
...(data.productId !== undefined && { productId: data.productId }),
|
||||
...(data.projectId !== undefined && { projectId: data.projectId }),
|
||||
...(data.versionId !== undefined && { versionId: data.versionId }),
|
||||
...(data.person !== undefined && { person: data.person }),
|
||||
...(data.startTime !== undefined && { startTime: data.startTime }),
|
||||
...(data.endTime !== undefined && { endTime: data.endTime }),
|
||||
...(data.duration !== undefined && { duration: data.duration }),
|
||||
...(data.reasonId !== undefined && { reasonId: data.reasonId }),
|
||||
...(data.remark !== undefined && { remark: data.remark }),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeOvertimeRecord(row: DomainOvertimeRow): OvertimeRecord {
|
||||
const [reasonId, ...remarkParts] = (row.reason ?? '').split(':');
|
||||
return {
|
||||
id: row.id,
|
||||
projectId: row.projectId ?? '',
|
||||
versionId: row.versionId ?? undefined,
|
||||
person: row.userId ?? '',
|
||||
startTime: isoString(row.startAt),
|
||||
endTime: isoString(row.endAt),
|
||||
duration: row.hours ?? 0,
|
||||
reasonId,
|
||||
remark: remarkParts.join(':') || undefined,
|
||||
createdAt: isoString(row.createdAt).slice(0, 10),
|
||||
};
|
||||
}
|
||||
|
||||
function toWorkActivityPayload(data: WorkActivity | WorkActivityDraft) {
|
||||
return {
|
||||
actorId: data.actorId,
|
||||
sourceType: data.sourceType,
|
||||
sourceId: data.sourceId,
|
||||
action: data.action,
|
||||
title: data.title,
|
||||
category: data.category,
|
||||
summary: data.summary,
|
||||
metadata: data.metadata,
|
||||
occurredAt: data.occurredAt,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeMutation<Row, Item>(
|
||||
response: DomainMutationResponse<Row>,
|
||||
mapper: (row: Row) => Item,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { hasPermission } from './permissions';
|
||||
|
||||
export interface OvertimeRecord {
|
||||
id: string;
|
||||
productId?: string;
|
||||
projectId: string;
|
||||
versionId?: string;
|
||||
requirementId?: string;
|
||||
|
||||
58
apps/web/lib/v245-domain-source.test.ts
Normal file
58
apps/web/lib/v245-domain-source.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
function store(name: string) {
|
||||
return readFileSync(join(process.cwd(), `stores/${name}`), 'utf8');
|
||||
}
|
||||
|
||||
function storeMethodBody(text: string, name: string) {
|
||||
const implementationStart = text.indexOf('export const');
|
||||
assert.notEqual(implementationStart, -1, 'missing store implementation');
|
||||
const start = text.indexOf(` ${name}:`, implementationStart);
|
||||
assert.notEqual(start, -1, `missing store method ${name}`);
|
||||
let depth = 0;
|
||||
let sawFirstBrace = false;
|
||||
for (let i = start; i < text.length; i += 1) {
|
||||
const char = text[i];
|
||||
if (char === '{') {
|
||||
depth += 1;
|
||||
sawFirstBrace = true;
|
||||
}
|
||||
if (char === '}') {
|
||||
depth -= 1;
|
||||
if (sawFirstBrace && depth === 0) return text.slice(start, i + 1);
|
||||
}
|
||||
}
|
||||
throw new Error(`could not extract store method ${name}`);
|
||||
}
|
||||
|
||||
test('member and dictionary stores use domain APIs as primary writes', () => {
|
||||
const members = store('useMemberStore.ts');
|
||||
const categories = store('useTaskCategoryStore.ts');
|
||||
|
||||
assert.match(members, /from '@\/lib\/domain-api'/);
|
||||
assert.match(storeMethodBody(members, 'createMember'), /createMemberDomain\(member,/);
|
||||
assert.match(storeMethodBody(members, 'updateMember'), /updateMemberDomain\(id,/);
|
||||
assert.match(storeMethodBody(members, 'deleteMember'), /deleteMemberDomain\(id\)/);
|
||||
|
||||
assert.match(categories, /from '@\/lib\/domain-api'/);
|
||||
assert.match(storeMethodBody(categories, 'addCategory'), /createTaskCategoryDomain\(item\)/);
|
||||
assert.match(storeMethodBody(categories, 'updateCategory'), /updateTaskCategoryDomain\(id,/);
|
||||
assert.match(storeMethodBody(categories, 'deleteCategory'), /deleteTaskCategoryDomain\(id\)/);
|
||||
});
|
||||
|
||||
test('evidence stores use append-style domain APIs as primary writes', () => {
|
||||
const worklogs = store('useTaskWorklogStore.ts');
|
||||
const overtime = store('useOvertimeStore.ts');
|
||||
const activities = store('useWorkActivityStore.ts');
|
||||
|
||||
assert.match(storeMethodBody(worklogs, 'addWorklog'), /createTaskWorklogDomain\(item\)/);
|
||||
assert.match(storeMethodBody(overtime, 'createRecord'), /createOvertimeRecordDomain\(record\)/);
|
||||
assert.match(storeMethodBody(activities, 'addActivity'), /createWorkActivityDomain\(item\)/);
|
||||
|
||||
assert.doesNotMatch(storeMethodBody(worklogs, 'addWorklog'), /saveServerData\('task-worklogs'/);
|
||||
assert.doesNotMatch(storeMethodBody(overtime, 'createRecord'), /saveServerData\('overtime'/);
|
||||
assert.doesNotMatch(storeMethodBody(activities, 'addActivity'), /saveServerData\('work-activities'/);
|
||||
});
|
||||
Reference in New Issue
Block a user