diff --git a/.idea/cvsa.iml b/.idea/cvsa.iml
index f6f712f..0b90050 100644
--- a/.idea/cvsa.iml
+++ b/.idea/cvsa.iml
@@ -40,6 +40,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml
index 8d18a04..a230540 100644
--- a/.idea/data_source_mapping.xml
+++ b/.idea/data_source_mapping.xml
@@ -1,17 +1,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/.idea/db-forest-config.xml b/.idea/db-forest-config.xml
index 53aee3b..d092723 100644
--- a/.idea/db-forest-config.xml
+++ b/.idea/db-forest-config.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/packages/core/.gitignore b/packages/core/.gitignore
index 014185e..5360788 100644
--- a/packages/core/.gitignore
+++ b/packages/core/.gitignore
@@ -2,4 +2,4 @@ node_modules
# Keep environment variables out of version control
.env
-/prisma/prisma
+/prisma/generated
diff --git a/packages/core/prisma.config.ts b/packages/core/prisma.config.ts
index d65471d..2cf485b 100644
--- a/packages/core/prisma.config.ts
+++ b/packages/core/prisma.config.ts
@@ -2,7 +2,7 @@
import { defineConfig, env } from "prisma/config";
export default defineConfig({
- schema: "prisma/schema.prisma",
+ schema: "prisma/",
migrations: {
path: "prisma/migrations",
},
diff --git a/packages/core/prisma/models/auth/README.md b/packages/core/prisma/models/auth/README.md
new file mode 100644
index 0000000..624b014
--- /dev/null
+++ b/packages/core/prisma/models/auth/README.md
@@ -0,0 +1,6 @@
+# Auth Schema
+
+This schema handles Identity and Access Management (IAM).
+
+- **User:** The central account entity.
+- **Session:** Tracks active logins, device information. Used in authorization.
diff --git a/packages/core/prisma/models/auth/session.prisma b/packages/core/prisma/models/auth/session.prisma
new file mode 100644
index 0000000..f999826
--- /dev/null
+++ b/packages/core/prisma/models/auth/session.prisma
@@ -0,0 +1,13 @@
+model Session {
+ id String @id
+ user User @relation(fields: [userId], references: [id])
+ userId Int @map("user_id")
+ ipAddress String? @map("ip_address")
+ userAgent String? @map("user_agent")
+ secretHash String @map("secret_hash")
+ createdAt DateTime @default(now()) @map("created_at")
+ lastVerifiedAt DateTime @map("last_verified_at")
+
+ @@map("session")
+ @@schema("auth")
+}
diff --git a/packages/core/prisma/models/auth/user.prisma b/packages/core/prisma/models/auth/user.prisma
new file mode 100644
index 0000000..e316ebf
--- /dev/null
+++ b/packages/core/prisma/models/auth/user.prisma
@@ -0,0 +1,22 @@
+model User {
+ id Int @id @default(autoincrement())
+ username String @unique
+ displayName String @map("display_name")
+ email String @unique
+ password String
+ changesMade History[]
+ role Role? @relation(fields: [roleId], references: [id])
+ roleId Int? @map("role_id")
+ sessions Session[]
+ artist Artist?
+ reputation Int @default(0)
+ reputationHistories ReputationHistory[]
+ posts Post[]
+ files File[]
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("user")
+ @@schema("auth")
+}
diff --git a/packages/core/prisma/models/core/README.md b/packages/core/prisma/models/core/README.md
new file mode 100644
index 0000000..feb25a4
--- /dev/null
+++ b/packages/core/prisma/models/core/README.md
@@ -0,0 +1,17 @@
+# Core Schema
+
+The `core` schema is the heart of the CVSA database. It functions as a structured encyclopedia for the Chinese Singing Voice Synthesis community.
+
+## Design Philosophy
+
+The data here is designed to be **objective and universal**. It models the relationships between music, creators, etc.
+
+## Sub-domains
+
+1. **Music Entities:** Songs, Albums, Lyrics, and Song Series.
+2. **Talent:** Artists (producers/illustrators) and Singers (virtual characters).
+3. **Classification:** A hierarchical Tag system for genre and thematic categorization.
+
+## Localization
+
+Most entities use `LocalizedField` (JSON) to support multi-language names and descriptions (e.g., Chinese, Japanese, and English).
diff --git a/packages/core/prisma/models/core/album.prisma b/packages/core/prisma/models/core/album.prisma
new file mode 100644
index 0000000..2c22677
--- /dev/null
+++ b/packages/core/prisma/models/core/album.prisma
@@ -0,0 +1,17 @@
+model Album {
+ id Int @id @default(autoincrement())
+ name String?
+ /// [LocalizedField]
+ localizedNames Json? @map("localized_names")
+ description String?
+ /// [LocalizedField]
+ localizedDescriptions Json? @map("localized_descriptions")
+ songInAlbums SongInAlbum[]
+ coverUrl String? @map("cover_url")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("album")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/models/core/artist.prisma b/packages/core/prisma/models/core/artist.prisma
new file mode 100644
index 0000000..cfbe2d2
--- /dev/null
+++ b/packages/core/prisma/models/core/artist.prisma
@@ -0,0 +1,19 @@
+model Artist {
+ id Int @id @default(autoincrement())
+ name String?
+ /// [LocalizedField]
+ localizedNames Json? @map("localized_names")
+ aliases String[]
+ description String?
+ /// [LocalizedField]
+ localizedDescriptions Json? @map("localized_descriptions")
+ artistSongRoles ArtistRole[]
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ user User? @relation(fields: [userId], references: [id])
+ userId Int? @unique @map("user_id")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("artist")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/models/core/artistRole.prisma b/packages/core/prisma/models/core/artistRole.prisma
new file mode 100644
index 0000000..40dc72b
--- /dev/null
+++ b/packages/core/prisma/models/core/artistRole.prisma
@@ -0,0 +1,16 @@
+model ArtistRole {
+ id Int @id @default(autoincrement())
+ role String
+ /// [LocalizedField]
+ localizedRoles Json? @map("localized_roles")
+ song Song @relation(fields: [songId], references: [id])
+ songId Int @map("song_id")
+ artist Artist @relation(fields: [artistId], references: [id])
+ artistId Int @map("artist_id")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("artist_role")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/models/core/externalLink.prisma b/packages/core/prisma/models/core/externalLink.prisma
new file mode 100644
index 0000000..4f97458
--- /dev/null
+++ b/packages/core/prisma/models/core/externalLink.prisma
@@ -0,0 +1,44 @@
+model ExternalLink {
+ id Int @id @default(autoincrement())
+ label String?
+ url String
+ platform Platform?
+ platformId String? @map("platform_id")
+ song Song @relation(fields: [songId], references: [id])
+ songId Int @map("song_id")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("external_link")
+ @@schema("core")
+}
+
+enum Platform {
+ YOUTUBE
+ NICONICO
+ BILIBILI
+ VOCALOID_WIKI
+ VOCALOID_LYRICS_WIKI
+ SOUNDCLOUD
+ NETEASE_MUSIC
+ QQ_MUSIC
+ FIVE_SING // 5sing.kugou.com
+ KUGOU
+ SPOTIFY
+ APPLE_MUSIC
+ YOUTUBE_MUSIC
+ WIKIPEDIA
+ BAIDU_BAIKE
+ PIXIV
+ WEIBO
+ TWITTER
+ VOCADB
+ MOEGIRLPEDIA
+ MUSICBRAINZ
+ VCPEDIA
+ XIAOHONGSHU // aka RedNote
+
+ @@map("third_party_platform")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/models/core/group.prisma b/packages/core/prisma/models/core/group.prisma
new file mode 100644
index 0000000..c8ac718
--- /dev/null
+++ b/packages/core/prisma/models/core/group.prisma
@@ -0,0 +1,11 @@
+model Group {
+ id Int @id @default(autoincrement())
+ name String
+ description String?
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("group")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/models/core/lyrics.prisma b/packages/core/prisma/models/core/lyrics.prisma
new file mode 100644
index 0000000..7ee4257
--- /dev/null
+++ b/packages/core/prisma/models/core/lyrics.prisma
@@ -0,0 +1,15 @@
+model Lyrics {
+ id Int @id @default(autoincrement())
+ song Song @relation(fields: [songId], references: [id])
+ songId Int @map("song_id")
+ language String?
+ plainText String? @map("plain_text")
+ ttml String?
+ lrc String?
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("lyrics")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/models/core/singer.prisma b/packages/core/prisma/models/core/singer.prisma
new file mode 100644
index 0000000..7cd3152
--- /dev/null
+++ b/packages/core/prisma/models/core/singer.prisma
@@ -0,0 +1,20 @@
+model Singer {
+ id Int @id @default(autoincrement())
+ name String?
+ avatarUrl String? @map("avatar_url")
+ /// [LocalizedField]
+ localizedNames Json? @map("localized_names")
+ description String?
+ /// [LocalizedField]
+ localizedDescriptions Json? @map("localized_descriptions")
+ singerSvsEngineVersions SingerSvsEngineVersion[]
+ singerSvsEngines SingerSvsEngine[]
+ voicebanks Voicebank[]
+ singerOfSongs SingerOfSong[]
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("singer")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/models/core/song.prisma b/packages/core/prisma/models/core/song.prisma
new file mode 100644
index 0000000..48e6e62
--- /dev/null
+++ b/packages/core/prisma/models/core/song.prisma
@@ -0,0 +1,47 @@
+model Song {
+ id Int @id @default(autoincrement())
+ type SongType?
+ originalVersion Song? @relation(name: "originalSong", fields: [originalSongId], references: [id])
+ originalSongId Int? @map("original_song_id")
+ derivations Song[] @relation(name: "originalSong")
+ name String?
+ duration Int?
+ lyrics Lyrics[]
+ songTags SongTag[]
+ /// [LocalizedField]
+ localizedNames Json? @map("localized_names")
+ description String?
+ /// [LocalizedField]
+ localizedDescriptions Json? @map("localized_descriptions")
+ songInAlbums SongInAlbum[]
+ bilibiliAid BigInt? @map("bilibili_aid")
+ bilibiliBvid String? @map("bilibili_bvid")
+ vocadbId Int? @map("vocadb_id")
+ vcpediaId Int? @map("vcpedia_id")
+ moegirlId Int? @map("moegirl_id")
+ externalLinks ExternalLink[]
+ coverUrl String? @map("cover_url")
+ artists ArtistRole[]
+ singerOfSongs SingerOfSong[]
+ publishedAt DateTime? @map("published_at")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+ songInSeries SongInSeries[]
+
+ @@map("song")
+ @@schema("core")
+}
+
+enum SongType {
+ ORIGINAL
+ COVER
+ REMIX
+ REMASTER
+ MASHUP
+ INSTRUMENTAL
+ OTHERS
+
+ @@map("song_type")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/models/core/songSeries.prisma b/packages/core/prisma/models/core/songSeries.prisma
new file mode 100644
index 0000000..414cadb
--- /dev/null
+++ b/packages/core/prisma/models/core/songSeries.prisma
@@ -0,0 +1,16 @@
+model SongSeries {
+ id Int @id @default(autoincrement())
+ name String?
+ /// [LocalizedField]
+ localizedNames Json? @map("localized_names")
+ description String?
+ /// [LocalizedField]
+ localizedDescriptions Json? @map("localized_descriptions")
+ songInSeries SongInSeries[]
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("song_series")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/models/core/svsEngine.prisma b/packages/core/prisma/models/core/svsEngine.prisma
new file mode 100644
index 0000000..ac999f5
--- /dev/null
+++ b/packages/core/prisma/models/core/svsEngine.prisma
@@ -0,0 +1,16 @@
+model SvsEngine {
+ id Int @id @default(autoincrement())
+ name String
+ description String?
+ /// [LocalizedField]
+ localizedDescriptions Json? @map("localized_descriptions")
+ svsEngineVersions SvsEngineVersion[]
+ singerSvsEngines SingerSvsEngine[]
+ performances SingerOfSong[]
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("svs_engine")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/models/core/svsEngineVersion.prisma b/packages/core/prisma/models/core/svsEngineVersion.prisma
new file mode 100644
index 0000000..154acd7
--- /dev/null
+++ b/packages/core/prisma/models/core/svsEngineVersion.prisma
@@ -0,0 +1,15 @@
+model SvsEngineVersion {
+ id Int @id @default(autoincrement())
+ versionString String @map("version_string")
+ singerSvsEngineVersions SingerSvsEngineVersion[]
+ svsEngine SvsEngine @relation(fields: [svsEngineId], references: [id])
+ svsEngineId Int @map("svs_engine_id")
+ singerOfSongs SingerOfSong[]
+ voicebanks Voicebank[]
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("svs_engine_version")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/models/core/tag.prisma b/packages/core/prisma/models/core/tag.prisma
new file mode 100644
index 0000000..bcd0f28
--- /dev/null
+++ b/packages/core/prisma/models/core/tag.prisma
@@ -0,0 +1,16 @@
+model Tag {
+ id Int @id @default(autoincrement())
+ name String
+ /// [LocalizedField]
+ localizedNames Json? @map("localized_names")
+ songTags SongTag[]
+ parent Tag? @relation(name: "tagTree", fields: [parentId], references: [id])
+ parentId Int? @map("parent_id")
+ children Tag[] @relation(name: "tagTree")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("tag")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/models/core/voicebank.prisma b/packages/core/prisma/models/core/voicebank.prisma
new file mode 100644
index 0000000..a7adae2
--- /dev/null
+++ b/packages/core/prisma/models/core/voicebank.prisma
@@ -0,0 +1,18 @@
+model Voicebank {
+ id Int @id @default(autoincrement())
+ singer Singer @relation(fields: [singerId], references: [id])
+ description String?
+ /// [LocalizedField]
+ localizedDescriptions Json? @map("localized_descriptions")
+ singerId Int @map("singer_id")
+ language String
+ svsEngineVersion SvsEngineVersion @relation(fields: [svsEngineVersionId], references: [id])
+ svsEngineVersionId Int @map("svs_engine_version_id")
+ singerOfSongs SingerOfSong[]
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("voicebank")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/models/meta/README.md b/packages/core/prisma/models/meta/README.md
new file mode 100644
index 0000000..73b0609
--- /dev/null
+++ b/packages/core/prisma/models/meta/README.md
@@ -0,0 +1,6 @@
+# Meta Schema
+
+The `meta` schema manages system governance, auditing, and configuration.
+
+- **Auditing:** The `History` table tracks every change made to the `core` entities (Create/Update/Delete) for rollback and accountability.
+- **RBAC:** Implements Role-Based Access Control via `Role` and `Permission` entities.
diff --git a/packages/core/prisma/models/meta/history.prisma b/packages/core/prisma/models/meta/history.prisma
new file mode 100644
index 0000000..4c6839c
--- /dev/null
+++ b/packages/core/prisma/models/meta/history.prisma
@@ -0,0 +1,25 @@
+model History {
+ id Int @id @default(autoincrement())
+ user User @relation(fields: [userId], references: [id])
+ userId Int @map("user_id")
+ table String
+ objectId Int @map("object_id")
+ type ChangeType
+ comment String?
+ old Json?
+ new Json?
+ createdAt DateTime @default(now()) @map("created_at")
+
+ @@map("history")
+ @@schema("meta")
+}
+
+enum ChangeType {
+ CREATE
+ UPDATE
+ DELETE
+ RESTORE
+
+ @@map("change_type")
+ @@schema("meta")
+}
diff --git a/packages/core/prisma/models/meta/permission.prisma b/packages/core/prisma/models/meta/permission.prisma
new file mode 100644
index 0000000..4a7be38
--- /dev/null
+++ b/packages/core/prisma/models/meta/permission.prisma
@@ -0,0 +1,11 @@
+model Permission {
+ id Int @id @default(autoincrement())
+ action String
+ roles RolePermission[]
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("permission")
+ @@schema("meta")
+}
diff --git a/packages/core/prisma/models/meta/role.prisma b/packages/core/prisma/models/meta/role.prisma
new file mode 100644
index 0000000..e4d04c2
--- /dev/null
+++ b/packages/core/prisma/models/meta/role.prisma
@@ -0,0 +1,14 @@
+model Role {
+ id Int @id @default(autoincrement())
+ name String @unique
+ /// [LocalizedField]
+ localizedNames Json? @map("localized_names")
+ permissions RolePermission[]
+ users User[]
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("role")
+ @@schema("meta")
+}
diff --git a/packages/core/prisma/models/models.prisma b/packages/core/prisma/models/models.prisma
new file mode 100644
index 0000000..d1a3861
--- /dev/null
+++ b/packages/core/prisma/models/models.prisma
@@ -0,0 +1,11 @@
+/*
+NOTE:
+This is an empty placeholder file to ensure the Prisma Language Server functions
+correctly, serving as a temporary workaround.
+
+See the link below for details:
+https://github.com/prisma/prisma/discussions/24413#discussioncomment-9670600
+
+This issue persists as of Prisma v7.3.0, which was the latest
+version at the time of writing.
+*/
\ No newline at end of file
diff --git a/packages/core/prisma/models/platform/README.md b/packages/core/prisma/models/platform/README.md
new file mode 100644
index 0000000..aa40682
--- /dev/null
+++ b/packages/core/prisma/models/platform/README.md
@@ -0,0 +1,15 @@
+# Platform Schema
+
+This schema contains tables and logic specific to the **CVSA as a platform**.
+
+## Purpose
+
+While the `core` schema focuses on universal SVS (Singing Voice Synthesis) data that could theoretically be shared across different encyclopedia projects, the `platform` schema manages data unique to this specific implementation.
+
+It includes:
+
+- **User Activity:** Personal notes, bookmarks, and interaction history.
+- **System State:** File storage metadata and upload tracking.
+
+and so on.
+
diff --git a/packages/core/prisma/models/platform/file.prisma b/packages/core/prisma/models/platform/file.prisma
new file mode 100644
index 0000000..0fcea7e
--- /dev/null
+++ b/packages/core/prisma/models/platform/file.prisma
@@ -0,0 +1,11 @@
+model File {
+ id Int @id @default(autoincrement())
+ originalName String? @map("original_name")
+ path String?
+ user User @relation(fields: [userId], references: [id])
+ userId Int @map("user_id")
+ createdAt DateTime @default(now()) @map("created_at")
+
+ @@map("file")
+ @@schema("platform")
+}
diff --git a/packages/core/prisma/models/platform/post.prisma b/packages/core/prisma/models/platform/post.prisma
new file mode 100644
index 0000000..3ee4c3c
--- /dev/null
+++ b/packages/core/prisma/models/platform/post.prisma
@@ -0,0 +1,12 @@
+model Post {
+ id Int @id @default(autoincrement())
+ text String?
+ user User @relation(fields: [userId], references: [id])
+ userId Int @map("user_id")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("post")
+ @@schema("platform")
+}
diff --git a/packages/core/prisma/models/platform/reputationHistory.prisma b/packages/core/prisma/models/platform/reputationHistory.prisma
new file mode 100644
index 0000000..4b635bc
--- /dev/null
+++ b/packages/core/prisma/models/platform/reputationHistory.prisma
@@ -0,0 +1,13 @@
+model ReputationHistory {
+ id Int @id @default(autoincrement())
+ user User @relation(fields: [userId], references: [id])
+ userId Int @map("user_id")
+ actionType String @map("action_type")
+ points Int
+ sourceType Int @map("source_type")
+ sourceId Int @map("source_id")
+ createdAt DateTime @default(now()) @map("created_at")
+
+ @@map("reputation_history")
+ @@schema("platform")
+}
diff --git a/packages/core/prisma/relations/README.md b/packages/core/prisma/relations/README.md
new file mode 100644
index 0000000..9c6d866
--- /dev/null
+++ b/packages/core/prisma/relations/README.md
@@ -0,0 +1,6 @@
+# Relations
+
+This directory contains join tables for Many-to-Many relationships.
+
+To maintain a clean and modular Prisma schema, we separate core data models (in `/models`) from their association entities.
+This structure helps prevent individual `.prisma` files from becoming too large and complex.
diff --git a/packages/core/prisma/relations/core/singerOfSong.prisma b/packages/core/prisma/relations/core/singerOfSong.prisma
new file mode 100644
index 0000000..bd42ca5
--- /dev/null
+++ b/packages/core/prisma/relations/core/singerOfSong.prisma
@@ -0,0 +1,22 @@
+// Sometimes, we only know the SVS engine used in the song (e.g. VOCALOID, Synthesizer V, UTAU, etc.).
+// In other cases, we know exactly which voicebank was used in the song. (e.g. Luo Tianyi VOCALOID 5 Chinese).
+// That's why we need to have both SvsEngine and SvsEngineVersion.
+model SingerOfSong {
+ id Int @id @default(autoincrement())
+ song Song @relation(fields: [songId], references: [id])
+ songId Int @map("song_id")
+ singer Singer @relation(fields: [singerId], references: [id])
+ singerId Int @map("singer_id")
+ voicebank Voicebank? @relation(fields: [voicebankId], references: [id])
+ voicebankId Int? @map("voicebank_id")
+ svsEngine SvsEngine? @relation(fields: [svsEngineId], references: [id])
+ svsEngineId Int? @map("svs_engine_id")
+ svsEngineVersion SvsEngineVersion? @relation(fields: [svsEngineVersionId], references: [id])
+ svsEngineVersionId Int? @map("svs_engine_version_id")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("singer_of_song")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/relations/core/singerSvsEngine.prisma b/packages/core/prisma/relations/core/singerSvsEngine.prisma
new file mode 100644
index 0000000..f4440b0
--- /dev/null
+++ b/packages/core/prisma/relations/core/singerSvsEngine.prisma
@@ -0,0 +1,13 @@
+model SingerSvsEngine {
+ id Int @id @default(autoincrement())
+ singer Singer @relation(fields: [singerId], references: [id])
+ singerId Int @map("singer_id")
+ svsEngine SvsEngine @relation(fields: [svsEngineId], references: [id])
+ svsEngineId Int @map("svs_engine_id")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("singer_svs_engine")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/relations/core/singerSvsEngineVersion.prisma b/packages/core/prisma/relations/core/singerSvsEngineVersion.prisma
new file mode 100644
index 0000000..bc21e73
--- /dev/null
+++ b/packages/core/prisma/relations/core/singerSvsEngineVersion.prisma
@@ -0,0 +1,13 @@
+model SingerSvsEngineVersion {
+ id Int @id @default(autoincrement())
+ singer Singer @relation(fields: [singerId], references: [id])
+ singerId Int @map("singer_id")
+ svsEngineVersion SvsEngineVersion @relation(fields: [svsEngineVersionId], references: [id])
+ svsEngineVersionId Int @map("svs_engine_version_id")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("singer_svs_engine_version")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/relations/core/songInAlbum.prisma b/packages/core/prisma/relations/core/songInAlbum.prisma
new file mode 100644
index 0000000..939d0bf
--- /dev/null
+++ b/packages/core/prisma/relations/core/songInAlbum.prisma
@@ -0,0 +1,15 @@
+model SongInAlbum {
+ id Int @id @default(autoincrement())
+ song Song @relation(fields: [songId], references: [id])
+ songId Int @map("song_id")
+ album Album @relation(fields: [albumId], references: [id])
+ albumId Int @map("album_id")
+ trackNumber Int @map("track_number")
+ discNumber Int @default(1) @map("disc_number")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("song_in_album")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/relations/core/songInSeries.prisma b/packages/core/prisma/relations/core/songInSeries.prisma
new file mode 100644
index 0000000..0e35608
--- /dev/null
+++ b/packages/core/prisma/relations/core/songInSeries.prisma
@@ -0,0 +1,14 @@
+model SongInSeries {
+ id Int @id @default(autoincrement())
+ song Song @relation(fields: [songId], references: [id])
+ songId Int @map("song_id")
+ series SongSeries @relation(fields: [seriesId], references: [id])
+ seriesId Int @map("series_id")
+ order Int
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("song_in_series")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/relations/core/songTag.prisma b/packages/core/prisma/relations/core/songTag.prisma
new file mode 100644
index 0000000..1a05034
--- /dev/null
+++ b/packages/core/prisma/relations/core/songTag.prisma
@@ -0,0 +1,13 @@
+model SongTag {
+ id Int @id @default(autoincrement())
+ song Song @relation(fields: [songId], references: [id])
+ songId Int @map("song_id")
+ tag Tag @relation(fields: [tagId], references: [id])
+ tagId Int @map("tag_id")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ deletedAt DateTime? @map("deleted_at")
+
+ @@map("song_tag")
+ @@schema("core")
+}
diff --git a/packages/core/prisma/relations/meta/rolePermission.prisma b/packages/core/prisma/relations/meta/rolePermission.prisma
new file mode 100644
index 0000000..8908d72
--- /dev/null
+++ b/packages/core/prisma/relations/meta/rolePermission.prisma
@@ -0,0 +1,10 @@
+model RolePermission {
+ id Int @id @default(autoincrement())
+ role Role @relation(fields: [roleId], references: [id])
+ roleId Int @map("role_id")
+ permission Permission @relation(fields: [permissionId], references: [id])
+ permissionId Int @map("permission_id")
+
+ @@map("role_permission")
+ @@schema("meta")
+}
diff --git a/packages/core/prisma/relations/relations.prisma b/packages/core/prisma/relations/relations.prisma
new file mode 100644
index 0000000..d1a3861
--- /dev/null
+++ b/packages/core/prisma/relations/relations.prisma
@@ -0,0 +1,11 @@
+/*
+NOTE:
+This is an empty placeholder file to ensure the Prisma Language Server functions
+correctly, serving as a temporary workaround.
+
+See the link below for details:
+https://github.com/prisma/prisma/discussions/24413#discussioncomment-9670600
+
+This issue persists as of Prisma v7.3.0, which was the latest
+version at the time of writing.
+*/
\ No newline at end of file
diff --git a/packages/core/prisma/schema.prisma b/packages/core/prisma/schema.prisma
index aefd8bf..34645f1 100644
--- a/packages/core/prisma/schema.prisma
+++ b/packages/core/prisma/schema.prisma
@@ -1,322 +1,60 @@
-// This is your Prisma schema file,
-// learn more about it in the docs: https://pris.ly/d/prisma-schema
-
-// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
-// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
-
generator client {
provider = "prisma-client"
- output = "./prisma"
+ output = "./generated"
}
generator json {
- provider = "prisma-json-types-generator"
+ provider = "prisma-json-types-generator"
}
datasource db {
provider = "postgresql"
+ schemas = ["core", "meta", "platform", "auth"]
}
-// TODO
-// Note:
-// 1. All the IDs are 12-digit string generated via Nano ID with a custom 32-character alphabet.
-// 2. Auth-related model has not yet been defined.
-// 3. Indexes and unique constraints have not yet been considered in the design.
+/*
+# CVSA (Chinese Vocal Synthesis Archive) Database Schema
-enum SongType {
- ORIGINAL
- COVER
- REMIX
- REMASTER
- MASHUP
- INSTRUMENTAL
- OTHERS
+## Overview
+The CVSA is a structured encyclopedia and data platform dedicated to the Chinese Vocal Synthesis (SVS/VC) community.
+It aims to bridge the gap between traditional wikis (like MediaWiki) and structured databases by providing:
+- Automation: Fully automated song discovery, metadata extraction, and statistical tracking.
+- Structured Data: Moving beyond flat text to rich, relational data for songs, artists, engines, and albums.
+- Collaboration: A platform for contributors to provide descriptive content, translations, and corrections.
- @@map("song_type")
-}
+## Schemas
+The database is partitioned into several PostgreSQL schemas.
-enum Platform {
- YOUTUBE
- NICONICO
- BILIBILI
- VOCALOID_WIKI
- VOCALOID_LYRIC_SWIKI
- SOUNDCLOUD
- NETEASE_MUSIC
- QQ_MUSIC
- FIVE_SING // 5sing.kugou.com
- KUGOU
- SPOTIFY
- APPLE_MUSIC
- YOUTUBE_MUSIC
- WIKIPEDIA
- BAIDU_BAIKE
- PIXIV
- WEIBO
- TWITTER
- VOCADB
- MOEGIRLPEDIA
- MUSICBRAINZ
- VCPEDIA
- XIAOHONGSHU // aka RedNote
+### A. [core] - The Encyclopedia
+This is the heart of the project. It stores universal SVS data that remains objective across platforms.
+- Entities: Songs, Singers (virtual characters), Artists (producers/illustrators),
+SVS Engines (Vocaloid, Synthesizer V), Voicebanks, Albums, and Tags.
+- Localization: Uses `LocalizedField` (JSON) to support multi-language metadata
+(Simplified Chinese, Traditional Chinese, Japanese, English, etc.).
+- Versioning: Supports tracking of specific SVS engine versions and voicebank iterations.
- @@map("platform")
-}
+### B. [platform] - The Service Layer
+Contains data specific to the CVSA website as a functional service.
+- Features: User-uploaded files, personal notes, etc.
+- Scope: Data here is specific to this implementation and not necessarily shared with other archives.
-model Song {
- id String @id
- type SongType?
- originalVersion Song? @relation(name: "originalSong", fields: [originalSongId], references: [id])
- originalSongId String? @map("original_song_id")
- derivations Song[] @relation(name: "originalSong")
- name String?
- duration Int?
- lyrics Lyrics[]
- songTags SongTag[]
- /// [LocalizedField]
- localizedNames Json?
- description String?
- /// [LocalizedField]
- localizedDescriptions Json?
- songInAlbums SongInAlbum[]
- bilibiliAid BigInt? @map("bilibili_aid")
- bilibiliBvid String? @map("bilibili_bvid")
- vocadbId Int? @map("vocadb_id")
- vcpediaId Int? @map("vcpedia_id")
- moegirlId Int? @map("moegirl_id")
- externalLinks ExternalLink[]
- coverUrl String? @map("cover_url")
- artists ArtistRole[]
- singerOfSongs SingerOfSong[]
- publishedAt DateTime? @map("published_at")
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
- deletedAt DateTime? @map("deleted_at")
+### C. [auth] - Identity & Access
+Handles user accounts, session persistence, and secure authentication.
+- RBAC: Connects users to roles defined in the `meta` schema.
- @@map("song")
-}
+### D. [meta] - Governance & Auditing
+Manages the "data about data."
+- Audit Logs: The `History` table tracks every mutation (Create/Update/Delete)
+in the `core` schema for accountability and rollbacks.
+- Permissions: Granular action-based permissions assigned to roles.
-model Tag {
- id String @id
- name String
- /// [LocalizedField]
- localizedNames Json?
- songTags SongTag[]
- parent Tag? @relation(name: "tagTree", fields: [parentId], references: [id])
- parentId String? @map("parent_id")
- children Tag[] @relation(name: "tagTree")
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
+## 3. Technical Constraints & Design Patterns
+- Soft Deletion: Most core entities implement a `deletedAt` timestamp for soft-deletion support.
+- Modular Relations: Many-to-Many relations are extracted into separate files
+within the `/relations` directory to keep entity definitions clean and prevent Prisma file bloat.
- @@map("tag")
-}
+## 4. Current Status & TODO
+- Indexing: Performance indexes and unique constraints beyond primary keys are still under design.
+- Constraints: Complex business logic constraints (e.g., preventing circular tag parents) are handled at the application layer.
-model Lyrics {
- id String @id
- song Song @relation(fields: [songId], references: [id])
- songId String @map("song_id")
- language String?
- plaintext String?
- ttml String?
- lrc String?
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
-
- @@map("lyrics")
-}
-
-model History {
- id String @id
- table String
- objectId String @map("object_id")
- old Json?
- new Json?
- createdAt DateTime @default(now()) @map("created_at")
-
- @@map("history")
-}
-
-model ExternalLink {
- id String @id
- label String?
- url String
- platform Platform?
- platformId String? @map("platform_id")
- song Song @relation(fields: [songId], references: [id])
- songId String @map("song_id")
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
-
- @@map("external_link")
-}
-
-// Sometimes, we only know the SVS engine (e.g. VOCALOID, Synthesizer V) used in the song.
-// And in other cases, we know exactly the voicebank used in the song. (e.g. Luo Tianyi VOCALOID 5 Chinese)
-// That's why we need to have both SvsEngine and SvsEngineVersion.
-model SingerOfSong {
- id String @id
- song Song @relation(fields: [songId], references: [id])
- songId String @map("song_id")
- singer Singer @relation(fields: [singerId], references: [id])
- singerId String @map("singer_id")
- voicebank Voicebank? @relation(fields: [voicebankId], references: [id])
- voicebankId String? @map("voicebank_id")
- svsEngine SvsEngine? @relation(fields: [svsEngineId], references: [id])
- svsEngineId String? @map("svs_engine_id")
- svsEngineVersion SvsEngineVersion? @relation(fields: [svsEngineVersionId], references: [id])
- svsEngineVersionId String? @map("svs_engine_version_id")
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
-
- @@map("singer_of_song")
-}
-
-model Voicebank {
- id String @id
- singer Singer @relation(fields: [singerId], references: [id])
- description String?
- /// [LocalizedField]
- localizedDescriptions Json?
- singerId String @map("singer_id")
- language String
- svsEngineVersion SvsEngineVersion @relation(fields: [svsEngineVersionId], references: [id])
- svsEngineVersionId String @map("svs_engine_version_id")
- singerOfSongs SingerOfSong[]
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
-
- @@map("voicebank")
-}
-
-model Album {
- id String @id
- name String?
- /// [LocalizedField]
- localizedNames Json?
- description String?
- /// [LocalizedField]
- localizedDescriptions Json?
- songInAlbums SongInAlbum[]
- coverUrl String? @map("cover_url")
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
-
- @@map("album")
-}
-
-model SongInAlbum {
- song Song @relation(fields: [songId], references: [id])
- songId String @map("song_id")
- album Album @relation(fields: [albumId], references: [id])
- albumId String @map("album_id")
- trackNumber Int @map("track_number")
- discNumber Int @default(1) @map("disc_number")
-
- @@id([songId, albumId])
- @@map("song_in_album")
-}
-
-model SongTag {
- song Song @relation(fields: [songId], references: [id])
- songId String @map("song_id")
- tag Tag @relation(fields: [tagId], references: [id])
- tagId String @map("tag_id")
-
- @@id([songId, tagId])
- @@map("song_tag")
-}
-
-model SingerSvsEngineVersion {
- singer Singer @relation(fields: [singerId], references: [id])
- singerId String @map("singer_id")
- svsEngineVersion SvsEngineVersion @relation(fields: [svsEngineVersionId], references: [id])
- svsEngineVersionId String @map("svs_engine_version_id")
-
- @@id([singerId, svsEngineVersionId])
- @@map("singer_svs_engine_version")
-}
-
-model SingerSvsEngine {
- singer Singer @relation(fields: [singerId], references: [id])
- singerId String @map("singer_id")
- svsEngine SvsEngine @relation(fields: [svsEngineId], references: [id])
- svsEngineId String @map("svs_engine_id")
-
- @@id([singerId, svsEngineId])
- @@map("singer_svs_engine")
-}
-
-model Singer {
- id String @id
- name String?
- avatarUrl String? @map("avatar_url")
- /// [LocalizedField]
- localizedNames Json?
- singerSvsEngineVersions SingerSvsEngineVersion[]
- singerSvsEngines SingerSvsEngine[]
- voicebanks Voicebank[]
- singerOfSongs SingerOfSong[]
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
-
- @@map("singer")
-}
-
-model SvsEngine {
- id String @id
- name String
- description String?
- /// [LocalizedField]
- localizedDescriptions Json?
- svsEngineVersions SvsEngineVersion[]
- singerSvsEngines SingerSvsEngine[]
- performances SingerOfSong[]
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
-
- @@map("svs_engine")
-}
-
-model SvsEngineVersion {
- id String @id
- versionString String @map("version_string")
- singerSvsEngineVersions SingerSvsEngineVersion[]
- svsEngine SvsEngine @relation(fields: [svsEngineId], references: [id])
- svsEngineId String @map("svs_engine_id")
- singerOfSongs SingerOfSong[]
- voicebanks Voicebank[]
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
-
- @@map("svs_engine_version")
-}
-
-model Artist {
- id String @id
- name String?
- /// [LocalizedField]
- localizedNames Json?
- description String?
- /// [LocalizedField]
- localizedDescriptions Json?
- artistSongRoles ArtistRole[]
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
-
- @@map("artist")
-}
-
-model ArtistRole {
- id String @id
- role String
- /// [LocalizedField]
- localizedRoles Json?
- song Song @relation(fields: [songId], references: [id])
- songId String @map("song_id")
- artist Artist @relation(fields: [artistId], references: [id])
- artistId String @map("artist_id")
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
-
- @@map("artist_role")
-}
+*/
\ No newline at end of file