mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-18 20:59:55 -05:00
291 lines
11 KiB
Plaintext
291 lines
11 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
enum Role {
|
|
ADMIN
|
|
MEMBER
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
passwordHash String
|
|
displayName String
|
|
role Role @default(MEMBER)
|
|
folders Folder[]
|
|
prints Print[]
|
|
collections Collection[]
|
|
importJobs ImportJob[]
|
|
notifications Notification[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model Folder {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
name String
|
|
tags String[] @default([])
|
|
parentId String?
|
|
parent Folder? @relation("FolderTree", fields: [parentId], references: [id], onDelete: SetNull)
|
|
children Folder[] @relation("FolderTree")
|
|
// Manual sort order within a sibling group (same userId + parentId), set by the "move up/down"
|
|
// controls in the category manager. Not DB-uniqueness-constrained (parentId is nullable, and
|
|
// Postgres treats NULLs as distinct, so a unique index wouldn't actually cover the root-level
|
|
// sibling group) -- ordering is only ever applied via the reorder endpoint, which always
|
|
// rewrites every sibling's position together.
|
|
position Int @default(0)
|
|
metaTitle String?
|
|
metaDescription String?
|
|
// When set, an import from that site whose own category id matches ANY of these auto-lands
|
|
// in this folder (see importService.ts's resolveFolderIdByCategory -- a Prisma `hasSome`
|
|
// overlap check), unless the caller already picked a folder explicitly. Each site's category
|
|
// ids are independent integer namespaces. A folder can list several ids (e.g. a parent
|
|
// category and a couple of its subcategories) since one folder may reasonably want to catch
|
|
// more than one upstream category -- entered/edited as a "800;71;1001"-style semicolon-
|
|
// separated string (see routes/folders.ts's parseCatIdsInput), stored as a real int array.
|
|
makerworldCatIds Int[] @default([])
|
|
thingiverseCatIds Int[] @default([])
|
|
printablesCatIds Int[] @default([])
|
|
prints Print[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([userId, parentId, position])
|
|
@@index([tags], type: Gin)
|
|
@@index([makerworldCatIds], type: Gin)
|
|
@@index([thingiverseCatIds], type: Gin)
|
|
@@index([printablesCatIds], type: Gin)
|
|
}
|
|
|
|
model Print {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
name String
|
|
nameNormalized String
|
|
title String?
|
|
notes String?
|
|
creator String?
|
|
collection String?
|
|
tags String[] @default([])
|
|
viewCount Int @default(0)
|
|
printCount Int @default(0)
|
|
// Backs the two built-in "Favourites"/"Browsing History" pseudo-collections (see
|
|
// routes/collections.ts's SYSTEM_COLLECTIONS) -- not a real Collection/CollectionItem row.
|
|
// lastViewedAt is stamped on every GET /print/:id alongside viewCount, so "Browsing History"
|
|
// falls out of `orderBy: lastViewedAt desc` with no separate event log.
|
|
favoritedAt DateTime?
|
|
lastViewedAt DateTime?
|
|
// Identifies the source design this print was imported from (MakerWorld today), so a
|
|
// re-import of the same design -- whether via a plain link paste or as part of a different
|
|
// collection's batch import -- reuses this row instead of re-downloading a duplicate. Null
|
|
// for uploads, zip/folder-scan imports, and anything not resolvable to a stable provider id.
|
|
sourceProvider String?
|
|
sourceExternalId String?
|
|
folderId String?
|
|
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
|
authorId String?
|
|
author Author? @relation(fields: [authorId], references: [id], onDelete: SetNull)
|
|
sourcePath String?
|
|
plates Plate[]
|
|
previewImages PreviewImage[]
|
|
files PrintFile[] @relation("PrintFiles")
|
|
preparedFileId String? @unique
|
|
preparedFile PrintFile? @relation("PreparedFile", fields: [preparedFileId], references: [id], onDelete: SetNull)
|
|
preparedMetadata Json?
|
|
collectionItems CollectionItem[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([userId, folderId, nameNormalized])
|
|
@@unique([userId, sourceProvider, sourceExternalId])
|
|
@@index([userId, folderId, name])
|
|
@@index([tags], type: Gin)
|
|
@@index([authorId])
|
|
@@index([userId, favoritedAt])
|
|
@@index([userId, lastViewedAt])
|
|
}
|
|
|
|
/// A user-curated, named group of models -- acts like a "playlist" for prints. Populated
|
|
/// manually (via the create/edit modal) or automatically when importing a MakerWorld
|
|
/// collection by URL (see collectionService.findOrCreateCollectionByName), which matches
|
|
/// on nameNormalized so re-importing the same MakerWorld collection reuses the same row
|
|
/// instead of creating a duplicate.
|
|
model Collection {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
name String
|
|
nameNormalized String
|
|
description String?
|
|
tags String[] @default([])
|
|
items CollectionItem[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([userId, nameNormalized])
|
|
@@index([tags], type: Gin)
|
|
}
|
|
|
|
/// Join row between Collection and Print (many-to-many), with a position for cover/display
|
|
/// ordering.
|
|
model CollectionItem {
|
|
id String @id @default(cuid())
|
|
collectionId String
|
|
collection Collection @relation(fields: [collectionId], references: [id], onDelete: Cascade)
|
|
printId String
|
|
print Print @relation(fields: [printId], references: [id], onDelete: Cascade)
|
|
position Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([collectionId, printId])
|
|
@@index([collectionId, position])
|
|
@@index([printId])
|
|
}
|
|
|
|
enum ImportJobType {
|
|
COLLECTION
|
|
ZIP
|
|
}
|
|
|
|
enum ImportJobStatus {
|
|
RUNNING
|
|
DONE
|
|
ERROR
|
|
}
|
|
|
|
/// Tracks one background batch import (a MakerWorld collection, or a remote zip's selected
|
|
/// entries) so the triggering request can return immediately instead of blocking on however
|
|
/// long the whole batch takes -- see services/importJobRunner.ts. At most one RUNNING job per
|
|
/// user is ever allowed (enforced in routes/imports.ts); that single row doubles as the "an
|
|
/// import is already in progress" lock, and survives a page refresh since the frontend can just
|
|
/// ask for the user's active job again.
|
|
model ImportJob {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
type ImportJobType
|
|
status ImportJobStatus @default(RUNNING)
|
|
sourceUrl String
|
|
sourceLabel String?
|
|
provider String?
|
|
total Int @default(0)
|
|
processed Int @default(0)
|
|
imported Int @default(0)
|
|
alreadyInLibrary Int @default(0)
|
|
failedCount Int @default(0)
|
|
errorMessage String?
|
|
resultCollectionId String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([userId, status])
|
|
}
|
|
|
|
/// A generic per-user notification -- today only ever created by importJobRunner.ts when a
|
|
/// batch import finishes, but the shape (title/body + an optional in-app and/or external link)
|
|
/// is deliberately not import-specific so a future notification source can reuse it.
|
|
model Notification {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
title String
|
|
body String?
|
|
externalUrl String?
|
|
internalPath String?
|
|
readAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId, readAt])
|
|
@@index([userId, createdAt])
|
|
}
|
|
|
|
/// A creator/designer on some source platform (MakerWorld today; Printables/Thingiverse etc.
|
|
/// later). `id` is provider-prefixed ("makerworld:3582000380") rather than a generated cuid so
|
|
/// re-importing a model from the same author upserts the same row instead of creating a
|
|
/// duplicate. Print.creator stays as the plain display string for backward compatibility and
|
|
/// non-provider sources; this table is the richer record clickable author pages will use.
|
|
model Author {
|
|
id String @id
|
|
provider String
|
|
externalId String
|
|
name String?
|
|
handle String?
|
|
bio String?
|
|
bioTranslated String?
|
|
links String[] @default([])
|
|
avatarUrl String?
|
|
backgroundUrl String?
|
|
prints Print[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([provider, externalId])
|
|
}
|
|
|
|
model Plate {
|
|
id String @id @default(cuid())
|
|
printId String
|
|
print Print @relation(fields: [printId], references: [id], onDelete: Cascade)
|
|
position Int
|
|
filename String
|
|
mime String
|
|
size Int @default(0)
|
|
storagePath String @unique
|
|
sourcePath String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([printId, position])
|
|
@@index([printId])
|
|
}
|
|
|
|
/// A photo of the finished model, shown as the model detail page's main image gallery -- distinct
|
|
/// from Plate (a printable 3D file) and PrintFile (a supporting document/prepared print). Always
|
|
/// stored as a normalized JPEG under config.PREVIEWS, keyed by id, so there's no filename/mime/
|
|
/// size to track the way Plate and PrintFile need. Position 0 is the default/main image.
|
|
model PreviewImage {
|
|
id String @id @default(cuid())
|
|
printId String
|
|
print Print @relation(fields: [printId], references: [id], onDelete: Cascade)
|
|
position Int
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([printId, position])
|
|
@@index([printId])
|
|
}
|
|
|
|
enum PrintFileRole {
|
|
SUPPORTING
|
|
PREPARED
|
|
}
|
|
|
|
model PrintFile {
|
|
id String @id @default(cuid())
|
|
printId String
|
|
print Print @relation("PrintFiles", fields: [printId], references: [id], onDelete: Cascade)
|
|
role PrintFileRole
|
|
filename String
|
|
mime String
|
|
size Int @default(0)
|
|
storagePath String @unique
|
|
metadata Json?
|
|
createdAt DateTime @default(now())
|
|
preparedFor Print? @relation("PreparedFile")
|
|
|
|
@@index([printId, role])
|
|
}
|
|
|
|
model Setting {
|
|
key String @id
|
|
value Json
|
|
}
|