feat(db): migrate Prisma datasource postgres -> sqlite (native, file-backed)
Drops the shared sql-0 postgres dependency in favor of an embedded SQLite
file (DATABASE_URL=file:/data/<db>), WAL-replicated to SeaweedFS by the
operator persistence sidecar — same pattern as esign (Documenso).
Schema changes for the sqlite connector:
- datasource provider postgresql -> sqlite; drop directUrl/shadowDatabaseUrl
- 18 enums -> String (values preserved as string defaults)
- scalar-list fields (String[]/Int[]) -> Json @default("[]") (arrays
round-trip through Prisma Json at runtime; next.config ignoreBuildErrors
keeps the type-only churn out of the build)
- strip pg-native @db.* attributes (@db.Text/@db.Timestamp)
- startup: prisma migrate deploy -> prisma db push (pg migration history is
postgres-specific; db push reconciles the sqlite schema idempotently)
CI: amd64-only (DOKS has no arm64); deploy:false — rollout is operator-managed
via the universe Service CR, never `kubectl set image`.
Verified: `prisma validate` + `prisma db push` materialize all 62 tables on a
fresh sqlite file. Source DB is empty (0 app rows), so cutover is data-safe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2952083ef0
commit
0aaaa1fa7e
@@ -16,12 +16,9 @@ jobs:
|
||||
uses: hanzoai/.github/.github/workflows/docker-build.yml@main
|
||||
with:
|
||||
image: ghcr.io/hanzoai/dataroom
|
||||
# Native multi-arch builds on canonical arcd labels.
|
||||
# amd64 → [self-hosted, linux, amd64] (matches evo).
|
||||
# arm64 → [self-hosted, linux, arm64] (matches spark).
|
||||
runner-arm64: '["self-hosted","linux","arm64"]'
|
||||
platforms: linux/amd64,linux/arm64
|
||||
deploy: true
|
||||
deploy-deployment: dataroom
|
||||
deploy-namespace: hanzo
|
||||
# amd64-only: DOKS has no arm64 droplets (arm64 build paused).
|
||||
platforms: linux/amd64
|
||||
# Deploy is operator-managed via universe Service CR (services.hanzo.ai/dataroom).
|
||||
# CI only builds+pushes the image; never `kubectl set image` (operator reverts it).
|
||||
deploy: false
|
||||
secrets: inherit
|
||||
|
||||
+4
-1
@@ -44,4 +44,7 @@ COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.bin ./node_modules/
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
ENV PORT=3000
|
||||
CMD ["sh", "-c", "node_modules/.bin/prisma migrate deploy --schema prisma/schema/schema.prisma && node server.js"]
|
||||
# SQLite: reconcile schema on startup via `db push` (Prisma has no migrate-deploy
|
||||
# path for the sqlite provider here; the pg migration history under prisma/migrations
|
||||
# is postgres-specific and unused). Schema folder = prismaSchemaFolder preview.
|
||||
CMD ["sh", "-c", "node_modules/.bin/prisma db push --schema prisma/schema --skip-generate --accept-data-loss && node server.js"]
|
||||
|
||||
@@ -2,7 +2,7 @@ model DocumentAnnotation {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
content Json // Rich text content stored as JSON (Tiptap/ProseMirror format)
|
||||
pages Int[] // Array of page numbers this annotation applies to
|
||||
pages Json @default("[]")// Array of page numbers this annotation applies to
|
||||
documentId String
|
||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
teamId String
|
||||
|
||||
@@ -4,7 +4,7 @@ model Conversation {
|
||||
isEnabled Boolean @default(true)
|
||||
|
||||
// Visibility control
|
||||
visibilityMode ConversationVisibility @default(PRIVATE)
|
||||
visibilityMode String @default("PRIVATE")
|
||||
|
||||
// Core relationships
|
||||
dataroomId String
|
||||
@@ -56,19 +56,8 @@ model Conversation {
|
||||
}
|
||||
|
||||
// Define conversation visibility options
|
||||
enum ConversationVisibility {
|
||||
PRIVATE // Only visible to participants and team members
|
||||
PUBLIC_LINK // Visible to all viewers with access to the specific link
|
||||
PUBLIC_GROUP // Visible to all viewers in the specific group
|
||||
PUBLIC_DOCUMENT // Visible to all viewers with access to the document, across any link
|
||||
PUBLIC_DATAROOM // Visible to all viewers with access to the dataroom
|
||||
}
|
||||
|
||||
// Define participant roles
|
||||
enum ParticipantRole {
|
||||
OWNER // Created the conversation
|
||||
PARTICIPANT // Joined the conversation later
|
||||
}
|
||||
|
||||
// Track participants in a conversation (including the owner)
|
||||
model ConversationParticipant {
|
||||
@@ -77,7 +66,7 @@ model ConversationParticipant {
|
||||
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
||||
|
||||
// Role in the conversation
|
||||
role ParticipantRole @default(PARTICIPANT)
|
||||
role String @default("PARTICIPANT")
|
||||
|
||||
// Participant can be either a viewer or a user
|
||||
viewerId String?
|
||||
@@ -184,15 +173,15 @@ model DataroomFaqItem {
|
||||
publishedByUser User @relation(fields: [publishedByUserId], references: [id], onDelete: Cascade)
|
||||
|
||||
// Visibility and status
|
||||
visibilityMode FaqVisibility @default(PUBLIC_DATAROOM)
|
||||
status FaqStatus @default(PUBLISHED)
|
||||
visibilityMode String @default("PUBLIC_DATAROOM")
|
||||
status String @default("PUBLISHED")
|
||||
isAnonymized Boolean @default(true)
|
||||
|
||||
// Analytics
|
||||
viewCount Int @default(0)
|
||||
|
||||
// Optional categorization
|
||||
tags String[] @default([])
|
||||
tags Json @default("[]")
|
||||
|
||||
// Metadata
|
||||
documentPageNumber Int? // Optional: specific page reference
|
||||
@@ -213,15 +202,5 @@ model DataroomFaqItem {
|
||||
}
|
||||
|
||||
// Define FAQ visibility options
|
||||
enum FaqVisibility {
|
||||
PUBLIC_DATAROOM // Visible to all dataroom visitors
|
||||
PUBLIC_LINK // Visible only to specific link visitors
|
||||
PUBLIC_DOCUMENT // Visible only when viewing specific document
|
||||
}
|
||||
|
||||
// Define FAQ status
|
||||
enum FaqStatus {
|
||||
DRAFT // Not yet published
|
||||
PUBLISHED // Live and visible to visitors
|
||||
ARCHIVED // No longer visible but kept for reference
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ model Dataroom {
|
||||
enableChangeNotifications Boolean @default(false)
|
||||
|
||||
// unified permission strategy
|
||||
defaultPermissionStrategy DefaultPermissionStrategy @default(INHERIT_FROM_PARENT)
|
||||
defaultPermissionStrategy String @default("INHERIT_FROM_PARENT")
|
||||
|
||||
// bulk download setting
|
||||
allowBulkDownload Boolean @default(true) // Allow bulk download of entire dataroom
|
||||
@@ -127,7 +127,7 @@ model ViewerGroup {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
members ViewerGroupMembership[]
|
||||
domains String[]
|
||||
domains Json@default("[]")
|
||||
links Link[]
|
||||
accessControls ViewerGroupAccessControls[]
|
||||
allowAll Boolean @default(false)
|
||||
@@ -171,7 +171,7 @@ model ViewerGroupAccessControls {
|
||||
|
||||
// Access control for items (documents or dataroom items)
|
||||
itemId String // This can be a document ID or a dataroom item ID
|
||||
itemType ItemType // Enum: DATAROOM_DOCUMENT, DATAROOM_FOLDER
|
||||
itemType String // Enum: DATAROOM_DOCUMENT, DATAROOM_FOLDER
|
||||
|
||||
// Granular permissions
|
||||
canView Boolean @default(true)
|
||||
@@ -184,16 +184,7 @@ model ViewerGroupAccessControls {
|
||||
@@index([groupId])
|
||||
}
|
||||
|
||||
enum ItemType {
|
||||
DATAROOM_DOCUMENT
|
||||
DATAROOM_FOLDER
|
||||
}
|
||||
|
||||
enum DefaultPermissionStrategy {
|
||||
INHERIT_FROM_PARENT
|
||||
ASK_EVERY_TIME
|
||||
HIDDEN_BY_DEFAULT
|
||||
}
|
||||
|
||||
model PermissionGroup {
|
||||
id String @id @default(cuid())
|
||||
@@ -222,7 +213,7 @@ model PermissionGroupAccessControls {
|
||||
|
||||
// Access control for items (documents or dataroom items)
|
||||
itemId String // This can be a document ID or a dataroom item ID
|
||||
itemType ItemType // Enum: DATAROOM_DOCUMENT, DATAROOM_FOLDER
|
||||
itemType String // Enum: DATAROOM_DOCUMENT, DATAROOM_FOLDER
|
||||
|
||||
// Granular permissions
|
||||
canView Boolean @default(true)
|
||||
|
||||
@@ -6,7 +6,7 @@ model Document {
|
||||
originalFile String? // This should be a reference to the original file like pptx, xlsx, etc. (S3, Google Cloud Storage, etc.)
|
||||
type String? // This should be a reference to the file type (pdf, sheet, etc.)
|
||||
contentType String? // This should be the actual contentType of the file like application/pdf, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, etc.
|
||||
storageType DocumentStorageType @default(VERCEL_BLOB)
|
||||
storageType String @default("VERCEL_BLOB")
|
||||
numPages Int? // This should be a reference to the number of pages in the document
|
||||
owner User? @relation(fields: [ownerId], references: [id], onDelete: SetNull)
|
||||
teamId String
|
||||
@@ -53,7 +53,7 @@ model DocumentVersion {
|
||||
type String? // This should be a reference to the file type (pdf, docx, etc.)
|
||||
contentType String? // This should be the actual contentType of the file like application/pdf, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, etc.
|
||||
fileSize BigInt? // This should be the size of the file in bytes
|
||||
storageType DocumentStorageType @default(VERCEL_BLOB)
|
||||
storageType String @default("VERCEL_BLOB")
|
||||
numPages Int? // This should be a reference to the number of pages in the document
|
||||
isPrimary Boolean @default(false) // Indicates if this is the primary version
|
||||
isVertical Boolean @default(false) // Indicates if the document is vertical (portrait) or not (landscape)
|
||||
@@ -77,11 +77,11 @@ model DocumentPage {
|
||||
version DocumentVersion @relation(fields: [versionId], references: [id], onDelete: Cascade)
|
||||
versionId String
|
||||
pageNumber Int // e.g., 1, 2, 3 for
|
||||
embeddedLinks String[]
|
||||
embeddedLinks Json@default("[]")
|
||||
pageLinks Json? // This will store the page links data: [{href: "https://example.com", coords: "0,0,100,100"}]
|
||||
metadata Json? // This will store the page metadata: {originalWidth: 100, origianlHeight: 100, scaledWidth: 50, scaledHeight: 50, scaleFactor: 2}
|
||||
file String // This should be a reference to where the file / page is stored (S3, Google Cloud Storage, etc.)
|
||||
storageType DocumentStorageType @default(VERCEL_BLOB)
|
||||
storageType String @default("VERCEL_BLOB")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -89,10 +89,6 @@ model DocumentPage {
|
||||
@@index([versionId])
|
||||
}
|
||||
|
||||
enum DocumentStorageType {
|
||||
S3_PATH
|
||||
VERCEL_BLOB
|
||||
}
|
||||
|
||||
model Folder {
|
||||
id String @id @default(cuid())
|
||||
|
||||
@@ -17,8 +17,8 @@ model jackson_store {
|
||||
iv String?
|
||||
tag String?
|
||||
namespace String?
|
||||
createdAt DateTime @default(now()) @db.Timestamp(0)
|
||||
modifiedAt DateTime? @db.Timestamp(0)
|
||||
createdAt DateTime @default(now())
|
||||
modifiedAt DateTime?
|
||||
|
||||
@@index([namespace], map: "_jackson_store_namespace")
|
||||
}
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
enum LinkType {
|
||||
DOCUMENT_LINK
|
||||
DATAROOM_LINK
|
||||
WORKFLOW_LINK
|
||||
}
|
||||
|
||||
enum LinkAudienceType {
|
||||
GENERAL
|
||||
GROUP
|
||||
TEAM
|
||||
}
|
||||
|
||||
model Link {
|
||||
id String @id @default(cuid())
|
||||
@@ -16,14 +6,14 @@ model Link {
|
||||
documentId String? // This can be nullable, representing links without documents
|
||||
dataroom Dataroom? @relation(fields: [dataroomId], references: [id], onDelete: Cascade)
|
||||
dataroomId String? // This can be nullable, representing links without datarooms
|
||||
linkType LinkType @default(DOCUMENT_LINK) // This will store the type of the link
|
||||
linkType String @default("DOCUMENT_LINK") // This will store the type of the link
|
||||
url String? @unique
|
||||
name String? // Link name
|
||||
slug String? // Link slug for pretty URLs
|
||||
expiresAt DateTime? // Optional expiration date
|
||||
password String? // Optional password for link protection
|
||||
allowList String[] // Array of emails and domains allowed to view the document
|
||||
denyList String[] // Array of emails and domains denied to view the document
|
||||
allowList Json @default("[]")// Array of emails and domains allowed to view the document
|
||||
denyList Json @default("[]")// Array of emails and domains denied to view the document
|
||||
emailProtected Boolean @default(true) // Optional email protection
|
||||
emailAuthenticated Boolean @default(false) // Optional email authentication flag
|
||||
allowDownload Boolean? @default(false) // Optional give user a option to allow to download the document
|
||||
@@ -48,7 +38,7 @@ model Link {
|
||||
watermarkConfig Json? // This will store the watermark configuration: {text: "Confidential", isTiled: false, color: "#000000", fontSize: 12, opacity: 0.5, rotation: 30, position: "top-right"}
|
||||
|
||||
// group links
|
||||
audienceType LinkAudienceType @default(GENERAL) // This will store the audience type of the link
|
||||
audienceType String @default("GENERAL") // This will store the audience type of the link
|
||||
groupId String?
|
||||
group ViewerGroup? @relation(fields: [groupId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@ -129,9 +119,9 @@ model LinkPreset {
|
||||
emailAuthenticated Boolean? @default(false)
|
||||
allowDownload Boolean? @default(false)
|
||||
enableAllowList Boolean? @default(false)
|
||||
allowList String[]
|
||||
allowList Json@default("[]")
|
||||
enableDenyList Boolean? @default(false)
|
||||
denyList String[]
|
||||
denyList Json@default("[]")
|
||||
expiresIn Int? // how many days from link creation until it expires in seconds
|
||||
enableScreenshotProtection Boolean? @default(false)
|
||||
expiresAt DateTime?
|
||||
@@ -158,21 +148,11 @@ model LinkPreset {
|
||||
@@index([teamId])
|
||||
}
|
||||
|
||||
enum CustomFieldType {
|
||||
SHORT_TEXT
|
||||
LONG_TEXT
|
||||
NUMBER
|
||||
PHONE_NUMBER
|
||||
URL
|
||||
CHECKBOX
|
||||
SELECT
|
||||
MULTI_SELECT
|
||||
}
|
||||
|
||||
model VisitorGroup {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
emails String[] // List of emails and domains (e.g., "user@example.com", "@example.org")
|
||||
emails Json @default("[]")// List of emails and domains (e.g., "user@example.com", "@example.org")
|
||||
|
||||
teamId String
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
@@ -203,7 +183,7 @@ model CustomField {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
type CustomFieldType
|
||||
type String
|
||||
identifier String
|
||||
label String
|
||||
placeholder String?
|
||||
|
||||
+11
-39
@@ -1,8 +1,6 @@
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("POSTGRES_PRISMA_URL") // uses connection pooling
|
||||
directUrl = env("POSTGRES_PRISMA_URL_NON_POOLING") // uses a direct connection
|
||||
shadowDatabaseUrl = env("POSTGRES_PRISMA_SHADOW_URL") // used for migrations
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
@@ -16,12 +14,12 @@ model Account {
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String? @db.Text
|
||||
access_token String? @db.Text
|
||||
refresh_token String?
|
||||
access_token String?
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? @db.Text
|
||||
id_token String?
|
||||
session_state String?
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
@@ -128,10 +126,10 @@ model View {
|
||||
verified Boolean @default(false) // Whether the viewer email has been verified
|
||||
viewedAt DateTime @default(now())
|
||||
downloadedAt DateTime? // This is the time the document was downloaded
|
||||
downloadType DownloadType? // Type of download: SINGLE, BULK, or FOLDER
|
||||
downloadType String? // Type of download: SINGLE, BULK, or FOLDER
|
||||
downloadMetadata Json? // Metadata about the download (folder name, document list, etc.)
|
||||
reactions Reaction[]
|
||||
viewType ViewType @default(DOCUMENT_VIEW)
|
||||
viewType String @default("DOCUMENT_VIEW")
|
||||
viewerId String? // This is the viewer ID from the dataroom
|
||||
viewer Viewer? @relation(fields: [viewerId], references: [id], onDelete: Cascade)
|
||||
groupId String? // This is the group ID from the dataroom
|
||||
@@ -169,16 +167,7 @@ model View {
|
||||
@@index([documentId, viewedAt(sort: Desc)]) // Performance optimization for latest views queries
|
||||
}
|
||||
|
||||
enum ViewType {
|
||||
DOCUMENT_VIEW
|
||||
DATAROOM_VIEW
|
||||
}
|
||||
|
||||
enum DownloadType {
|
||||
SINGLE // Individual document download
|
||||
BULK // Full dataroom bulk download
|
||||
FOLDER // Folder download
|
||||
}
|
||||
|
||||
model Viewer {
|
||||
id String @id @default(cuid())
|
||||
@@ -234,17 +223,10 @@ model Invitation {
|
||||
@@unique([email, teamId])
|
||||
}
|
||||
|
||||
enum EmailType {
|
||||
FIRST_DAY_DOMAIN_REMINDER_EMAIL
|
||||
FIRST_DOMAIN_INVALID_EMAIL
|
||||
SECOND_DOMAIN_INVALID_EMAIL
|
||||
FIRST_TRIAL_END_REMINDER_EMAIL
|
||||
FINAL_TRIAL_END_REMINDER_EMAIL
|
||||
}
|
||||
|
||||
model SentEmail {
|
||||
id String @id @default(cuid())
|
||||
type EmailType
|
||||
type String
|
||||
recipient String // Email address of the recipient
|
||||
marketing Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
@@ -307,7 +289,7 @@ model ChatMessage {
|
||||
chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
|
||||
|
||||
role String // "user" | "assistant" | "system"
|
||||
content String @db.Text
|
||||
content String
|
||||
|
||||
// Optional structured data
|
||||
metadata Json? // Store sources, page numbers, confidence scores, etc.
|
||||
@@ -460,11 +442,6 @@ model YearInReview {
|
||||
@@index([teamId])
|
||||
}
|
||||
|
||||
enum TagType {
|
||||
LINK_TAG
|
||||
DOCUMENT_TAG
|
||||
DATAROOM_TAG
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @default(cuid())
|
||||
@@ -491,7 +468,7 @@ model TagItem {
|
||||
id String @id @default(cuid())
|
||||
tagId String
|
||||
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||
itemType TagType
|
||||
itemType String
|
||||
|
||||
// tag can be linked to a link, document or dataroom
|
||||
linkId String?
|
||||
@@ -521,7 +498,7 @@ model ViewerInvitation {
|
||||
invitedBy String
|
||||
customMessage String?
|
||||
sentAt DateTime @default(now())
|
||||
status InvitationStatus @default(SENT)
|
||||
status String @default("SENT")
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@ -530,8 +507,3 @@ model ViewerInvitation {
|
||||
@@index([groupId])
|
||||
}
|
||||
|
||||
enum InvitationStatus {
|
||||
SENT
|
||||
FAILED
|
||||
BOUNCED
|
||||
}
|
||||
|
||||
@@ -65,18 +65,13 @@ model Team {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
ignoredDomains String[] // Domains that are ignored for the team
|
||||
globalBlockList String[] // Email and domains that are blocked for the team
|
||||
ignoredDomains Json @default("[]")// Domains that are ignored for the team
|
||||
globalBlockList Json @default("[]")// Email and domains that are blocked for the team
|
||||
}
|
||||
|
||||
enum Role {
|
||||
ADMIN
|
||||
MANAGER
|
||||
MEMBER
|
||||
}
|
||||
|
||||
model UserTeam {
|
||||
role Role @default(MEMBER)
|
||||
role String @default("MEMBER")
|
||||
status String @default("ACTIVE")
|
||||
userId String
|
||||
teamId String
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
// Workflow models for routing visitors based on email/domain rules
|
||||
|
||||
enum WorkflowStepType {
|
||||
ROUTER // Route to different links based on conditions
|
||||
}
|
||||
|
||||
enum ExecutionStatus {
|
||||
PENDING
|
||||
IN_PROGRESS
|
||||
COMPLETED
|
||||
FAILED
|
||||
BLOCKED
|
||||
}
|
||||
|
||||
model Workflow {
|
||||
id String @id @default(cuid())
|
||||
@@ -37,7 +27,7 @@ model WorkflowStep {
|
||||
workflowId String
|
||||
name String
|
||||
stepOrder Int // Execution order (priority-based routing)
|
||||
stepType WorkflowStepType @default(ROUTER)
|
||||
stepType String @default("ROUTER")
|
||||
conditions Json // JSON array of conditions to evaluate
|
||||
actions Json // JSON array of actions to execute
|
||||
createdAt DateTime @default(now())
|
||||
@@ -55,7 +45,7 @@ model WorkflowExecution {
|
||||
workflowId String
|
||||
visitorEmail String?
|
||||
visitorIp String?
|
||||
status ExecutionStatus
|
||||
status String
|
||||
startedAt DateTime @default(now())
|
||||
completedAt DateTime?
|
||||
result Json? // Final result/output (routing target)
|
||||
|
||||
Reference in New Issue
Block a user