382 lines
12 KiB
TypeScript
382 lines
12 KiB
TypeScript
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)],
|
|
);
|