142 lines
6.8 KiB
TypeScript
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);
|
|
},
|
|
};
|
|
}
|