All files / web/src/db/schema print-service.ts

100% Statements 94/94
100% Branches 9/9
100% Functions 3/3
100% Lines 94/94

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 952x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 43x 43x 43x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 42x 42x 42x 42x 2x 2x 2x 2x 2x 2x  
import { createId } from '@paralleldrive/cuid2'
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { users } from './users'
 
/**
 * Print-service connections — per-account pairings with a THH print service
 * (Abacus Studio Phase 2a, ticket #8).
 *
 * Each row is one paired connection owned by exactly one user (the tenant key
 * for everything downstream: proxy reads, job ownership, doorbell fan-out).
 * The bearer token and the webhook ring secret are encrypted at rest with
 * `@/lib/secret-box` — the browser-facing PrintConnection shape never carries
 * either sealed field.
 */
export const printServiceConnections = sqliteTable(
  'print_service_connections',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => createId()),
 
    /** Owning account — the tenant boundary for reads, jobs, and doorbell emits */
    userId: text('user_id')
      .notNull()
      .references(() => users.id, { onDelete: 'cascade' }),
 
    /** Human-readable connection name (e.g. "Home X1C") */
    name: text('name').notNull(),
 
    /** Print-service origin, e.g. "https://things.haunt.house" (no trailing slash) */
    origin: text('origin').notNull(),
 
    /** Durable bearer token from POST /pair, sealed with secret-box */
    tokenSealed: text('token_sealed').notNull(),
 
    /** Per-connection webhook ring secret, sealed with secret-box (null until webhook registered) */
    ringSecretSealed: text('ring_secret_sealed'),
 
    /** When PUT /webhook last succeeded for this connection (null = no doorbell) */
    webhookRegisteredAt: integer('webhook_registered_at', { mode: 'timestamp' }),
 
    createdAt: integer('created_at', { mode: 'timestamp' })
      .notNull()
      .$defaultFn(() => new Date()),
 
    updatedAt: integer('updated_at', { mode: 'timestamp' })
      .notNull()
      .$defaultFn(() => new Date()),
  },
  (table) => ({
    /** Roster lookups are always per-owner */
    userIdIdx: index('print_service_connections_user_id_idx').on(table.userId),
  })
)
 
/**
 * Print-job ownership map. The print service stays the single source of truth
 * for job state — this table only records which connection/user a submitted
 * job belongs to, so `jobs/*` proxy reads can be authorized and doorbell rings
 * resolved to the owning tenant.
 */
export const printJobs = sqliteTable(
  'print_jobs',
  {
    /** Job id as returned by the print service's 202 submit response */
    jobId: text('job_id').primaryKey(),
 
    connectionId: text('connection_id')
      .notNull()
      .references(() => printServiceConnections.id, { onDelete: 'cascade' }),
 
    /** Denormalized owner — matches the connection's userId at submit time */
    userId: text('user_id')
      .notNull()
      .references(() => users.id, { onDelete: 'cascade' }),
 
    /** Printer the job was submitted to (service-side printer id) */
    printerId: text('printer_id').notNull(),
 
    createdAt: integer('created_at', { mode: 'timestamp' })
      .notNull()
      .$defaultFn(() => new Date()),
  },
  (table) => ({
    /** "My active jobs" listings */
    userIdIdx: index('print_jobs_user_id_idx').on(table.userId),
    connectionIdIdx: index('print_jobs_connection_id_idx').on(table.connectionId),
  })
)
 
export type PrintServiceConnection = typeof printServiceConnections.$inferSelect
export type NewPrintServiceConnection = typeof printServiceConnections.$inferInsert
export type PrintJob = typeof printJobs.$inferSelect
export type NewPrintJob = typeof printJobs.$inferInsert