135 lines
4.8 KiB
TypeScript
135 lines
4.8 KiB
TypeScript
/**
|
|
* 评估向导草稿持久化(服务端,跨设备)。
|
|
* - source_assessment_id 为空:全新建评估的草稿
|
|
* - source_assessment_id 非空:编辑某既有评估时的草稿
|
|
* form 存完整向导快照(任意 JSON)。
|
|
*/
|
|
import type pg from 'pg';
|
|
|
|
export interface DraftRecord {
|
|
id: string;
|
|
assessorId: string | null;
|
|
sourceAssessmentId: string | null;
|
|
projectName: string | null;
|
|
form: unknown;
|
|
updatedAt: string;
|
|
}
|
|
|
|
/** 列表项(不含 form 大字段,仅元信息)。 */
|
|
export interface DraftSummary {
|
|
id: string;
|
|
assessorId: string | null;
|
|
sourceAssessmentId: string | null;
|
|
projectName: string | null;
|
|
updatedAt: string;
|
|
}
|
|
|
|
function isoOf(v: unknown): string {
|
|
return v instanceof Date ? v.toISOString() : String(v);
|
|
}
|
|
|
|
/** JSONB 不能存 \u0000(空字符),而指标复合键用其作分隔符。存储时编码为私有区字符,读取时还原。 */
|
|
const NUL = '\u0000';
|
|
const NUL_SENTINEL = '\uE000';
|
|
function deepReplace(value: unknown, from: string, to: string): unknown {
|
|
if (typeof value === 'string') return value.split(from).join(to);
|
|
if (Array.isArray(value)) return value.map((v) => deepReplace(v, from, to));
|
|
if (value !== null && typeof value === 'object') {
|
|
const out: Record<string, unknown> = {};
|
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
out[k.split(from).join(to)] = deepReplace(v, from, to);
|
|
}
|
|
return out;
|
|
}
|
|
return value;
|
|
}
|
|
/** 存库前编码(去除 \u0000)。 */
|
|
function encodeForm(form: unknown): unknown {
|
|
return deepReplace(form ?? null, NUL, NUL_SENTINEL);
|
|
}
|
|
/** 读出后还原。 */
|
|
function decodeForm(form: unknown): unknown {
|
|
return deepReplace(form ?? null, NUL_SENTINEL, NUL);
|
|
}
|
|
|
|
/** 列出草稿(可按 assessorId 过滤),不返回 form 大字段。 */
|
|
export async function listDrafts(pool: pg.Pool, assessorId?: string): Promise<DraftSummary[]> {
|
|
const where = assessorId ? 'WHERE assessor_id = $1' : '';
|
|
const params = assessorId ? [assessorId] : [];
|
|
const res = await pool.query(
|
|
`SELECT id, assessor_id, source_assessment_id, project_name, updated_at
|
|
FROM wizard_drafts ${where} ORDER BY updated_at DESC`,
|
|
params,
|
|
);
|
|
return (res.rows as Array<Record<string, unknown>>).map((r) => ({
|
|
id: String(r.id),
|
|
assessorId: r.assessor_id != null ? String(r.assessor_id) : null,
|
|
sourceAssessmentId: r.source_assessment_id != null ? String(r.source_assessment_id) : null,
|
|
projectName: r.project_name != null ? String(r.project_name) : null,
|
|
updatedAt: isoOf(r.updated_at),
|
|
}));
|
|
}
|
|
|
|
/** 取单个草稿(含 form)。 */
|
|
export async function getDraft(pool: pg.Pool, id: string): Promise<DraftRecord | null> {
|
|
const res = await pool.query(
|
|
`SELECT id, assessor_id, source_assessment_id, project_name, form, updated_at
|
|
FROM wizard_drafts WHERE id = $1`,
|
|
[id],
|
|
);
|
|
const r = (res.rows as Array<Record<string, unknown>>)[0];
|
|
if (!r) return null;
|
|
return {
|
|
id: String(r.id),
|
|
assessorId: r.assessor_id != null ? String(r.assessor_id) : null,
|
|
sourceAssessmentId: r.source_assessment_id != null ? String(r.source_assessment_id) : null,
|
|
projectName: r.project_name != null ? String(r.project_name) : null,
|
|
form: decodeForm(r.form),
|
|
updatedAt: isoOf(r.updated_at),
|
|
};
|
|
}
|
|
|
|
export interface UpsertDraftInput {
|
|
id: string;
|
|
assessorId?: string | null;
|
|
sourceAssessmentId?: string | null;
|
|
projectName?: string | null;
|
|
form: unknown;
|
|
}
|
|
|
|
/** 新增或更新草稿(按 id)。 */
|
|
export async function upsertDraft(pool: pg.Pool, input: UpsertDraftInput): Promise<DraftRecord> {
|
|
const res = await pool.query(
|
|
`INSERT INTO wizard_drafts(id, assessor_id, source_assessment_id, project_name, form, updated_at)
|
|
VALUES($1,$2,$3,$4,$5,now())
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
assessor_id = EXCLUDED.assessor_id,
|
|
source_assessment_id = EXCLUDED.source_assessment_id,
|
|
project_name = EXCLUDED.project_name,
|
|
form = EXCLUDED.form,
|
|
updated_at = now()
|
|
RETURNING id, assessor_id, source_assessment_id, project_name, form, updated_at`,
|
|
[
|
|
input.id,
|
|
input.assessorId ?? null,
|
|
input.sourceAssessmentId ?? null,
|
|
input.projectName ?? null,
|
|
JSON.stringify(encodeForm(input.form)),
|
|
],
|
|
);
|
|
const r = (res.rows as Array<Record<string, unknown>>)[0]!;
|
|
return {
|
|
id: String(r.id),
|
|
assessorId: r.assessor_id != null ? String(r.assessor_id) : null,
|
|
sourceAssessmentId: r.source_assessment_id != null ? String(r.source_assessment_id) : null,
|
|
projectName: r.project_name != null ? String(r.project_name) : null,
|
|
form: decodeForm(r.form),
|
|
updatedAt: isoOf(r.updated_at),
|
|
};
|
|
}
|
|
|
|
/** 删除草稿。 */
|
|
export async function deleteDraft(pool: pg.Pool, id: string): Promise<void> {
|
|
await pool.query('DELETE FROM wizard_drafts WHERE id = $1', [id]);
|
|
}
|