diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..59a4a161f --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Turso Database Configuration +TURSO_DATABASE_URL=libsql://-.turso.io +TURSO_AUTH_TOKEN= + +# Backup Configuration (optional) +BACKUP_INTERVAL_MS=86400000 +BACKUP_RETENTION_DAYS=30 +BACKUP_REGION=us-east + +# Clerk Authentication +VITE_CLERK_PUBLISHABLE_KEY=pk_ +VITE_CLERK_SIGN_IN_URL=/sign-in +VITE_CLERK_SIGN_UP_URL=/sign-up diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 000000000..bdc51d448 --- /dev/null +++ b/drizzle.config.ts @@ -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!, + }, +}); diff --git a/package.json b/package.json index 219fe5492..a22a60978 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/db/README.md b/src/db/README.md new file mode 100644 index 000000000..7da6044c0 --- /dev/null +++ b/src/db/README.md @@ -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://-.turso.io" +TURSO_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("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 diff --git a/src/db/backup/index.ts b/src/db/backup/index.ts new file mode 100644 index 000000000..baeb9a8c7 --- /dev/null +++ b/src/db/backup/index.ts @@ -0,0 +1,2 @@ +// Backup exports +export { DatabaseBackupManager, createDatabaseBackupManager } from "../config/backup"; diff --git a/src/db/config/backup.ts b/src/db/config/backup.ts new file mode 100644 index 000000000..9ee88eb61 --- /dev/null +++ b/src/db/config/backup.ts @@ -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 { + try { + console.log(`Performing database backup to ${this.config.backupRegion}...`); + + // Get all tables + const tables = await this.dbManager.query( + "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'" + ); + + for (const table of tables) { + const data = await this.dbManager.query>( + `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 { + 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 { + 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[]; + 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); +} diff --git a/src/db/config/database.ts b/src/db/config/database.ts new file mode 100644 index 000000000..00da9c5a4 --- /dev/null +++ b/src/db/config/database.ts @@ -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 { + if (this.client) { + await this.client.close(); + this.client = null; + } + } + + async execute(query: string, params?: unknown[]): Promise { + const client = this.getClient(); + await client.execute(query, params as any); + } + + async query(query: string, params?: unknown[]): Promise { + 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); +} diff --git a/src/db/config/edge-database.ts b/src/db/config/edge-database.ts new file mode 100644 index 000000000..c469d8f87 --- /dev/null +++ b/src/db/config/edge-database.ts @@ -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; + 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 { + await this.primaryManager.close(); + for (const manager of this.edgeManagers.values()) { + await manager.close(); + } + } + + async executeOnPrimary(query: string, params?: unknown[]): Promise { + await this.primaryManager.execute(query, params); + } + + async queryOnEdge(region: string, query: string, params?: unknown[]): Promise { + const manager = this.getEdge(region); + return manager.query(query, params); + } + + async queryOnDefaultEdge(query: string, params?: unknown[]): Promise { + const manager = this.getDefaultEdge(); + return manager.query(query, params); + } +} + +export function createEdgeDatabaseManager(config: EdgeConfig): EdgeDatabaseManager { + return new EdgeDatabaseManager(config); +} diff --git a/src/db/config/migrations.ts b/src/db/config/migrations.ts new file mode 100644 index 000000000..0443677a2 --- /dev/null +++ b/src/db/config/migrations.ts @@ -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; diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 000000000..5f8a02235 --- /dev/null +++ b/src/db/index.ts @@ -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"; diff --git a/src/db/migrations/0000_complex_donald_blake.sql b/src/db/migrations/0000_complex_donald_blake.sql new file mode 100644 index 000000000..f2f849eb4 --- /dev/null +++ b/src/db/migrations/0000_complex_donald_blake.sql @@ -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`); \ No newline at end of file diff --git a/src/db/migrations/0001_tan_machine_man.sql b/src/db/migrations/0001_tan_machine_man.sql new file mode 100644 index 000000000..25f46c3e1 --- /dev/null +++ b/src/db/migrations/0001_tan_machine_man.sql @@ -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"'; \ No newline at end of file diff --git a/src/db/migrations/meta/0000_snapshot.json b/src/db/migrations/meta/0000_snapshot.json new file mode 100644 index 000000000..0126c4452 --- /dev/null +++ b/src/db/migrations/meta/0000_snapshot.json @@ -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": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/0001_snapshot.json b/src/db/migrations/meta/0001_snapshot.json new file mode 100644 index 000000000..b8124c5a1 --- /dev/null +++ b/src/db/migrations/meta/0001_snapshot.json @@ -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": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json new file mode 100644 index 000000000..cf871fe5f --- /dev/null +++ b/src/db/migrations/meta/_journal.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/src/db/schema/projects.ts b/src/db/schema/projects.ts new file mode 100644 index 000000000..6df996dff --- /dev/null +++ b/src/db/schema/projects.ts @@ -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; diff --git a/src/db/schema/scenes.ts b/src/db/schema/scenes.ts new file mode 100644 index 000000000..0da88927e --- /dev/null +++ b/src/db/schema/scenes.ts @@ -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; diff --git a/src/db/schema/scripts.ts b/src/db/schema/scripts.ts new file mode 100644 index 000000000..284529e55 --- /dev/null +++ b/src/db/schema/scripts.ts @@ -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; diff --git a/src/db/schema/users.ts b/src/db/schema/users.ts new file mode 100644 index 000000000..42ce8222a --- /dev/null +++ b/src/db/schema/users.ts @@ -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; diff --git a/src/db/seed.ts b/src/db/seed.ts new file mode 100644 index 000000000..236134c43 --- /dev/null +++ b/src/db/seed.ts @@ -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], + }; +}