feat(14-01): add multi-signer types and columns to schema.ts

- Add signerEmail?: string to SignatureFieldData interface
- Add getSignerEmail() helper function with fallback pattern
- Add DocumentSigner interface { email, color }
- Add documents.signers JSONB column typed as DocumentSigner[]
- Add documents.completionTriggeredAt nullable TIMESTAMP column
- Add signingTokens.signerEmail nullable TEXT column
This commit is contained in:
Chandler Copeland
2026-04-03 15:15:32 -06:00
parent 07555ed6c5
commit c658f13ea0

View File

@@ -18,6 +18,7 @@ export interface SignatureFieldData {
width: number; // PDF points (default: 144 — 2 inches) width: number; // PDF points (default: 144 — 2 inches)
height: number; // PDF points (default: 36 — 0.5 inches) height: number; // PDF points (default: 36 — 0.5 inches)
type?: SignatureFieldType; // Optional — v1.0 documents have no type; fallback = 'client-signature' type?: SignatureFieldType; // Optional — v1.0 documents have no type; fallback = 'client-signature'
signerEmail?: string; // Optional — absent = legacy single-signer or agent-owned field
} }
/** /**
@@ -39,6 +40,16 @@ export function isClientVisibleField(field: SignatureFieldData): boolean {
return t !== 'agent-signature' && t !== 'agent-initials'; return t !== 'agent-signature' && t !== 'agent-initials';
} }
/**
* Safe signer email reader — returns the field's signerEmail or a fallback.
* Legacy single-signer documents have no signerEmail on their fields;
* this coalesces to the fallback (typically the document's single recipient email).
* ALWAYS use this instead of reading field.signerEmail directly.
*/
export function getSignerEmail(field: SignatureFieldData, fallbackEmail: string): string {
return field.signerEmail ?? fallbackEmail;
}
export const users = pgTable("users", { export const users = pgTable("users", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
email: text("email").notNull().unique(), email: text("email").notNull().unique(),
@@ -72,6 +83,12 @@ export const formTemplates = pgTable("form_templates", {
updatedAt: timestamp("updated_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(),
}); });
/** Shape of each entry in documents.signers JSONB array. */
export interface DocumentSigner {
email: string;
color: string;
}
export const documents = pgTable("documents", { export const documents = pgTable("documents", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text("name").notNull(), name: text("name").notNull(),
@@ -92,6 +109,10 @@ export const documents = pgTable("documents", {
signedFilePath: text("signed_file_path"), signedFilePath: text("signed_file_path"),
pdfHash: text("pdf_hash"), pdfHash: text("pdf_hash"),
signedAt: timestamp("signed_at"), signedAt: timestamp("signed_at"),
/** Per-signer list with assigned colors. NULL = legacy single-signer document. */
signers: jsonb("signers").$type<DocumentSigner[]>(),
/** Atomic completion guard — set once by the last signer's handler. NULL = not yet completed. */
completionTriggeredAt: timestamp("completion_triggered_at"),
}); });
export const documentsRelations = relations(documents, ({ one }) => ({ export const documentsRelations = relations(documents, ({ one }) => ({
@@ -115,6 +136,8 @@ export const signingTokens = pgTable('signing_tokens', {
jti: text('jti').primaryKey(), jti: text('jti').primaryKey(),
documentId: text('document_id').notNull() documentId: text('document_id').notNull()
.references(() => documents.id, { onDelete: 'cascade' }), .references(() => documents.id, { onDelete: 'cascade' }),
/** Signer this token belongs to. NULL = legacy single-signer token. */
signerEmail: text('signer_email'),
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
expiresAt: timestamp('expires_at').notNull(), expiresAt: timestamp('expires_at').notNull(),
usedAt: timestamp('used_at'), usedAt: timestamp('used_at'),