feat(documents): Phase A schema + service skeletons
Adds Phase A data model deltas to documents/templates and the new document_watchers table. Introduces createFromWizard/createFromUpload stubs, getDocumentDetail aggregator, cancelDocument flow, signed-doc email composer, reservation agreement context, and notifyDocumentEvent fan-out. Validator update accepts new template formats with html-only bodyHtml requirement. EOI cadence backfilled to 1 day to preserve current effective behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,22 +49,16 @@ export interface TemplateVersion {
|
||||
* We use a convention: version is stored in the `mergeFields` jsonb array
|
||||
* as `["__version__:N"]` to avoid adding a new column.
|
||||
*/
|
||||
function getVersionFromRecord(
|
||||
record: typeof documentTemplates.$inferSelect,
|
||||
): number {
|
||||
function getVersionFromRecord(record: typeof documentTemplates.$inferSelect): number {
|
||||
const mf = record.mergeFields as unknown;
|
||||
if (!Array.isArray(mf)) return 1;
|
||||
const versionEntry = (mf as string[]).find((e) =>
|
||||
e.startsWith('__version__:'),
|
||||
);
|
||||
const versionEntry = (mf as string[]).find((e) => e.startsWith('__version__:'));
|
||||
if (!versionEntry) return 1;
|
||||
const n = parseInt(versionEntry.split(':')[1] ?? '1', 10);
|
||||
return isNaN(n) ? 1 : n;
|
||||
}
|
||||
|
||||
function buildMergeFieldsWithVersion(
|
||||
version: number,
|
||||
): string[] {
|
||||
function buildMergeFieldsWithVersion(version: number): string[] {
|
||||
return [`__version__:${version}`];
|
||||
}
|
||||
|
||||
@@ -72,9 +66,7 @@ function buildMergeFieldsWithVersion(
|
||||
* Parse TipTap JSON from bodyHtml field. Returns the parsed object, or null
|
||||
* if bodyHtml is plain HTML (legacy records).
|
||||
*/
|
||||
function parseTipTapContent(
|
||||
bodyHtml: string,
|
||||
): Record<string, unknown> | null {
|
||||
function parseTipTapContent(bodyHtml: string): Record<string, unknown> | null {
|
||||
try {
|
||||
const parsed = JSON.parse(bodyHtml) as unknown;
|
||||
if (
|
||||
@@ -92,10 +84,7 @@ function parseTipTapContent(
|
||||
|
||||
// ─── List ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function listAdminTemplates(
|
||||
portId: string,
|
||||
query: ListAdminTemplatesInput,
|
||||
) {
|
||||
export async function listAdminTemplates(portId: string, query: ListAdminTemplatesInput) {
|
||||
const { type, isActive } = query;
|
||||
|
||||
const conditions = [eq(documentTemplates.portId, portId)];
|
||||
@@ -116,21 +105,15 @@ export async function listAdminTemplates(
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
version: getVersionFromRecord(row),
|
||||
content: parseTipTapContent(row.bodyHtml),
|
||||
content: parseTipTapContent(row.bodyHtml ?? ''),
|
||||
}));
|
||||
}
|
||||
|
||||
// ─── Get by ID ────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function getAdminTemplate(
|
||||
portId: string,
|
||||
templateId: string,
|
||||
) {
|
||||
export async function getAdminTemplate(portId: string, templateId: string) {
|
||||
const row = await db.query.documentTemplates.findFirst({
|
||||
where: and(
|
||||
eq(documentTemplates.id, templateId),
|
||||
eq(documentTemplates.portId, portId),
|
||||
),
|
||||
where: and(eq(documentTemplates.id, templateId), eq(documentTemplates.portId, portId)),
|
||||
});
|
||||
|
||||
if (!row) {
|
||||
@@ -140,15 +123,13 @@ export async function getAdminTemplate(
|
||||
return {
|
||||
...row,
|
||||
version: getVersionFromRecord(row),
|
||||
content: parseTipTapContent(row.bodyHtml),
|
||||
content: parseTipTapContent(row.bodyHtml ?? ''),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Validate TipTap Content ─────────────────────────────────────────────────
|
||||
|
||||
function assertValidContent(
|
||||
content: Record<string, unknown>,
|
||||
): void {
|
||||
function assertValidContent(content: Record<string, unknown>): void {
|
||||
const unsupported = validateTipTapDocument(
|
||||
content as unknown as Parameters<typeof validateTipTapDocument>[0],
|
||||
);
|
||||
@@ -257,21 +238,13 @@ export async function updateAdminTemplate(
|
||||
const [updated] = await db
|
||||
.update(documentTemplates)
|
||||
.set(updateValues)
|
||||
.where(
|
||||
and(
|
||||
eq(documentTemplates.id, templateId),
|
||||
eq(documentTemplates.portId, portId),
|
||||
),
|
||||
)
|
||||
.where(and(eq(documentTemplates.id, templateId), eq(documentTemplates.portId, portId)))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
...updated!,
|
||||
version: newVersion,
|
||||
content:
|
||||
data.content !== undefined
|
||||
? data.content
|
||||
: existing.content,
|
||||
content: data.content !== undefined ? data.content : existing.content,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -287,12 +260,7 @@ export async function deleteAdminTemplate(
|
||||
|
||||
await db
|
||||
.delete(documentTemplates)
|
||||
.where(
|
||||
and(
|
||||
eq(documentTemplates.id, templateId),
|
||||
eq(documentTemplates.portId, portId),
|
||||
),
|
||||
);
|
||||
.where(and(eq(documentTemplates.id, templateId), eq(documentTemplates.portId, portId)));
|
||||
|
||||
void createAuditLog({
|
||||
userId: meta.userId,
|
||||
@@ -337,10 +305,7 @@ export async function getAdminTemplateVersions(
|
||||
.filter((log) => {
|
||||
const meta = log.metadata as Record<string, unknown> | null;
|
||||
return (
|
||||
meta !== null &&
|
||||
typeof meta === 'object' &&
|
||||
'versionSnapshot' in meta &&
|
||||
'content' in meta
|
||||
meta !== null && typeof meta === 'object' && 'versionSnapshot' in meta && 'content' in meta
|
||||
);
|
||||
})
|
||||
.map((log) => {
|
||||
@@ -403,12 +368,7 @@ export async function rollbackAdminTemplate(
|
||||
mergeFields: buildMergeFieldsWithVersion(newVersion),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(documentTemplates.id, templateId),
|
||||
eq(documentTemplates.portId, portId),
|
||||
),
|
||||
)
|
||||
.where(and(eq(documentTemplates.id, templateId), eq(documentTemplates.portId, portId)))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user