FRE-608: Add Turso database setup with Drizzle ORM

- Create schema for users, projects, scripts, characters, scenes, revisions
- Implement DatabaseManager with connection pooling
- Implement EdgeDatabaseManager for multi-region replicas
- Implement DatabaseBackupManager with automated scheduling
- Generate initial migration with 9 tables
- Add seed script and documentation
- Configure Drizzle Kit for migration management
- Add NPM scripts for database operations

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-24 15:41:03 -04:00
parent 0ba20e5b31
commit 36f9b420f5
20 changed files with 2783 additions and 1 deletions

13
.env.example Normal file
View File

@@ -0,0 +1,13 @@
# Turso Database Configuration
TURSO_DATABASE_URL=libsql://<region>-<project>.turso.io
TURSO_AUTH_TOKEN=<auth-token>
# Backup Configuration (optional)
BACKUP_INTERVAL_MS=86400000
BACKUP_RETENTION_DAYS=30
BACKUP_REGION=us-east
# Clerk Authentication
VITE_CLERK_PUBLISHABLE_KEY=pk_<your-publishable-key>
VITE_CLERK_SIGN_IN_URL=/sign-in
VITE_CLERK_SIGN_UP_URL=/sign-up

11
drizzle.config.ts Normal file
View File

@@ -0,0 +1,11 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema/index.ts",
out: "./src/db/migrations",
dialect: "turso",
dbCredentials: {
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
},
});

View File

@@ -21,10 +21,14 @@
"tauri:build": "tauri build",
"tauri:build:macos": "TAURI_TARGET=x86_64-apple-darwin tauri build",
"tauri:build:windows": "TAURI_TARGET=x86_64-pc-windows-msvc tauri build",
"tauri:build:linux": "TAURI_TARGET=x86_64-unknown-linux-gnu tauri build"
"tauri:build:linux": "TAURI_TARGET=x86_64-unknown-linux-gnu tauri build",
"tauri:test": "cargo test --manifest-path src-tauri/Cargo.toml",
"tauri:icons": "bash src-tauri/generate-icons.sh"
},
"dependencies": {
"@clerk/clerk-js": "^6.7.5",
"@libsql/client": "^0.17.3",
"@solidjs/router": "^0.16.1",
"@tanstack/react-query": "^5.100.1",
"@tanstack/solid-query": "^5.100.1",
"@trpc/client": "^11.16.0",

118
src/db/README.md Normal file
View File

@@ -0,0 +1,118 @@
# Database Setup
Turso (SQLite at edge) with Drizzle ORM for type-safe database access.
## Structure
```
src/db/
├── schema/ # Database schema definitions
│ ├── index.ts # Schema exports
│ ├── users.ts # User accounts
│ ├── projects.ts # Projects
│ ├── scripts.ts # Scripts
│ ├── characters.ts # Characters
│ └── scenes.ts # Scenes with character relationships
├── config/ # Database configuration
│ ├── database.ts # Primary database manager
│ ├── edge-database.ts # Edge replica manager
│ └── migrations.ts # Drizzle ORM setup
└── migrations/ # Migration files (generated)
```
## Environment Variables
```bash
TURSO_DATABASE_URL="libsql://<region>-<project>.turso.io"
TURSO_AUTH_TOKEN="<auth-token>"
```
## Installation
```bash
npm install @libsql/client drizzle-orm drizzle-kit
```
## Usage
### Primary Database
```typescript
import { db } from "./config/migrations";
import { users } from "../schema";
// Query
const allUsers = await db.select().from(users);
// Insert
const newUser = await db.insert(users).values({
email: "user@example.com",
username: "johndoe",
role: "editor",
}).returning();
```
### Edge Database
```typescript
import { createEdgeDatabaseManager } from "./config/edge-database";
const edgeDb = createEdgeDatabaseManager({
primaryRegion: {
region: "primary",
url: "libsql://primary.turso.io",
authToken: process.env.TURSO_AUTH_TOKEN,
isPrimary: true,
},
edgeReplicas: [
{
region: "us-east",
url: "libsql://us-east.turso.io",
authToken: process.env.TURSO_AUTH_TOKEN,
},
{
region: "eu-west",
url: "libsql://eu-west.turso.io",
authToken: process.env.TURSO_AUTH_TOKEN,
},
],
});
// Query on edge
const users = await edgeDb.queryOnDefaultEdge<User>("SELECT * FROM users");
```
## Migrations
```bash
# Generate migrations
npx drizzle-kit generate
# Push schema changes
npx drizzle-kit push
# Run migrations
npx drizzle-kit migrate
```
## Schema Overview
### users
- User accounts with roles (admin, editor, viewer)
- Authentication and authorization
### projects
- Project containers owned by users
- Public/private visibility
### scripts
- Screenplay documents within projects
- Version tracking and status management
### characters
- Character definitions for scripts
- Role-based categorization
### scenes
- Scene content with character relationships
- Many-to-many relationship between scenes and characters

2
src/db/backup/index.ts Normal file
View File

@@ -0,0 +1,2 @@
// Backup exports
export { DatabaseBackupManager, createDatabaseBackupManager } from "../config/backup";

109
src/db/config/backup.ts Normal file
View File

@@ -0,0 +1,109 @@
import { DatabaseManager } from "./database";
interface BackupConfig {
backupIntervalMs: number;
retentionDays: number;
backupRegion: string;
}
export class DatabaseBackupManager {
private dbManager: DatabaseManager;
private config: BackupConfig;
private backupTimer: NodeJS.Timeout | null = null;
constructor(dbManager: DatabaseManager, config: BackupConfig) {
this.dbManager = dbManager;
this.config = config;
}
start(): void {
console.log(`Starting database backup every ${this.config.backupIntervalMs / (1000 * 60)} minutes`);
this.backupTimer = setInterval(async () => {
await this.performBackup();
}, this.config.backupIntervalMs);
}
stop(): void {
if (this.backupTimer) {
clearInterval(this.backupTimer);
this.backupTimer = null;
console.log("Database backup stopped");
}
}
async performBackup(): Promise<void> {
try {
console.log(`Performing database backup to ${this.config.backupRegion}...`);
// Get all tables
const tables = await this.dbManager.query<string>(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
);
for (const table of tables) {
const data = await this.dbManager.query<Record<string, unknown>>(
`SELECT * FROM ${table}`
);
console.log(`Backed up ${table}: ${data.length} rows`);
// Store backup with timestamp
const timestamp = new Date().toISOString();
await this.dbManager.execute(
`INSERT INTO backups (table_name, data, backup_time, region) VALUES (?, ?, ?, ?)`,
[table, JSON.stringify(data), timestamp, this.config.backupRegion]
);
}
await this.cleanupOldBackups();
console.log("Database backup completed successfully");
} catch (error) {
console.error("Database backup failed:", error);
throw error;
}
}
private async cleanupOldBackups(): Promise<void> {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays);
await this.dbManager.execute(
"DELETE FROM backups WHERE backup_time < ?",
[cutoffDate.toISOString()]
);
console.log(`Cleaned up backups older than ${this.config.retentionDays} days`);
}
async restoreFromBackup(backupTime: string): Promise<void> {
console.log(`Restoring database from backup at ${backupTime}...`);
const backups = await this.dbManager.query<{
table_name: string;
data: string;
}>(
"SELECT table_name, data FROM backups WHERE backup_time = ? ORDER BY table_name",
[backupTime]
);
for (const backup of backups) {
const data = JSON.parse(backup.data) as Record<string, unknown>[];
console.log(`Restoring ${backup.table_name}: ${data.length} rows`);
// Note: This is a simplified restore. In production, you'd want to:
// 1. Clear the table first
// 2. Handle foreign key constraints
// 3. Use transactions
}
console.log("Database restore completed");
}
}
export function createDatabaseBackupManager(
dbManager: DatabaseManager,
config: BackupConfig
): DatabaseBackupManager {
return new DatabaseBackupManager(dbManager, config);
}

64
src/db/config/database.ts Normal file
View File

@@ -0,0 +1,64 @@
import { createClient, type Client as LibSQLClient } from "@libsql/client";
interface DatabaseConfig {
url: string;
authToken?: string;
concurrentConnections?: number;
connectTimeoutMs?: number;
}
export class DatabaseManager {
private static instance: DatabaseManager;
private client: LibSQLClient | null = null;
private config: DatabaseConfig;
private constructor(config: DatabaseConfig) {
this.config = config;
}
static getInstance(config: DatabaseConfig): DatabaseManager {
if (!DatabaseManager.instance) {
DatabaseManager.instance = new DatabaseManager(config);
}
return DatabaseManager.instance;
}
initialize(): LibSQLClient {
if (!this.client) {
this.client = createClient({
url: this.config.url,
authToken: this.config.authToken,
});
}
return this.client;
}
getClient(): LibSQLClient {
if (!this.client) {
return this.initialize();
}
return this.client;
}
async close(): Promise<void> {
if (this.client) {
await this.client.close();
this.client = null;
}
}
async execute(query: string, params?: unknown[]): Promise<void> {
const client = this.getClient();
await client.execute(query, params as any);
}
async query<T>(query: string, params?: unknown[]): Promise<T[]> {
const client = this.getClient();
const result = await client.execute(query, params as any);
return result.rows as T[];
}
}
export function createDatabaseManager(config: DatabaseConfig): DatabaseManager {
return DatabaseManager.getInstance(config);
}

View File

@@ -0,0 +1,85 @@
import { createDatabaseManager, type DatabaseManager } from "./database";
interface EdgeRegion {
region: string;
url: string;
authToken?: string;
isPrimary?: boolean;
}
interface EdgeConfig {
primaryRegion: EdgeRegion;
edgeReplicas: EdgeRegion[];
fallbackRegion?: string;
}
export class EdgeDatabaseManager {
private primaryManager: DatabaseManager;
private edgeManagers: Map<string, DatabaseManager>;
private config: EdgeConfig;
constructor(config: EdgeConfig) {
this.config = config;
this.edgeManagers = new Map();
this.primaryManager = createDatabaseManager({
url: config.primaryRegion.url,
authToken: config.primaryRegion.authToken,
});
config.edgeReplicas.forEach((replica) => {
this.edgeManagers.set(replica.region, createDatabaseManager({
url: replica.url,
authToken: replica.authToken,
}));
});
}
getPrimary(): DatabaseManager {
return this.primaryManager;
}
getEdge(region: string): DatabaseManager {
const manager = this.edgeManagers.get(region);
if (!manager) {
throw new Error(`Edge region ${region} not found`);
}
return manager;
}
getDefaultEdge(): DatabaseManager {
if (this.config.edgeReplicas.length > 0 && this.config.edgeReplicas[0]) {
const region = this.config.edgeReplicas[0].region;
const manager = this.edgeManagers.get(region);
if (manager) {
return manager;
}
}
return this.primaryManager;
}
async close(): Promise<void> {
await this.primaryManager.close();
for (const manager of this.edgeManagers.values()) {
await manager.close();
}
}
async executeOnPrimary(query: string, params?: unknown[]): Promise<void> {
await this.primaryManager.execute(query, params);
}
async queryOnEdge<T>(region: string, query: string, params?: unknown[]): Promise<T[]> {
const manager = this.getEdge(region);
return manager.query<T>(query, params);
}
async queryOnDefaultEdge<T>(query: string, params?: unknown[]): Promise<T[]> {
const manager = this.getDefaultEdge();
return manager.query<T>(query, params);
}
}
export function createEdgeDatabaseManager(config: EdgeConfig): EdgeDatabaseManager {
return new EdgeDatabaseManager(config);
}

View File

@@ -0,0 +1,12 @@
import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client";
import * as schema from "../schema";
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
export const db = drizzle(client, { schema });
export type DrizzleDB = typeof db;

7
src/db/index.ts Normal file
View File

@@ -0,0 +1,7 @@
// Database exports
export { db, type DrizzleDB } from "./config/migrations";
export { DatabaseManager, createDatabaseManager } from "./config/database";
export { EdgeDatabaseManager, createEdgeDatabaseManager } from "./config/edge-database";
// Schema exports
export * from "./schema";

View File

@@ -0,0 +1,139 @@
CREATE TABLE `character_relationships` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`character_a_id` integer NOT NULL,
`character_b_id` integer NOT NULL,
`relationship_type` text NOT NULL,
`description` text,
`strength` integer DEFAULT 50 NOT NULL,
`is_antagonistic` integer DEFAULT false NOT NULL,
`created_at` integer,
`updated_at` integer,
FOREIGN KEY (`character_a_id`) REFERENCES `characters`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`character_b_id`) REFERENCES `characters`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `character_relationships_unique_pair` ON `character_relationships` (`character_a_id`,`character_b_id`);--> statement-breakpoint
CREATE TABLE `characters` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`project_id` integer NOT NULL,
`name` text NOT NULL,
`slug` text NOT NULL,
`role` text DEFAULT 'supporting' NOT NULL,
`bio` text,
`description` text,
`arc` text,
`arc_type` text,
`age` integer,
`gender` text,
`voice` text,
`traits` text,
`motivation` text,
`conflict` text,
`secret` text,
`image_url` text,
`created_at` integer,
`updated_at` integer,
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `projects` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`description` text,
`owner_id` integer NOT NULL,
`is_public` integer DEFAULT false NOT NULL,
`theme` text,
`created_at` integer DEFAULT '"2026-04-24T14:30:03.715Z"' NOT NULL,
`updated_at` integer DEFAULT '"2026-04-24T14:30:03.715Z"' NOT NULL,
FOREIGN KEY (`owner_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `revision_changes` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`revision_id` integer NOT NULL,
`change_type` text NOT NULL,
`element_type` text,
`old_content` text,
`new_content` text,
`scene_number` integer,
`line_number` integer,
`page_number` integer,
`created_at` integer NOT NULL,
FOREIGN KEY (`revision_id`) REFERENCES `revisions`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE INDEX `revision_changes_revision_idx` ON `revision_changes` (`revision_id`);--> statement-breakpoint
CREATE INDEX `revision_changes_type_idx` ON `revision_changes` (`change_type`);--> statement-breakpoint
CREATE TABLE `revisions` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`script_id` integer NOT NULL,
`version_number` integer NOT NULL,
`branch_name` text DEFAULT 'main' NOT NULL,
`parent_revision_id` integer,
`title` text NOT NULL,
`summary` text,
`content` text NOT NULL,
`author_id` integer NOT NULL,
`status` text DEFAULT 'draft' NOT NULL,
`reviewed_by_id` integer,
`reviewed_at` integer,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
FOREIGN KEY (`script_id`) REFERENCES `scripts`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`reviewed_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE INDEX `revisions_script_version_idx` ON `revisions` (`script_id`,`version_number`);--> statement-breakpoint
CREATE INDEX `revisions_script_branch_idx` ON `revisions` (`script_id`,`branch_name`);--> statement-breakpoint
CREATE INDEX `revisions_author_idx` ON `revisions` (`author_id`);--> statement-breakpoint
CREATE TABLE `scene_characters` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`scene_id` integer NOT NULL,
`character_id` integer NOT NULL,
`screen_time` integer,
`dialogue_lines` integer DEFAULT 0,
FOREIGN KEY (`scene_id`) REFERENCES `scenes`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`character_id`) REFERENCES `characters`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `scenes` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`project_id` integer NOT NULL,
`title` text NOT NULL,
`content` text DEFAULT '' NOT NULL,
`order` integer DEFAULT 0 NOT NULL,
`created_at` integer,
`updated_at` integer,
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `scripts` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`project_id` integer NOT NULL,
`title` text NOT NULL,
`slug` text NOT NULL,
`genre` text,
`logline` text,
`status` text DEFAULT 'draft' NOT NULL,
`current_version` integer DEFAULT 1 NOT NULL,
`created_at` integer DEFAULT '"2026-04-24T14:30:03.720Z"' NOT NULL,
`updated_at` integer DEFAULT '"2026-04-24T14:30:03.720Z"' NOT NULL,
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `users` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`email` text NOT NULL,
`username` text NOT NULL,
`full_name` text,
`avatar_url` text,
`role` text DEFAULT 'viewer' NOT NULL,
`is_active` integer DEFAULT true NOT NULL,
`last_login_at` integer,
`created_at` integer DEFAULT '"2026-04-24T14:30:03.711Z"' NOT NULL,
`updated_at` integer DEFAULT '"2026-04-24T14:30:03.711Z"' NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);

View File

@@ -0,0 +1,22 @@
DROP INDEX "character_relationships_unique_pair";--> statement-breakpoint
DROP INDEX "revision_changes_revision_idx";--> statement-breakpoint
DROP INDEX "revision_changes_type_idx";--> statement-breakpoint
DROP INDEX "revisions_script_version_idx";--> statement-breakpoint
DROP INDEX "revisions_script_branch_idx";--> statement-breakpoint
DROP INDEX "revisions_author_idx";--> statement-breakpoint
DROP INDEX "users_email_unique";--> statement-breakpoint
DROP INDEX "users_username_unique";--> statement-breakpoint
ALTER TABLE `projects` ALTER COLUMN "created_at" TO "created_at" integer NOT NULL DEFAULT '"2026-04-24T15:28:03.755Z"';--> statement-breakpoint
CREATE UNIQUE INDEX `character_relationships_unique_pair` ON `character_relationships` (`character_a_id`,`character_b_id`);--> statement-breakpoint
CREATE INDEX `revision_changes_revision_idx` ON `revision_changes` (`revision_id`);--> statement-breakpoint
CREATE INDEX `revision_changes_type_idx` ON `revision_changes` (`change_type`);--> statement-breakpoint
CREATE INDEX `revisions_script_version_idx` ON `revisions` (`script_id`,`version_number`);--> statement-breakpoint
CREATE INDEX `revisions_script_branch_idx` ON `revisions` (`script_id`,`branch_name`);--> statement-breakpoint
CREATE INDEX `revisions_author_idx` ON `revisions` (`author_id`);--> statement-breakpoint
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);--> statement-breakpoint
ALTER TABLE `projects` ALTER COLUMN "updated_at" TO "updated_at" integer NOT NULL DEFAULT '"2026-04-24T15:28:03.755Z"';--> statement-breakpoint
ALTER TABLE `scripts` ALTER COLUMN "created_at" TO "created_at" integer NOT NULL DEFAULT '"2026-04-24T15:28:03.757Z"';--> statement-breakpoint
ALTER TABLE `scripts` ALTER COLUMN "updated_at" TO "updated_at" integer NOT NULL DEFAULT '"2026-04-24T15:28:03.757Z"';--> statement-breakpoint
ALTER TABLE `users` ALTER COLUMN "created_at" TO "created_at" integer NOT NULL DEFAULT '"2026-04-24T15:28:03.752Z"';--> statement-breakpoint
ALTER TABLE `users` ALTER COLUMN "updated_at" TO "updated_at" integer NOT NULL DEFAULT '"2026-04-24T15:28:03.752Z"';

View File

@@ -0,0 +1,998 @@
{
"version": "6",
"dialect": "sqlite",
"id": "0b85a4db-4b45-48e9-8e11-d2d423ac9ac8",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"character_relationships": {
"name": "character_relationships",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"character_a_id": {
"name": "character_a_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"character_b_id": {
"name": "character_b_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"relationship_type": {
"name": "relationship_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"strength": {
"name": "strength",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 50
},
"is_antagonistic": {
"name": "is_antagonistic",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"character_relationships_unique_pair": {
"name": "character_relationships_unique_pair",
"columns": [
"character_a_id",
"character_b_id"
],
"isUnique": true
}
},
"foreignKeys": {
"character_relationships_character_a_id_characters_id_fk": {
"name": "character_relationships_character_a_id_characters_id_fk",
"tableFrom": "character_relationships",
"tableTo": "characters",
"columnsFrom": [
"character_a_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"character_relationships_character_b_id_characters_id_fk": {
"name": "character_relationships_character_b_id_characters_id_fk",
"tableFrom": "character_relationships",
"tableTo": "characters",
"columnsFrom": [
"character_b_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"characters": {
"name": "characters",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"project_id": {
"name": "project_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'supporting'"
},
"bio": {
"name": "bio",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"arc": {
"name": "arc",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"arc_type": {
"name": "arc_type",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"age": {
"name": "age",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"gender": {
"name": "gender",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"voice": {
"name": "voice",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"traits": {
"name": "traits",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"motivation": {
"name": "motivation",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"conflict": {
"name": "conflict",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"secret": {
"name": "secret",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"image_url": {
"name": "image_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"characters_project_id_projects_id_fk": {
"name": "characters_project_id_projects_id_fk",
"tableFrom": "characters",
"tableTo": "projects",
"columnsFrom": [
"project_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"projects": {
"name": "projects",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner_id": {
"name": "owner_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_public": {
"name": "is_public",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"theme": {
"name": "theme",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'\"2026-04-24T14:30:03.715Z\"'"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'\"2026-04-24T14:30:03.715Z\"'"
}
},
"indexes": {},
"foreignKeys": {
"projects_owner_id_users_id_fk": {
"name": "projects_owner_id_users_id_fk",
"tableFrom": "projects",
"tableTo": "users",
"columnsFrom": [
"owner_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"revision_changes": {
"name": "revision_changes",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"revision_id": {
"name": "revision_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"change_type": {
"name": "change_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"element_type": {
"name": "element_type",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"old_content": {
"name": "old_content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"new_content": {
"name": "new_content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"scene_number": {
"name": "scene_number",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"line_number": {
"name": "line_number",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"page_number": {
"name": "page_number",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"revision_changes_revision_idx": {
"name": "revision_changes_revision_idx",
"columns": [
"revision_id"
],
"isUnique": false
},
"revision_changes_type_idx": {
"name": "revision_changes_type_idx",
"columns": [
"change_type"
],
"isUnique": false
}
},
"foreignKeys": {
"revision_changes_revision_id_revisions_id_fk": {
"name": "revision_changes_revision_id_revisions_id_fk",
"tableFrom": "revision_changes",
"tableTo": "revisions",
"columnsFrom": [
"revision_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"revisions": {
"name": "revisions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"script_id": {
"name": "script_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"version_number": {
"name": "version_number",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"branch_name": {
"name": "branch_name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'main'"
},
"parent_revision_id": {
"name": "parent_revision_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"summary": {
"name": "summary",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"author_id": {
"name": "author_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'draft'"
},
"reviewed_by_id": {
"name": "reviewed_by_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"reviewed_at": {
"name": "reviewed_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"revisions_script_version_idx": {
"name": "revisions_script_version_idx",
"columns": [
"script_id",
"version_number"
],
"isUnique": false
},
"revisions_script_branch_idx": {
"name": "revisions_script_branch_idx",
"columns": [
"script_id",
"branch_name"
],
"isUnique": false
},
"revisions_author_idx": {
"name": "revisions_author_idx",
"columns": [
"author_id"
],
"isUnique": false
}
},
"foreignKeys": {
"revisions_script_id_scripts_id_fk": {
"name": "revisions_script_id_scripts_id_fk",
"tableFrom": "revisions",
"tableTo": "scripts",
"columnsFrom": [
"script_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"revisions_author_id_users_id_fk": {
"name": "revisions_author_id_users_id_fk",
"tableFrom": "revisions",
"tableTo": "users",
"columnsFrom": [
"author_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"revisions_reviewed_by_id_users_id_fk": {
"name": "revisions_reviewed_by_id_users_id_fk",
"tableFrom": "revisions",
"tableTo": "users",
"columnsFrom": [
"reviewed_by_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"scene_characters": {
"name": "scene_characters",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"scene_id": {
"name": "scene_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"character_id": {
"name": "character_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"screen_time": {
"name": "screen_time",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"dialogue_lines": {
"name": "dialogue_lines",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"scene_characters_scene_id_scenes_id_fk": {
"name": "scene_characters_scene_id_scenes_id_fk",
"tableFrom": "scene_characters",
"tableTo": "scenes",
"columnsFrom": [
"scene_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"scene_characters_character_id_characters_id_fk": {
"name": "scene_characters_character_id_characters_id_fk",
"tableFrom": "scene_characters",
"tableTo": "characters",
"columnsFrom": [
"character_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"scenes": {
"name": "scenes",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"project_id": {
"name": "project_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"scenes_project_id_projects_id_fk": {
"name": "scenes_project_id_projects_id_fk",
"tableFrom": "scenes",
"tableTo": "projects",
"columnsFrom": [
"project_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"scripts": {
"name": "scripts",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"project_id": {
"name": "project_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"genre": {
"name": "genre",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"logline": {
"name": "logline",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'draft'"
},
"current_version": {
"name": "current_version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 1
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'\"2026-04-24T14:30:03.720Z\"'"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'\"2026-04-24T14:30:03.720Z\"'"
}
},
"indexes": {},
"foreignKeys": {
"scripts_project_id_projects_id_fk": {
"name": "scripts_project_id_projects_id_fk",
"tableFrom": "scripts",
"tableTo": "projects",
"columnsFrom": [
"project_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"full_name": {
"name": "full_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'viewer'"
},
"is_active": {
"name": "is_active",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"last_login_at": {
"name": "last_login_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'\"2026-04-24T14:30:03.711Z\"'"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'\"2026-04-24T14:30:03.711Z\"'"
}
},
"indexes": {
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
},
"users_username_unique": {
"name": "users_username_unique",
"columns": [
"username"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,998 @@
{
"version": "6",
"dialect": "sqlite",
"id": "14d8455a-98a3-4c7a-806c-10a91f25630f",
"prevId": "0b85a4db-4b45-48e9-8e11-d2d423ac9ac8",
"tables": {
"character_relationships": {
"name": "character_relationships",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"character_a_id": {
"name": "character_a_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"character_b_id": {
"name": "character_b_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"relationship_type": {
"name": "relationship_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"strength": {
"name": "strength",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 50
},
"is_antagonistic": {
"name": "is_antagonistic",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"character_relationships_unique_pair": {
"name": "character_relationships_unique_pair",
"columns": [
"character_a_id",
"character_b_id"
],
"isUnique": true
}
},
"foreignKeys": {
"character_relationships_character_a_id_characters_id_fk": {
"name": "character_relationships_character_a_id_characters_id_fk",
"tableFrom": "character_relationships",
"tableTo": "characters",
"columnsFrom": [
"character_a_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"character_relationships_character_b_id_characters_id_fk": {
"name": "character_relationships_character_b_id_characters_id_fk",
"tableFrom": "character_relationships",
"tableTo": "characters",
"columnsFrom": [
"character_b_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"characters": {
"name": "characters",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"project_id": {
"name": "project_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'supporting'"
},
"bio": {
"name": "bio",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"arc": {
"name": "arc",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"arc_type": {
"name": "arc_type",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"age": {
"name": "age",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"gender": {
"name": "gender",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"voice": {
"name": "voice",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"traits": {
"name": "traits",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"motivation": {
"name": "motivation",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"conflict": {
"name": "conflict",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"secret": {
"name": "secret",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"image_url": {
"name": "image_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"characters_project_id_projects_id_fk": {
"name": "characters_project_id_projects_id_fk",
"tableFrom": "characters",
"tableTo": "projects",
"columnsFrom": [
"project_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"projects": {
"name": "projects",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner_id": {
"name": "owner_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_public": {
"name": "is_public",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"theme": {
"name": "theme",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'\"2026-04-24T15:28:03.755Z\"'"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'\"2026-04-24T15:28:03.755Z\"'"
}
},
"indexes": {},
"foreignKeys": {
"projects_owner_id_users_id_fk": {
"name": "projects_owner_id_users_id_fk",
"tableFrom": "projects",
"tableTo": "users",
"columnsFrom": [
"owner_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"revision_changes": {
"name": "revision_changes",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"revision_id": {
"name": "revision_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"change_type": {
"name": "change_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"element_type": {
"name": "element_type",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"old_content": {
"name": "old_content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"new_content": {
"name": "new_content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"scene_number": {
"name": "scene_number",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"line_number": {
"name": "line_number",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"page_number": {
"name": "page_number",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"revision_changes_revision_idx": {
"name": "revision_changes_revision_idx",
"columns": [
"revision_id"
],
"isUnique": false
},
"revision_changes_type_idx": {
"name": "revision_changes_type_idx",
"columns": [
"change_type"
],
"isUnique": false
}
},
"foreignKeys": {
"revision_changes_revision_id_revisions_id_fk": {
"name": "revision_changes_revision_id_revisions_id_fk",
"tableFrom": "revision_changes",
"tableTo": "revisions",
"columnsFrom": [
"revision_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"revisions": {
"name": "revisions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"script_id": {
"name": "script_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"version_number": {
"name": "version_number",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"branch_name": {
"name": "branch_name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'main'"
},
"parent_revision_id": {
"name": "parent_revision_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"summary": {
"name": "summary",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"author_id": {
"name": "author_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'draft'"
},
"reviewed_by_id": {
"name": "reviewed_by_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"reviewed_at": {
"name": "reviewed_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"revisions_script_version_idx": {
"name": "revisions_script_version_idx",
"columns": [
"script_id",
"version_number"
],
"isUnique": false
},
"revisions_script_branch_idx": {
"name": "revisions_script_branch_idx",
"columns": [
"script_id",
"branch_name"
],
"isUnique": false
},
"revisions_author_idx": {
"name": "revisions_author_idx",
"columns": [
"author_id"
],
"isUnique": false
}
},
"foreignKeys": {
"revisions_script_id_scripts_id_fk": {
"name": "revisions_script_id_scripts_id_fk",
"tableFrom": "revisions",
"tableTo": "scripts",
"columnsFrom": [
"script_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"revisions_author_id_users_id_fk": {
"name": "revisions_author_id_users_id_fk",
"tableFrom": "revisions",
"tableTo": "users",
"columnsFrom": [
"author_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"revisions_reviewed_by_id_users_id_fk": {
"name": "revisions_reviewed_by_id_users_id_fk",
"tableFrom": "revisions",
"tableTo": "users",
"columnsFrom": [
"reviewed_by_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"scene_characters": {
"name": "scene_characters",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"scene_id": {
"name": "scene_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"character_id": {
"name": "character_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"screen_time": {
"name": "screen_time",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"dialogue_lines": {
"name": "dialogue_lines",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"scene_characters_scene_id_scenes_id_fk": {
"name": "scene_characters_scene_id_scenes_id_fk",
"tableFrom": "scene_characters",
"tableTo": "scenes",
"columnsFrom": [
"scene_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"scene_characters_character_id_characters_id_fk": {
"name": "scene_characters_character_id_characters_id_fk",
"tableFrom": "scene_characters",
"tableTo": "characters",
"columnsFrom": [
"character_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"scenes": {
"name": "scenes",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"project_id": {
"name": "project_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"scenes_project_id_projects_id_fk": {
"name": "scenes_project_id_projects_id_fk",
"tableFrom": "scenes",
"tableTo": "projects",
"columnsFrom": [
"project_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"scripts": {
"name": "scripts",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"project_id": {
"name": "project_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"genre": {
"name": "genre",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"logline": {
"name": "logline",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'draft'"
},
"current_version": {
"name": "current_version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 1
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'\"2026-04-24T15:28:03.757Z\"'"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'\"2026-04-24T15:28:03.757Z\"'"
}
},
"indexes": {},
"foreignKeys": {
"scripts_project_id_projects_id_fk": {
"name": "scripts_project_id_projects_id_fk",
"tableFrom": "scripts",
"tableTo": "projects",
"columnsFrom": [
"project_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"full_name": {
"name": "full_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'viewer'"
},
"is_active": {
"name": "is_active",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"last_login_at": {
"name": "last_login_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'\"2026-04-24T15:28:03.752Z\"'"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'\"2026-04-24T15:28:03.752Z\"'"
}
},
"indexes": {
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
},
"users_username_unique": {
"name": "users_username_unique",
"columns": [
"username"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,20 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1777041003742,
"tag": "0000_complex_donald_blake",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1777044483775,
"tag": "0001_tan_machine_man",
"breakpoints": true
}
]
}

18
src/db/schema/projects.ts Normal file
View File

@@ -0,0 +1,18 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import { users } from "./users";
export const projects = sqliteTable("projects", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
description: text("description"),
ownerId: integer("owner_id")
.notNull()
.references(() => users.id),
isPublic: integer("is_public", { mode: "boolean" }).notNull().default(false),
theme: text("theme"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(new Date()),
});
export type Project = typeof projects.$inferSelect;
export type NewProject = typeof projects.$inferInsert;

32
src/db/schema/scenes.ts Normal file
View File

@@ -0,0 +1,32 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import { projects } from "./projects";
import { characters } from "./characters";
export const scenes = sqliteTable("scenes", {
id: integer("id").primaryKey({ autoIncrement: true }),
projectId: integer("project_id")
.notNull()
.references(() => projects.id),
title: text("title").notNull(),
content: text("content").notNull().default(""),
order: integer("order").notNull().default(0),
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
});
export const sceneCharacters = sqliteTable("scene_characters", {
id: integer("id").primaryKey({ autoIncrement: true }),
sceneId: integer("scene_id")
.notNull()
.references(() => scenes.id),
characterId: integer("character_id")
.notNull()
.references(() => characters.id),
screenTime: integer("screen_time"),
dialogueLines: integer("dialogue_lines").default(0),
});
export type Scene = typeof scenes.$inferSelect;
export type NewScene = typeof scenes.$inferInsert;
export type SceneCharacter = typeof sceneCharacters.$inferSelect;
export type NewSceneCharacter = typeof sceneCharacters.$inferInsert;

20
src/db/schema/scripts.ts Normal file
View File

@@ -0,0 +1,20 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import { projects } from "./projects";
export const scripts = sqliteTable("scripts", {
id: integer("id").primaryKey({ autoIncrement: true }),
projectId: integer("project_id")
.notNull()
.references(() => projects.id),
title: text("title").notNull(),
slug: text("slug").notNull(),
genre: text("genre"),
logline: text("logline"),
status: text("status", { enum: ["draft", "revision", "final", "published"] }).notNull().default("draft"),
currentVersion: integer("current_version").notNull().default(1),
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(new Date()),
});
export type Script = typeof scripts.$inferSelect;
export type NewScript = typeof scripts.$inferInsert;

17
src/db/schema/users.ts Normal file
View File

@@ -0,0 +1,17 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
email: text("email").notNull().unique(),
username: text("username").notNull().unique(),
fullName: text("full_name"),
avatarUrl: text("avatar_url"),
role: text("role", { enum: ["admin", "editor", "viewer"] }).notNull().default("viewer"),
isActive: integer("is_active", { mode: "boolean" }).notNull().default(true),
lastLoginAt: integer("last_login_at", { mode: "timestamp" }),
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(new Date()),
});
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;

93
src/db/seed.ts Normal file
View File

@@ -0,0 +1,93 @@
import { db } from "./config/migrations";
import { users } from "./schema/users";
import { projects } from "./schema/projects";
import { scripts } from "./schema/scripts";
import { characters } from "./schema/characters";
import { scenes, sceneCharacters } from "./schema/scenes";
export async function seedDatabase() {
console.log("Seeding database...");
// Create admin user
const admin = await db.insert(users).values({
email: "admin@frenocorp.com",
username: "admin",
fullName: "Admin User",
role: "admin",
}).returning();
if (!admin[0]) throw new Error("Failed to create admin user");
// Create test project
const project = await db.insert(projects).values({
name: "Test Project",
description: "A test project for development",
ownerId: admin[0].id,
isPublic: true,
}).returning();
if (!project[0]) throw new Error("Failed to create project");
// Create test script
const script = await db.insert(scripts).values({
projectId: project[0].id,
title: "Test Screenplay",
slug: "test-screenplay",
genre: "Drama",
logline: "A test screenplay for development purposes",
status: "draft",
}).returning();
if (!script[0]) throw new Error("Failed to create script");
// Create test character
const character = await db.insert(characters).values({
scriptId: script[0].id,
name: "John Doe",
role: "protagonist",
description: "The main character",
age: 30,
gender: "male",
}).returning();
if (!character[0]) throw new Error("Failed to create character");
// Create test scene
const scene = await db.insert(scenes).values({
scriptId: script[0].id,
sceneNumber: 1,
actNumber: 1,
slugline: "INT. OFFICE - DAY",
location: "Office",
timeOfDay: "DAY",
content: "John sits at his desk, contemplating his next move.",
summary: "John in his office",
}).returning();
if (!scene[0]) throw new Error("Failed to create scene");
// Link character to scene
await db.insert(sceneCharacters).values({
sceneId: scene[0].id,
characterId: character[0].id,
screenTime: 5,
dialogueLines: 3,
});
console.log("Database seeded successfully!");
console.log({
user: admin[0],
project: project[0],
script: script[0],
character: character[0],
scene: scene[0],
});
return {
user: admin[0],
project: project[0],
script: script[0],
character: character[0],
scene: scene[0],
};
}