Files
Jyotisha/frontend/src/lib/personal-report-section-service-core.ts
T
Jesse_Chen 0ae3e2d796
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
feat(reports): generate personal reports by section
2026-08-30 22:59:17 +08:00

142 lines
6.8 KiB
TypeScript

import type { PersonalReportAgentOutput } from "@/mastra/personal-report";
export type PersonalReportSectionPayload = PersonalReportAgentOutput["thematicNarrative"][number];
export type PersonalReportSectionStatus = "pending" | "ready" | "blocked";
export type PersonalReportSectionRecord = Readonly<{
userId: string;
requestId: string;
sectionId: string;
payload: PersonalReportSectionPayload | null;
status: PersonalReportSectionStatus;
attemptCount: number;
maxAttempts: number;
lastErrorCode: string | null;
createdAt: string;
updatedAt: string;
}>;
type SectionIdentity = Readonly<{ userId: string; requestId: string; sectionId: string }>;
export type PersonalReportSectionService = Readonly<{
ensure(input: SectionIdentity & { maxAttempts: number }): Promise<PersonalReportSectionRecord>;
list(userId: string, requestId: string): Promise<readonly PersonalReportSectionRecord[]>;
start(input: SectionIdentity): Promise<PersonalReportSectionRecord | null>;
complete(input: SectionIdentity & { payload: PersonalReportSectionPayload }): Promise<PersonalReportSectionRecord | null>;
block(input: SectionIdentity & { errorCode: string }): Promise<PersonalReportSectionRecord | null>;
}>;
export type PersonalReportSectionQueryResult = Readonly<{
data: unknown;
error: Readonly<{ message: string; code?: string }> | null;
}>;
type QueryBuilder = PromiseLike<PersonalReportSectionQueryResult> & {
select(columns: string): QueryBuilder;
eq(column: string, value: unknown): QueryBuilder;
order(column: string, options?: Readonly<{ ascending?: boolean }>): QueryBuilder;
maybeSingle(): PromiseLike<PersonalReportSectionQueryResult>;
};
type DataClient = {
from(table: string): QueryBuilder;
rpc(functionName: string, args?: Readonly<Record<string, unknown>>): PromiseLike<PersonalReportSectionQueryResult>;
};
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const sectionIdPattern = /^[a-z][a-z0-9_-]{0,95}$/;
const errorCodePattern = /^[a-z][a-z0-9_]{0,63}$/;
const columns = [
"user_id", "request_id", "section_id", "payload", "status", "attempt_count",
"max_attempts", "last_error_code", "created_at", "updated_at",
].join(",");
type DbRow = Record<string, unknown>;
function requireUuid(value: string, field: string): void {
if (!uuidPattern.test(value)) throw new Error(`${field} is invalid`);
}
function requireSectionId(value: string): void {
if (!sectionIdPattern.test(value)) throw new Error("sectionId is invalid");
}
function row(value: unknown): PersonalReportSectionRecord {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("personal report section row is invalid");
const source = value as DbRow;
const userId = String(source.user_id);
const requestId = String(source.request_id);
const sectionId = String(source.section_id);
requireUuid(userId, "userId");
requireUuid(requestId, "requestId");
requireSectionId(sectionId);
const status = source.status;
if (status !== "pending" && status !== "ready" && status !== "blocked") throw new Error("section status is invalid");
const payload = source.payload === null || source.payload === undefined ? null : source.payload as PersonalReportSectionPayload;
if (status === "ready" && payload === null) throw new Error("ready section payload is missing");
if (status !== "ready" && payload !== null) throw new Error("non-ready section payload is unexpected");
const attemptCount = Number(source.attempt_count);
const maxAttempts = Number(source.max_attempts);
if (!Number.isInteger(attemptCount) || !Number.isInteger(maxAttempts) || attemptCount < 0 || maxAttempts < 1 || attemptCount > maxAttempts) {
throw new Error("section attempt budget is invalid");
}
const createdAt = String(source.created_at);
const updatedAt = String(source.updated_at);
if (!Number.isFinite(Date.parse(createdAt)) || !Number.isFinite(Date.parse(updatedAt))) throw new Error("section timestamp is invalid");
return {
userId, requestId, sectionId, payload, status, attemptCount, maxAttempts,
lastErrorCode: source.last_error_code === null || source.last_error_code === undefined ? null : String(source.last_error_code),
createdAt, updatedAt,
};
}
function first(data: unknown): unknown {
return Array.isArray(data) ? data[0] ?? null : data;
}
function requireData(result: PersonalReportSectionQueryResult): unknown {
if (result.error) throw new Error(result.error.message);
return result.data;
}
export function createPersonalReportSectionService(client: DataClient): PersonalReportSectionService {
return {
async ensure(input) {
requireUuid(input.userId, "userId");
requireUuid(input.requestId, "requestId");
requireSectionId(input.sectionId);
if (!Number.isInteger(input.maxAttempts) || input.maxAttempts < 1 || input.maxAttempts > 10) throw new Error("maxAttempts is invalid");
const result = await client.rpc("ensure_personal_report_section", {
p_user_id: input.userId, p_request_id: input.requestId, p_section_id: input.sectionId, p_max_attempts: input.maxAttempts,
});
const value = requireData(result);
const parsed = row(first(value));
if (!parsed) throw new Error("section ensure returned no row");
return parsed;
},
async list(userId, requestId) {
requireUuid(userId, "userId");
requireUuid(requestId, "requestId");
const result = await client.from("personal_report_sections").select(columns).eq("user_id", userId).eq("request_id", requestId).order("section_id", { ascending: true });
const value = requireData(result);
if (!Array.isArray(value)) throw new Error("section list returned invalid data");
return value.map(row);
},
async start(input) {
const result = await client.rpc("start_personal_report_section", { p_user_id: input.userId, p_request_id: input.requestId, p_section_id: input.sectionId });
const value = requireData(result);
const parsed = first(value);
return parsed === null ? null : row(parsed);
},
async complete(input) {
const result = await client.rpc("complete_personal_report_section", { p_user_id: input.userId, p_request_id: input.requestId, p_section_id: input.sectionId, p_payload: input.payload });
const value = requireData(result);
const parsed = first(value);
return parsed === null ? null : row(parsed);
},
async block(input) {
if (!errorCodePattern.test(input.errorCode)) throw new Error("errorCode is invalid");
const result = await client.rpc("block_personal_report_section", { p_user_id: input.userId, p_request_id: input.requestId, p_section_id: input.sectionId, p_error_code: input.errorCode });
const value = requireData(result);
const parsed = first(value);
return parsed === null ? null : row(parsed);
},
};
}