Files
zpan/server/db/schema.ts
T

528 lines
21 KiB
TypeScript

import { sql } from 'drizzle-orm'
import { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'
import { organization } from './auth-schema'
export const matters = sqliteTable('matters', {
id: text('id').primaryKey(),
orgId: text('org_id').notNull(),
alias: text('alias').notNull().unique(),
name: text('name').notNull(),
type: text('type').notNull(),
size: integer('size').default(0),
dirtype: integer('dirtype').default(0),
parent: text('parent').notNull().default(''),
object: text('object').notNull().default(''),
storageId: text('storage_id').notNull(),
status: text('status').notNull().default('draft'), // draft, active, trashed
trashedAt: integer('trashed_at'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
})
export const webdavDeadProperties = sqliteTable(
'webdav_dead_properties',
{
id: text('id').primaryKey(),
orgId: text('org_id').notNull(),
resourcePath: text('resource_path').notNull(),
namespace: text('namespace').notNull(),
name: text('name').notNull(),
value: text('value').notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
},
(t) => [
uniqueIndex('webdav_dead_properties_resource_prop_uniq').on(t.orgId, t.resourcePath, t.namespace, t.name),
index('webdav_dead_properties_resource_idx').on(t.orgId, t.resourcePath),
],
)
export const webdavLocks = sqliteTable(
'webdav_locks',
{
id: text('id').primaryKey(),
token: text('token').notNull().unique(),
orgId: text('org_id').notNull(),
resourcePath: text('resource_path').notNull(),
owner: text('owner').notNull().default(''),
depth: text('depth').notNull().default('infinity'),
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
},
(t) => [
index('webdav_locks_resource_idx').on(t.orgId, t.resourcePath),
index('webdav_locks_expires_idx').on(t.expiresAt),
],
)
export const storages = sqliteTable('storages', {
id: text('id').primaryKey(),
title: text('title').notNull(),
mode: text('mode').notNull(),
bucket: text('bucket').notNull(),
endpoint: text('endpoint').notNull(),
region: text('region').notNull().default('auto'),
accessKey: text('access_key').notNull(),
secretKey: text('secret_key').notNull(),
filePath: text('file_path').notNull().default(''),
customHost: text('custom_host').default(''),
capacity: integer('capacity').notNull().default(0),
egressCreditBillingEnabled: integer('egress_credit_billing_enabled', { mode: 'boolean' }).notNull().default(false),
egressCreditUnitBytes: integer('egress_credit_unit_bytes').notNull().default(104857600),
egressCreditPerUnit: integer('egress_credit_per_unit').notNull().default(1),
used: integer('used').notNull().default(0),
status: text('status').notNull().default('active'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
})
export const orgQuotas = sqliteTable('org_quotas', {
id: text('id').primaryKey(),
orgId: text('org_id').notNull(),
quota: integer('quota').notNull().default(0),
used: integer('used').notNull().default(0),
trafficQuota: integer('traffic_quota').notNull().default(0),
trafficUsed: integer('traffic_used').notNull().default(0),
trafficPeriod: text('traffic_period').notNull().default('1970-01'),
})
export const cloudTrafficReports = sqliteTable(
'cloud_traffic_reports',
{
id: text('id').primaryKey(),
orgId: text('org_id').notNull(),
period: text('period').notNull(),
source: text('source').notNull(),
sourceId: text('source_id').notNull(),
eventId: text('event_id').notNull(),
bytes: integer('bytes').notNull(),
storageId: text('storage_id'),
unitBytes: integer('unit_bytes'),
creditsPerUnit: integer('credits_per_unit'),
status: text('status').notNull(),
error: text('error'),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
},
(t) => [
uniqueIndex('cloud_traffic_reports_event_uniq').on(t.eventId),
index('cloud_traffic_reports_org_period_idx').on(t.orgId, t.period),
index('cloud_traffic_reports_status_idx').on(t.status),
],
)
export const orgQuotaEntitlements = sqliteTable(
'org_quota_entitlements',
{
id: text('id').primaryKey(),
orgId: text('org_id').notNull(),
resourceType: text('resource_type').notNull(),
entitlementType: text('entitlement_type').notNull().default('grant'),
source: text('source').notNull(),
sourceId: text('source_id').notNull(),
bytes: integer('bytes').notNull(),
startsAt: integer('starts_at', { mode: 'timestamp_ms' }).notNull(),
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }),
status: text('status').notNull(),
metadata: text('metadata'),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
},
(t) => [
index('org_quota_entitlements_org_resource_idx').on(t.orgId, t.resourceType, t.status),
index('org_quota_entitlements_org_type_idx').on(t.orgId, t.resourceType, t.entitlementType, t.status),
uniqueIndex('org_quota_entitlements_active_plan_uniq')
.on(t.orgId, t.resourceType, t.entitlementType)
.where(sql`status = 'active' AND entitlement_type = 'plan'`),
uniqueIndex('org_quota_entitlements_source_resource_uniq').on(t.source, t.sourceId, t.resourceType),
],
)
export const webhookEvents = sqliteTable(
'webhook_events',
{
id: text('id').primaryKey(),
source: text('source').notNull().default('cloud'),
eventId: text('event_id').notNull(),
eventType: text('event_type').notNull().default('order.quota_changed'),
payloadHash: text('payload_hash').notNull(),
rawPayload: text('raw_payload').notNull(),
status: text('status').notNull(),
error: text('error'),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
processedAt: integer('processed_at', { mode: 'timestamp_ms' }),
},
(t) => [
uniqueIndex('webhook_events_source_event_uniq').on(t.source, t.eventId),
index('webhook_events_source_created_idx').on(t.source, t.createdAt),
index('webhook_events_status_idx').on(t.status),
],
)
export const inviteCodes = sqliteTable('invite_codes', {
id: text('id').primaryKey(),
code: text('code').notNull().unique(),
createdBy: text('created_by').notNull(),
usedBy: text('used_by'),
usedAt: integer('used_at', { mode: 'timestamp' }),
expiresAt: integer('expires_at', { mode: 'timestamp' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
})
export const siteInvitations = sqliteTable(
'site_invitations',
{
id: text('id').primaryKey(),
email: text('email').notNull(),
token: text('token').notNull().unique(),
invitedBy: text('invited_by').notNull(),
acceptedBy: text('accepted_by'),
acceptedAt: integer('accepted_at', { mode: 'timestamp' }),
revokedBy: text('revoked_by'),
revokedAt: integer('revoked_at', { mode: 'timestamp' }),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
},
(t) => [
index('site_invitations_email_idx').on(t.email),
index('site_invitations_created_idx').on(t.createdAt),
index('site_invitations_expires_idx').on(t.expiresAt),
],
)
export const systemOptions = sqliteTable('system_options', {
key: text('key').primaryKey(),
value: text('value').notNull().default(''),
public: integer('public', { mode: 'boolean' }).default(false),
})
export const licenseBindings = sqliteTable(
'license_bindings',
{
id: text('id').primaryKey(),
cloudBindingId: text('cloud_binding_id').notNull(),
cloudStoreId: text('cloud_store_id'),
instanceId: text('instance_id').notNull(),
cloudAccountId: text('cloud_account_id').notNull(),
cloudAccountEmail: text('cloud_account_email'),
status: text('status').notNull(),
refreshToken: text('refresh_token'),
cachedCertificate: text('cached_certificate'),
cachedCertificateExpiresAt: integer('cached_certificate_expires_at'),
boundAt: integer('bound_at').notNull(),
disconnectedAt: integer('disconnected_at'),
lastRefreshAt: integer('last_refresh_at'),
lastRefreshError: text('last_refresh_error'),
createdAt: integer('created_at').notNull(),
updatedAt: integer('updated_at').notNull(),
},
(t) => [
uniqueIndex('license_bindings_active_uniq').on(t.status).where(sql`status = 'active'`),
index('license_bindings_cloud_binding_idx').on(t.cloudBindingId),
index('license_bindings_instance_idx').on(t.instanceId),
],
)
export const teamInviteLinks = sqliteTable('team_invite_links', {
id: text('id').primaryKey(),
token: text('token').notNull().unique(),
organizationId: text('organization_id').notNull(),
role: text('role').notNull().default('member'),
inviterId: text('inviter_id').notNull(),
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
})
export const notifications = sqliteTable(
'notifications',
{
id: text('id').primaryKey(),
userId: text('user_id').notNull(),
type: text('type').notNull(), // e.g. 'share_received'
title: text('title').notNull(),
body: text('body').notNull().default(''),
refType: text('ref_type'), // e.g. 'share'
refId: text('ref_id'),
metadata: text('metadata'), // JSON string for extra context
readAt: integer('read_at', { mode: 'timestamp' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
},
(t) => [
index('notifications_user_created_idx').on(t.userId, t.createdAt),
index('notifications_user_read_idx').on(t.userId, t.readAt),
],
)
export const backgroundJobs = sqliteTable(
'background_jobs',
{
id: text('id').primaryKey(),
orgId: text('org_id').notNull(),
userId: text('user_id').notNull(),
type: text('type').notNull(),
status: text('status').notNull(),
targetFolder: text('target_folder'),
targetPath: text('target_path'),
metadata: text('metadata'),
inputBytes: integer('input_bytes').notNull().default(0),
outputBytes: integer('output_bytes').notNull().default(0),
processedBytes: integer('processed_bytes').notNull().default(0),
fileCount: integer('file_count').notNull().default(0),
currentFilename: text('current_filename'),
errorMessage: text('error_message'),
resultMetadata: text('result_metadata'),
retryable: integer('retryable', { mode: 'boolean' }).notNull().default(false),
cancelable: integer('cancelable', { mode: 'boolean' }).notNull().default(true),
retriedFromJobId: text('retried_from_job_id'),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
startedAt: integer('started_at', { mode: 'timestamp_ms' }),
finishedAt: integer('finished_at', { mode: 'timestamp_ms' }),
},
(t) => [
index('background_jobs_org_created_idx').on(t.orgId, t.createdAt),
index('background_jobs_org_status_idx').on(t.orgId, t.status),
index('background_jobs_org_type_idx').on(t.orgId, t.type),
],
)
export const downloaders = sqliteTable(
'downloaders',
{
id: text('id').primaryKey(),
name: text('name').notNull(),
tokenHash: text('token_hash').notNull(),
tokenJti: text('token_jti').notNull().unique(),
status: text('status').notNull().default('offline'),
enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
version: text('version').notNull().default('unknown'),
hostname: text('hostname').notNull().default('unknown'),
platform: text('platform').notNull().default('unknown'),
arch: text('arch').notNull().default('unknown'),
engine: text('engine').notNull().default('builtin'),
capabilities: text('capabilities').notNull().default('[]'),
maxConcurrentTasks: integer('max_concurrent_tasks').notNull().default(1),
currentTasks: integer('current_tasks').notNull().default(0),
downloadBps: integer('download_bps').notNull().default(0),
uploadBps: integer('upload_bps').notNull().default(0),
freeDiskBytes: integer('free_disk_bytes').notNull().default(0),
remoteDownloadCreditBillingEnabled: integer('remote_download_credit_billing_enabled', {
mode: 'boolean',
})
.notNull()
.default(false),
remoteDownloadCreditUnitBytes: integer('remote_download_credit_unit_bytes').notNull().default(104857600),
remoteDownloadCreditPerUnit: integer('remote_download_credit_per_unit').notNull().default(1),
lastHeartbeatAt: integer('last_heartbeat_at', { mode: 'timestamp_ms' }),
createdBy: text('created_by').notNull(),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
},
(t) => [
index('downloaders_status_idx').on(t.status),
index('downloaders_enabled_idx').on(t.enabled),
index('downloaders_created_idx').on(t.createdAt),
],
)
export const downloadTasks = sqliteTable(
'download_tasks',
{
id: text('id').primaryKey(),
orgId: text('org_id').notNull(),
createdByUserId: text('created_by_user_id').notNull(),
sourceType: text('source_type').notNull(),
sourceUri: text('source_uri').notNull(),
name: text('name'),
targetFolder: text('target_folder').notNull().default(''),
category: text('category'),
tags: text('tags').notNull().default('[]'),
assignedDownloaderId: text('assigned_downloader_id'),
status: text('status').notNull(),
downloadedBytes: integer('downloaded_bytes').notNull().default(0),
uploadedBytes: integer('uploaded_bytes').notNull().default(0),
totalBytes: integer('total_bytes'),
authorizedBytes: integer('authorized_bytes').notNull().default(0),
billedBytes: integer('billed_bytes').notNull().default(0),
billedCredits: integer('billed_credits').notNull().default(0),
billingStatus: text('billing_status').notNull().default('none'),
downloadBps: integer('download_bps').notNull().default(0),
uploadBps: integer('upload_bps').notNull().default(0),
errorMessage: text('error_message'),
resultObjectId: text('result_object_id'),
detail: text('detail'),
uploadTokenHash: text('upload_token_hash'),
uploadTokenJti: text('upload_token_jti'),
uploadTokenIssuedAt: integer('upload_token_issued_at', { mode: 'timestamp_ms' }),
uploadTokenExpiresAt: integer('upload_token_expires_at', { mode: 'timestamp_ms' }),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
assignedAt: integer('assigned_at', { mode: 'timestamp_ms' }),
startedAt: integer('started_at', { mode: 'timestamp_ms' }),
finishedAt: integer('finished_at', { mode: 'timestamp_ms' }),
},
(t) => [
index('download_tasks_org_created_idx').on(t.orgId, t.createdAt),
index('download_tasks_org_status_idx').on(t.orgId, t.status),
index('download_tasks_org_category_idx').on(t.orgId, t.category),
index('download_tasks_org_tags_idx').on(t.orgId, t.tags),
index('download_tasks_downloader_idx').on(t.assignedDownloaderId, t.status),
],
)
export const objectUploadSessions = sqliteTable(
'object_upload_sessions',
{
id: text('id').primaryKey(),
orgId: text('org_id').notNull(),
objectId: text('object_id').notNull(),
storageId: text('storage_id').notNull(),
storageKey: text('storage_key').notNull(),
uploadId: text('upload_id').notNull(),
partSize: integer('part_size').notNull(),
status: text('status').notNull(),
createdBy: text('created_by').notNull(),
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
},
(t) => [
index('object_upload_sessions_object_idx').on(t.orgId, t.objectId),
index('object_upload_sessions_expires_idx').on(t.expiresAt),
],
)
export const remoteDownloadUsageReports = sqliteTable(
'remote_download_usage_reports',
{
id: text('id').primaryKey(),
orgId: text('org_id').notNull(),
downloaderId: text('downloader_id').notNull(),
taskId: text('task_id').notNull(),
eventId: text('event_id').notNull().unique(),
unitIndex: integer('unit_index').notNull(),
unitBytes: integer('unit_bytes').notNull(),
creditsPerUnit: integer('credits_per_unit').notNull(),
status: text('status').notNull(),
error: text('error'),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
},
(t) => [
uniqueIndex('remote_download_usage_task_unit_uniq').on(t.taskId, t.unitIndex),
index('remote_download_usage_org_idx').on(t.orgId),
index('remote_download_usage_status_idx').on(t.status),
],
)
export const announcements = sqliteTable(
'announcements',
{
id: text('id').primaryKey(),
title: text('title').notNull(),
body: text('body').notNull().default(''),
status: text('status').notNull().default('draft'),
priority: integer('priority').notNull().default(0),
publishedAt: integer('published_at', { mode: 'timestamp' }),
expiresAt: integer('expires_at', { mode: 'timestamp' }),
createdBy: text('created_by').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
},
(t) => [
index('announcements_status_priority_idx').on(t.status, t.priority),
index('announcements_published_idx').on(t.publishedAt),
],
)
export const activityEvents = sqliteTable('activity_events', {
id: text('id').primaryKey(),
orgId: text('org_id').notNull(),
userId: text('user_id').notNull(),
action: text('action').notNull(), // 'upload', 'create', 'delete', 'rename', 'move', 'restore'
targetType: text('target_type').notNull(), // 'file', 'folder'
targetId: text('target_id'),
targetName: text('target_name').notNull(),
metadata: text('metadata'), // JSON
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
})
export const shares = sqliteTable(
'shares',
{
id: text('id').primaryKey(),
token: text('token').notNull().unique(),
kind: text('kind').notNull(), // 'landing' | 'direct'
matterId: text('matter_id').notNull(),
orgId: text('org_id').notNull(),
creatorId: text('creator_id').notNull(),
passwordHash: text('password_hash'),
expiresAt: integer('expires_at', { mode: 'timestamp' }),
downloadLimit: integer('download_limit'),
views: integer('views').notNull().default(0),
downloads: integer('downloads').notNull().default(0),
status: text('status').notNull().default('active'), // 'active' | 'revoked'
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
},
(t) => [index('shares_creator_status_created_idx').on(t.creatorId, t.status, t.createdAt)],
)
export const shareRecipients = sqliteTable(
'share_recipients',
{
id: text('id').primaryKey(),
shareId: text('share_id').notNull(),
recipientUserId: text('recipient_user_id'),
recipientEmail: text('recipient_email'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
},
(t) => [
index('share_recipients_share_id_idx').on(t.shareId),
index('share_recipients_user_id_idx').on(t.recipientUserId),
],
)
// image_hosting_configs — per-org singleton; row exists => feature enabled
export const imageHostingConfigs = sqliteTable('image_hosting_configs', {
orgId: text('org_id')
.primaryKey()
.references(() => organization.id, { onDelete: 'cascade' }),
customDomain: text('custom_domain').unique(),
cfHostnameId: text('cf_hostname_id'),
domainVerifiedAt: integer('domain_verified_at', { mode: 'timestamp_ms' }),
refererAllowlist: text('referer_allowlist'), // JSON array of strings; null/empty => allow all
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
})
// image_hostings — one row per hosted image
export const imageHostings = sqliteTable(
'image_hostings',
{
id: text('id').primaryKey(), // nanoid(12)
orgId: text('org_id')
.notNull()
.references(() => organization.id, { onDelete: 'cascade' }),
token: text('token').notNull().unique(), // "ih_" + nanoid(10)
path: text('path').notNull(), // virtual path e.g. "blog/2026/04/shot.png"
storageId: text('storage_id')
.notNull()
.references(() => storages.id),
storageKey: text('storage_key').notNull(), // "ih/<orgId>/<id>.<ext>"
size: integer('size').notNull(),
mime: text('mime').notNull(),
width: integer('width'),
height: integer('height'),
status: text('status').notNull().default('draft'), // 'draft' | 'active'
accessCount: integer('access_count').notNull().default(0),
lastAccessedAt: integer('last_accessed_at', { mode: 'timestamp_ms' }),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
},
(t) => [
uniqueIndex('image_hostings_org_path_uniq').on(t.orgId, t.path),
index('image_hostings_org_created_idx').on(t.orgId, t.createdAt),
index('image_hostings_token_idx').on(t.token),
],
)