2026-03-19 23:53:12 -06:00
|
|
|
import { jsonb, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
2026-03-19 21:44:17 -06:00
|
|
|
import { relations } from "drizzle-orm";
|
2026-03-19 13:30:52 -06:00
|
|
|
|
2026-03-21 11:46:49 -06:00
|
|
|
export type SignatureFieldType =
|
|
|
|
|
| 'client-signature'
|
|
|
|
|
| 'initials'
|
|
|
|
|
| 'text'
|
|
|
|
|
| 'checkbox'
|
|
|
|
|
| 'date'
|
|
|
|
|
| 'agent-signature';
|
|
|
|
|
|
2026-03-19 23:53:12 -06:00
|
|
|
export interface SignatureFieldData {
|
|
|
|
|
id: string;
|
|
|
|
|
page: number; // 1-indexed
|
|
|
|
|
x: number; // PDF user space, bottom-left origin, points
|
|
|
|
|
y: number; // PDF user space, bottom-left origin, points
|
|
|
|
|
width: number; // PDF points (default: 144 — 2 inches)
|
|
|
|
|
height: number; // PDF points (default: 36 — 0.5 inches)
|
2026-03-21 11:46:49 -06:00
|
|
|
type?: SignatureFieldType; // Optional — v1.0 documents have no type; fallback = 'client-signature'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Safe field type reader — always returns a SignatureFieldType, never undefined.
|
|
|
|
|
* v1.0 documents have no `type` on their JSONB fields; this coalesces to 'client-signature'.
|
|
|
|
|
* ALWAYS use this instead of reading field.type directly.
|
|
|
|
|
*/
|
|
|
|
|
export function getFieldType(field: SignatureFieldData): SignatureFieldType {
|
|
|
|
|
return field.type ?? 'client-signature';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns true for field types that should be visible in the client signing session.
|
|
|
|
|
* agent-signature fields are embedded during document preparation and must never
|
|
|
|
|
* surface to the client as required unsigned fields.
|
|
|
|
|
*/
|
|
|
|
|
export function isClientVisibleField(field: SignatureFieldData): boolean {
|
|
|
|
|
return getFieldType(field) !== 'agent-signature';
|
2026-03-19 23:53:12 -06:00
|
|
|
}
|
|
|
|
|
|
2026-03-19 13:30:52 -06:00
|
|
|
export const users = pgTable("users", {
|
|
|
|
|
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
|
|
|
email: text("email").notNull().unique(),
|
|
|
|
|
passwordHash: text("password_hash").notNull(),
|
|
|
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
|
|
|
});
|
feat(03-01): add clients and documents tables to Drizzle schema
- Add pgEnum import and documentStatusEnum (Draft, Sent, Viewed, Signed)
- Add clients table (id, name, email, createdAt, updatedAt)
- Add documents table (id, name, clientId FK, status enum, sentAt, createdAt)
- Generate migration 0001_watery_blindfold.sql and apply to local PostgreSQL
2026-03-19 16:17:26 -06:00
|
|
|
|
|
|
|
|
export const documentStatusEnum = pgEnum("document_status", [
|
|
|
|
|
"Draft",
|
|
|
|
|
"Sent",
|
|
|
|
|
"Viewed",
|
|
|
|
|
"Signed",
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
export const clients = pgTable("clients", {
|
|
|
|
|
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
|
|
|
name: text("name").notNull(),
|
|
|
|
|
email: text("email").notNull(),
|
2026-03-21 12:13:55 -06:00
|
|
|
propertyAddress: text("property_address"),
|
feat(03-01): add clients and documents tables to Drizzle schema
- Add pgEnum import and documentStatusEnum (Draft, Sent, Viewed, Signed)
- Add clients table (id, name, email, createdAt, updatedAt)
- Add documents table (id, name, clientId FK, status enum, sentAt, createdAt)
- Generate migration 0001_watery_blindfold.sql and apply to local PostgreSQL
2026-03-19 16:17:26 -06:00
|
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
|
|
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-19 21:32:30 -06:00
|
|
|
export const formTemplates = pgTable("form_templates", {
|
|
|
|
|
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
|
|
|
name: text("name").notNull(),
|
|
|
|
|
filename: text("filename").notNull().unique(),
|
|
|
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
|
|
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
|
|
|
});
|
|
|
|
|
|
feat(03-01): add clients and documents tables to Drizzle schema
- Add pgEnum import and documentStatusEnum (Draft, Sent, Viewed, Signed)
- Add clients table (id, name, email, createdAt, updatedAt)
- Add documents table (id, name, clientId FK, status enum, sentAt, createdAt)
- Generate migration 0001_watery_blindfold.sql and apply to local PostgreSQL
2026-03-19 16:17:26 -06:00
|
|
|
export const documents = pgTable("documents", {
|
|
|
|
|
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
|
|
|
name: text("name").notNull(),
|
|
|
|
|
clientId: text("client_id")
|
|
|
|
|
.notNull()
|
|
|
|
|
.references(() => clients.id, { onDelete: "cascade" }),
|
|
|
|
|
status: documentStatusEnum("status").notNull().default("Draft"),
|
|
|
|
|
sentAt: timestamp("sent_at"),
|
|
|
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
2026-03-19 21:32:30 -06:00
|
|
|
formTemplateId: text("form_template_id").references(() => formTemplates.id),
|
|
|
|
|
filePath: text("file_path"),
|
2026-03-19 23:53:12 -06:00
|
|
|
signatureFields: jsonb("signature_fields").$type<SignatureFieldData[]>(),
|
|
|
|
|
textFillData: jsonb("text_fill_data").$type<Record<string, string>>(),
|
|
|
|
|
assignedClientId: text("assigned_client_id"),
|
|
|
|
|
preparedFilePath: text("prepared_file_path"),
|
2026-03-20 00:21:34 -06:00
|
|
|
/** Email addresses collected at prepare time: assigned client email + any CC addresses. */
|
|
|
|
|
emailAddresses: jsonb("email_addresses").$type<string[]>(),
|
2026-03-20 11:24:02 -06:00
|
|
|
signedFilePath: text("signed_file_path"),
|
|
|
|
|
pdfHash: text("pdf_hash"),
|
|
|
|
|
signedAt: timestamp("signed_at"),
|
feat(03-01): add clients and documents tables to Drizzle schema
- Add pgEnum import and documentStatusEnum (Draft, Sent, Viewed, Signed)
- Add clients table (id, name, email, createdAt, updatedAt)
- Add documents table (id, name, clientId FK, status enum, sentAt, createdAt)
- Generate migration 0001_watery_blindfold.sql and apply to local PostgreSQL
2026-03-19 16:17:26 -06:00
|
|
|
});
|
2026-03-19 21:44:17 -06:00
|
|
|
|
|
|
|
|
export const documentsRelations = relations(documents, ({ one }) => ({
|
|
|
|
|
client: one(clients, { fields: [documents.clientId], references: [clients.id] }),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
export const clientsRelations = relations(clients, ({ many }) => ({
|
|
|
|
|
documents: many(documents),
|
|
|
|
|
}));
|
2026-03-20 11:24:02 -06:00
|
|
|
|
|
|
|
|
export const auditEventTypeEnum = pgEnum('audit_event_type', [
|
|
|
|
|
'document_prepared',
|
|
|
|
|
'email_sent',
|
|
|
|
|
'link_opened',
|
|
|
|
|
'document_viewed',
|
|
|
|
|
'signature_submitted',
|
|
|
|
|
'pdf_hash_computed',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
export const signingTokens = pgTable('signing_tokens', {
|
|
|
|
|
jti: text('jti').primaryKey(),
|
|
|
|
|
documentId: text('document_id').notNull()
|
|
|
|
|
.references(() => documents.id, { onDelete: 'cascade' }),
|
|
|
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
|
|
|
expiresAt: timestamp('expires_at').notNull(),
|
|
|
|
|
usedAt: timestamp('used_at'),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const auditEvents = pgTable('audit_events', {
|
|
|
|
|
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
|
|
|
documentId: text('document_id').notNull()
|
|
|
|
|
.references(() => documents.id, { onDelete: 'cascade' }),
|
|
|
|
|
eventType: auditEventTypeEnum('event_type').notNull(),
|
|
|
|
|
ipAddress: text('ip_address'),
|
|
|
|
|
userAgent: text('user_agent'),
|
|
|
|
|
metadata: jsonb('metadata').$type<Record<string, unknown>>(),
|
|
|
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
|
|
|
});
|