import { sqliteTable, text, integer, index, uniqueIndex, check } from 'drizzle-orm/sqlite-core'; import { sql } from 'drizzle-orm'; export const users = sqliteTable('users', { id: text().primaryKey(), email: text().unique(), displayName: text().notNull(), passwordHash: text(), role: text({ enum: ['user', 'admin'] }) .notNull() .default('user'), status: text({ enum: ['pending', 'active', 'suspended', 'deleted'] }).notNull(), verifiedAt: integer(), createdAt: integer().notNull(), receivePolicy: text({ enum: ['requests', 'allowed', 'none'] }) .notNull() .default('requests'), }); export const accountSessions = sqliteTable( 'account_sessions', { id: text().primaryKey(), userId: text() .notNull() .references(() => users.id), tokenHash: text().notNull().unique(), createdAt: integer().notNull(), lastSeen: integer().notNull(), expiresAt: integer().notNull(), revokedAt: integer(), }, (t) => [index('account_session_user').on(t.userId, t.expiresAt)], ); export const accountTokens = sqliteTable( 'account_tokens', { id: text().primaryKey(), userId: text() .notNull() .references(() => users.id), purpose: text({ enum: ['verify', 'reset', 'email-change'] }).notNull(), tokenHash: text().notNull().unique(), value: text(), createdAt: integer().notNull(), expiresAt: integer().notNull(), usedAt: integer(), }, (t) => [index('account_token_expiry').on(t.expiresAt)], ); export const rules = sqliteTable( 'user_rules', { id: text().primaryKey(), userId: text() .notNull() .references(() => users.id), targetUserId: text() .notNull() .references(() => users.id), action: text({ enum: ['allow', 'block'] }).notNull(), }, (t) => [ uniqueIndex('user_rule_pair').on(t.userId, t.targetUserId), check('rule_not_self', sql`${t.userId} <> ${t.targetUserId}`), ], ); export const guests = sqliteTable( 'guest_sessions', { id: text().primaryKey(), tokenHash: text().notNull().unique(), expiresAt: integer().notNull(), revokedAt: integer(), createdAt: integer().notNull(), }, (t) => [index('guest_expiry').on(t.expiresAt)], ); export const devices = sqliteTable( 'browser_devices', { id: text().primaryKey(), guestId: text() .notNull() .unique() .references(() => guests.id), name: text().notNull(), code: text().notNull().unique(), discoverable: integer({ mode: 'boolean' }).notNull().default(true), lastSeen: integer().notNull(), }, (t) => [check('device_name_length', sql`length(${t.name}) between 1 and 40`)], ); export const spaces = sqliteTable( 'connection_spaces', { id: text().primaryKey(), creatorGuestId: text() .notNull() .references(() => guests.id), tokenHash: text().notNull().unique(), codeHash: text().notNull().unique(), approvalRequired: integer({ mode: 'boolean' }).notNull(), expiresAt: integer().notNull(), closedAt: integer(), createdAt: integer().notNull(), }, (t) => [index('space_expiry').on(t.expiresAt)], ); export const members = sqliteTable( 'space_members', { id: text().primaryKey(), spaceId: text() .notNull() .references(() => spaces.id), deviceId: text() .notNull() .references(() => devices.id), status: text({ enum: ['pending', 'approved', 'removed'] }).notNull(), joinedAt: integer().notNull(), }, (t) => [ uniqueIndex('space_device').on(t.spaceId, t.deviceId), uniqueIndex('member_single_active_device') .on(t.deviceId) .where(sql`${t.status} IN ('pending', 'approved')`), ], ); export const blocks = sqliteTable( 'guest_blocks', { id: text().primaryKey(), guestId: text() .notNull() .references(() => guests.id), targetGuestId: text() .notNull() .references(() => guests.id), }, (t) => [uniqueIndex('guest_block_pair').on(t.guestId, t.targetGuestId)], ); export const requests = sqliteTable( 'transfer_requests', { id: text().primaryKey(), mode: text({ enum: ['local', 'email'] }) .notNull() .default('local'), senderGuestId: text().references(() => guests.id), recipientGuestId: text().references(() => guests.id), senderUserId: text().references(() => users.id), recipientUserId: text().references(() => users.id), kind: text({ enum: ['text', 'url', 'file'] }).notNull(), byteSize: integer().notNull(), manifest: text(), note: text(), count: integer().notNull(), status: text({ enum: ['pending', 'accepted', 'rejected', 'expired', 'cancelled'] }).notNull(), createdAt: integer().notNull(), expiresAt: integer().notNull(), acceptedAt: integer(), rejectedAt: integer(), startBy: integer(), }, (t) => [ index('requests_sender_time').on(t.senderGuestId, t.createdAt), index('requests_recipient_time').on(t.recipientGuestId, t.createdAt), index('requests_expiry').on(t.status, t.expiresAt), check( 'request_different_guests', sql`${t.mode} <> 'local' OR ${t.senderGuestId} <> ${t.recipientGuestId}`, ), check( 'request_sender_subject', sql`(${t.senderGuestId} IS NOT NULL) + (${t.senderUserId} IS NOT NULL) = 1`, ), check( 'request_recipient_subject', sql`(${t.recipientGuestId} IS NOT NULL) + (${t.recipientUserId} IS NOT NULL) = 1`, ), check( 'request_mode_subjects', sql`(${t.mode} = 'local' AND ${t.senderGuestId} IS NOT NULL AND ${t.recipientGuestId} IS NOT NULL) OR (${t.mode} = 'email' AND ${t.senderUserId} IS NOT NULL AND ${t.recipientUserId} IS NOT NULL)`, ), index('requests_sender_user').on(t.senderUserId, t.createdAt), index('requests_recipient_user').on(t.recipientUserId, t.createdAt), ], ); export const batches = sqliteTable('transfer_batches', { id: text().primaryKey(), requestId: text() .notNull() .unique() .references(() => requests.id), status: text({ enum: ['waiting', 'uploading', 'stored', 'failed'] }).notNull(), storedAt: integer(), }); export const files = sqliteTable( 'files', { id: text().primaryKey(), requestId: text() .notNull() .references(() => requests.id), ownerGuestId: text().references(() => guests.id), ownerUserId: text().references(() => users.id), position: integer().notNull(), storageKey: text().notNull().unique(), name: text().notNull(), type: text().notNull(), size: integer().notNull(), checksum: text(), status: text({ enum: ['pending', 'uploading', 'stored', 'failed', 'cleaning'] }).notNull(), uploadId: text(), uploadKey: text(), updatedAt: integer().notNull(), }, (t) => [ uniqueIndex('request_file_position').on(t.requestId, t.position), index('files_owner_state').on(t.ownerGuestId, t.status), check('file_nonnegative_size', sql`${t.size} >= 0`), check( 'file_owner_subject', sql`(${t.ownerGuestId} IS NOT NULL) + (${t.ownerUserId} IS NOT NULL) = 1`, ), ], ); export const fileUploadContexts = sqliteTable( 'file_upload_contexts', { fileId: text() .primaryKey() .references(() => files.id, { onDelete: 'cascade' }), context: text().notNull(), expiresAt: integer().notNull(), }, (t) => [index('file_upload_context_expiry').on(t.expiresAt)], ); export const reservations = sqliteTable( 'quota_reservations', { id: text().primaryKey(), fileId: text() .notNull() .unique() .references(() => files.id), userId: text().references(() => users.id), guestId: text().references(() => guests.id), bytes: integer().notNull(), expiresAt: integer().notNull(), }, (t) => [ check('reservations_subject', sql`(${t.guestId} IS NOT NULL) + (${t.userId} IS NOT NULL) = 1`), index('quota_guest_expiry').on(t.guestId, t.expiresAt), check('reservation_nonnegative', sql`${t.bytes} >= 0`), ], ); export const deletions = sqliteTable( 'deletion_operations', { id: text().primaryKey(), userId: text().references(() => users.id), guestId: text().references(() => guests.id), undoUntil: integer().notNull(), createdAt: integer().notNull(), restoredAt: integer(), }, (t) => [ check('deletion_subject', sql`(${t.guestId} IS NOT NULL) + (${t.userId} IS NOT NULL) = 1`), ], ); export const deletionTargets = sqliteTable('deletion_targets', { id: text().primaryKey(), operationId: text() .notNull() .references(() => deletions.id), accessId: text().notNull(), }); export const items = sqliteTable( 'items', { id: text().primaryKey(), batchId: text() .notNull() .references(() => batches.id), kind: text({ enum: ['text', 'url', 'file'] }).notNull(), text: text(), url: text(), fileId: text().references(() => files.id), createdAt: integer().notNull(), expiresAt: integer().notNull(), }, (t) => [ index('items_expiry').on(t.expiresAt), check( 'item_payload', sql`(${t.kind} = 'text' AND ${t.text} IS NOT NULL AND ${t.url} IS NULL AND ${t.fileId} IS NULL) OR (${t.kind} = 'url' AND ${t.url} IS NOT NULL AND ${t.text} IS NULL AND ${t.fileId} IS NULL) OR (${t.kind} = 'file' AND ${t.fileId} IS NOT NULL AND ${t.text} IS NULL AND ${t.url} IS NULL)`, ), ], ); export const access = sqliteTable( 'item_access', { id: text().primaryKey(), itemId: text() .notNull() .references(() => items.id), userId: text().references(() => users.id), guestId: text().references(() => guests.id), role: text({ enum: ['sender', 'recipient', 'self'] }).notNull(), read: integer({ mode: 'boolean' }).notNull().default(false), deletedAt: integer(), favorite: integer({ mode: 'boolean' }).notNull().default(false), note: text(), }, (t) => [ check('access_subject', sql`(${t.guestId} IS NOT NULL) + (${t.userId} IS NOT NULL) = 1`), uniqueIndex('access_subject_role').on(t.itemId, t.guestId, t.role), uniqueIndex('access_account_role').on(t.itemId, t.userId, t.role), index('access_account').on(t.userId, t.deletedAt), index('access_guest').on(t.guestId, t.deletedAt), ], ); export const receiptAccounts = sqliteTable('request_receipt_accounts', { requestId: text() .primaryKey() .references(() => requests.id), userId: text() .notNull() .references(() => users.id), sessionId: text() .notNull() .references(() => accountSessions.id), }); export const vaultTransfers = sqliteTable('vault_transfers', { requestId: text() .primaryKey() .references(() => requests.id), accessId: text() .notNull() .references(() => access.id), userId: text() .notNull() .references(() => users.id), sessionId: text() .notNull() .references(() => accountSessions.id), }); export const idempotency = sqliteTable( 'idempotency_records', { id: text().primaryKey(), userId: text().references(() => users.id), guestId: text().references(() => guests.id), operation: text().notNull(), key: text().notNull(), payloadHash: text().notNull(), response: text().notNull(), createdAt: integer().notNull(), }, (t) => [ uniqueIndex('idempotency_scope').on(t.guestId, t.operation, t.key), uniqueIndex('account_idempotency_scope').on(t.userId, t.operation, t.key), check('idempotency_subject', sql`(${t.guestId} IS NOT NULL) + (${t.userId} IS NOT NULL) = 1`), ], ); export const adminSettings = sqliteTable('admin_settings', { name: text().primaryKey(), value: text().notNull(), updatedAt: integer().notNull(), }); export const adminAudit = sqliteTable( 'admin_audit', { id: text().primaryKey(), actorUserId: text().references(() => users.id, { onDelete: 'set null' }), action: text().notNull(), details: text().notNull(), createdAt: integer().notNull(), }, (t) => [index('admin_audit_created').on(t.createdAt)], );