Initial hop. implementation and UGREEN NAS deployment guide
This commit is contained in:
354
apps/server/src/db/schema.ts
Normal file
354
apps/server/src/db/schema.ts
Normal file
@@ -0,0 +1,354 @@
|
||||
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 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`),
|
||||
],
|
||||
);
|
||||
Reference in New Issue
Block a user