From 815279f49fcc547dc94b47c6e353914d7d2aa0a9 Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 26 Dec 2025 00:35:07 +0100 Subject: [PATCH 1/2] feat(db): add shared database library with sql config Introduce a shared database schema library for Drizzle ORM and configure its TypeScript build settings. Move the canonical schema into libs/shared/database/src/lib/schema.ts and expose strong typing and table definitions for playlists, categories, content, recently_viewed and favorites. Add tsconfig.lib.json to set outDir, declaration output and node types for the shared lib. Keep apps/electron-backend compatibility by re-exporting the schema and types from the new shared library (apps/electron-backend now imports the shared definitions rather than duplicating them). These changes centralize the database schema as a single source of truth, prevent duplication, and enable read-only consumers (e.g. agent-backend) to depend on compiled type declarations. --- .../src/app/database/connection.ts | 152 +------------- .../src/app/database/schema.ts | 127 ++---------- libs/shared/database/README.md | 33 +++ libs/shared/database/project.json | 23 +++ libs/shared/database/src/index.ts | 8 + libs/shared/database/src/lib/connection.ts | 189 ++++++++++++++++++ libs/shared/database/src/lib/schema.ts | 140 +++++++++++++ libs/shared/database/tsconfig.json | 21 ++ libs/shared/database/tsconfig.lib.json | 10 + tsconfig.base.json | 3 +- 10 files changed, 457 insertions(+), 249 deletions(-) create mode 100644 libs/shared/database/README.md create mode 100644 libs/shared/database/project.json create mode 100644 libs/shared/database/src/index.ts create mode 100644 libs/shared/database/src/lib/connection.ts create mode 100644 libs/shared/database/src/lib/schema.ts create mode 100644 libs/shared/database/tsconfig.json create mode 100644 libs/shared/database/tsconfig.lib.json diff --git a/apps/electron-backend/src/app/database/connection.ts b/apps/electron-backend/src/app/database/connection.ts index e9108cd24..eaaff155d 100644 --- a/apps/electron-backend/src/app/database/connection.ts +++ b/apps/electron-backend/src/app/database/connection.ts @@ -1,146 +1,14 @@ /** - * Database connection and initialization for Electron - * Uses Drizzle ORM with better-sqlite3 - * Stores database file under ~/.iptvnator/databases/ + * Database connection re-export from shared library + * This file maintains backwards compatibility for existing imports */ -import Database from 'better-sqlite3'; -import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'; -import { drizzle } from 'drizzle-orm/better-sqlite3'; -import { existsSync, mkdirSync } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; -import * as schema from './schema'; +export { + getDatabase, + initDatabase, + getDatabasePath, + closeDatabase, + type DatabaseInstance, + type DatabaseOptions, +} from 'database'; -let db: BetterSQLite3Database | null = null; -let sqlite: Database.Database | null = null; -let initPromise: Promise> | null = null; - -/** - * Get the database file path - */ -function getDatabasePath(): string { - const dbDir = join(homedir(), '.iptvnator', 'databases'); - - // Ensure the directory exists - if (!existsSync(dbDir)) { - mkdirSync(dbDir, { recursive: true }); - } - - return join(dbDir, 'iptvnator.db'); -} - -/** - * Initialize the database connection - * Creates tables if they don't exist - */ -export async function initDatabase(): Promise> { - if (db) return db; - if (initPromise) return initPromise; - - initPromise = (async () => { - const filePath = getDatabasePath(); - sqlite = new Database(filePath); - - // Enable foreign keys - sqlite.pragma('foreign_keys = ON'); - - const database = drizzle(sqlite, { schema }); - - // Create tables if they don't exist - createTables(); - - db = database; - return database; - })(); - - return initPromise; -} - -/** - * Create tables if they don't exist - */ -function createTables() { - if (!sqlite) return; - - const statements = [ - `CREATE TABLE IF NOT EXISTS playlists ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - serverUrl TEXT, - username TEXT, - password TEXT, - date_created TEXT DEFAULT (datetime('now')), - last_updated TEXT, - type TEXT NOT NULL CHECK (type IN ('xtream', 'stalker', 'm3u-file', 'm3u-text', 'm3u-url')), - userAgent TEXT, - origin TEXT, - referrer TEXT, - filePath TEXT, - autoRefresh INTEGER DEFAULT 0, - macAddress TEXT, - url TEXT, - last_usage TEXT - )`, - `CREATE TABLE IF NOT EXISTS categories ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - playlist_id TEXT NOT NULL, - name TEXT NOT NULL, - type TEXT NOT NULL CHECK (type IN ('live', 'movies', 'series')), - xtream_id INTEGER NOT NULL, - FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE - )`, - `CREATE TABLE IF NOT EXISTS content ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - category_id INTEGER NOT NULL, - title TEXT NOT NULL, - rating TEXT, - added TEXT, - poster_url TEXT, - xtream_id INTEGER NOT NULL, - type TEXT NOT NULL CHECK (type IN ('live', 'movie', 'series')), - FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE - )`, - `CREATE TABLE IF NOT EXISTS recently_viewed ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - content_id INTEGER NOT NULL, - playlist_id TEXT NOT NULL, - viewed_at TEXT DEFAULT (datetime('now')), - FOREIGN KEY (content_id) REFERENCES content (id) ON DELETE CASCADE, - FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE - )`, - `CREATE TABLE IF NOT EXISTS favorites ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - content_id INTEGER NOT NULL, - playlist_id TEXT NOT NULL, - added_at TEXT DEFAULT (datetime('now')), - FOREIGN KEY (content_id) REFERENCES content(id) ON DELETE CASCADE, - FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE - )`, - `CREATE INDEX IF NOT EXISTS idx_content_type ON content(type)`, - `CREATE INDEX IF NOT EXISTS idx_content_category ON content(category_id)`, - `CREATE INDEX IF NOT EXISTS idx_categories_playlist ON categories(playlist_id)`, - `CREATE INDEX IF NOT EXISTS idx_content_title ON content(title)`, - `CREATE INDEX IF NOT EXISTS idx_content_xtream ON content(xtream_id)`, - `CREATE INDEX IF NOT EXISTS idx_categories_type ON categories(type)`, - `CREATE UNIQUE INDEX IF NOT EXISTS favorites_content_playlist_unique ON favorites(content_id, playlist_id)`, - `CREATE INDEX IF NOT EXISTS favorites_playlist_idx ON favorites(playlist_id)`, - `CREATE INDEX IF NOT EXISTS favorites_content_idx ON favorites(content_id)`, - `CREATE UNIQUE INDEX IF NOT EXISTS recently_viewed_content_playlist_unique ON recently_viewed(content_id, playlist_id)`, - `CREATE INDEX IF NOT EXISTS recently_viewed_playlist_idx ON recently_viewed(playlist_id)`, - `CREATE INDEX IF NOT EXISTS recently_viewed_viewed_at_idx ON recently_viewed(viewed_at)`, - ]; - - for (const stmt of statements) { - sqlite.exec(stmt); - } -} - -/** - * Get the database instance - * Initializes the database if not already initialized - */ -export async function getDatabase(): Promise> { - if (db) return db; - return initDatabase(); -} diff --git a/apps/electron-backend/src/app/database/schema.ts b/apps/electron-backend/src/app/database/schema.ts index 8ebfa87e1..752ad0784 100644 --- a/apps/electron-backend/src/app/database/schema.ts +++ b/apps/electron-backend/src/app/database/schema.ts @@ -1,110 +1,25 @@ /** - * Drizzle ORM schema for IPTVnator database - * This schema defines the structure for Xtream Codes API data storage + * Database schema re-export from shared library + * This file maintains backwards compatibility for existing imports */ -import { sql } from 'drizzle-orm'; -import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +export { + // Tables + playlists, + categories, + content, + recentlyViewed, + favorites, + // Types + type Playlist, + type NewPlaylist, + type Category, + type NewCategory, + type Content, + type NewContent, + type RecentlyViewed, + type NewRecentlyViewed, + type Favorite, + type NewFavorite, +} from 'database'; -// Playlists table -export const playlists = sqliteTable('playlists', { - id: text('id').primaryKey(), - name: text('name').notNull(), - serverUrl: text('serverUrl'), - username: text('username'), - password: text('password'), - dateCreated: text('date_created').default(sql`CURRENT_TIMESTAMP`), - lastUpdated: text('last_updated'), - type: text('type', { - enum: ['xtream', 'stalker', 'm3u-file', 'm3u-text', 'm3u-url'], - }).notNull(), - userAgent: text('userAgent'), - origin: text('origin'), - referrer: text('referrer'), - filePath: text('filePath'), - autoRefresh: integer('autoRefresh', { mode: 'boolean' }).default(false), - macAddress: text('macAddress'), - url: text('url'), - lastUsage: text('last_usage'), -}); - -// Categories table -export const categories = sqliteTable( - 'categories', - { - id: integer('id').primaryKey({ autoIncrement: true }), - playlistId: text('playlist_id') - .notNull() - .references(() => playlists.id, { onDelete: 'cascade' }), - name: text('name').notNull(), - type: text('type', { enum: ['live', 'movies', 'series'] }).notNull(), - xtreamId: integer('xtream_id').notNull(), - }, - (table) => ({ - playlistIdx: index('idx_categories_playlist').on(table.playlistId), - typeIdx: index('idx_categories_type').on(table.type), - }) -); - -// Content table (streams/VODs/series) -export const content = sqliteTable( - 'content', - { - id: integer('id').primaryKey({ autoIncrement: true }), - categoryId: integer('category_id') - .notNull() - .references(() => categories.id, { onDelete: 'cascade' }), - title: text('title').notNull(), - rating: text('rating'), - added: text('added'), - posterUrl: text('poster_url'), - xtreamId: integer('xtream_id').notNull(), - type: text('type', { enum: ['live', 'movie', 'series'] }).notNull(), - }, - (table) => ({ - typeIdx: index('idx_content_type').on(table.type), - categoryIdx: index('idx_content_category').on(table.categoryId), - titleIdx: index('idx_content_title').on(table.title), - xtreamIdx: index('idx_content_xtream').on(table.xtreamId), - }) -); - -// Recently viewed table -export const recentlyViewed = sqliteTable('recently_viewed', { - id: integer('id').primaryKey({ autoIncrement: true }), - contentId: integer('content_id') - .notNull() - .references(() => content.id, { onDelete: 'cascade' }), - playlistId: text('playlist_id') - .notNull() - .references(() => playlists.id, { onDelete: 'cascade' }), - viewedAt: text('viewed_at').default(sql`CURRENT_TIMESTAMP`), -}); - -// Favorites table -export const favorites = sqliteTable('favorites', { - id: integer('id').primaryKey({ autoIncrement: true }), - contentId: integer('content_id') - .notNull() - .references(() => content.id, { onDelete: 'cascade' }), - playlistId: text('playlist_id') - .notNull() - .references(() => playlists.id, { onDelete: 'cascade' }), - addedAt: text('added_at').default(sql`CURRENT_TIMESTAMP`), -}); - -// Type exports for TypeScript -export type Playlist = typeof playlists.$inferSelect; -export type NewPlaylist = typeof playlists.$inferInsert; - -export type Category = typeof categories.$inferSelect; -export type NewCategory = typeof categories.$inferInsert; - -export type Content = typeof content.$inferSelect; -export type NewContent = typeof content.$inferInsert; - -export type RecentlyViewed = typeof recentlyViewed.$inferSelect; -export type NewRecentlyViewed = typeof recentlyViewed.$inferInsert; - -export type Favorite = typeof favorites.$inferSelect; -export type NewFavorite = typeof favorites.$inferInsert; diff --git a/libs/shared/database/README.md b/libs/shared/database/README.md new file mode 100644 index 000000000..651b51002 --- /dev/null +++ b/libs/shared/database/README.md @@ -0,0 +1,33 @@ +# Database + +Shared database library for IPTVnator providing Drizzle ORM schema and connection utilities. + +## Usage + +```typescript +// Full access (electron-backend) +import { getDatabase, initDatabase } from 'database'; + +// Read-only access (agent-backend) +import { getReadOnlyDatabase } from 'database'; + +// Schema and types +import { content, categories, playlists, type Content } from 'database'; +``` + +## Exports + +### Schema (`schema.ts`) +- **Tables**: `playlists`, `categories`, `content`, `recentlyViewed`, `favorites` +- **Types**: `Playlist`, `Category`, `Content`, `RecentlyViewed`, `Favorite` (and `New*` variants) + +### Connection (`connection.ts`) +- `getDatabase(options?)` - Full read-write access +- `getReadOnlyDatabase()` - Read-only access for agent queries +- `initDatabase(options?)` - Initialize with custom options +- `closeDatabase()` - Close connection +- `getDatabasePath()` - Get database file path + +## Database Location + +The SQLite database is stored at: `~/.iptvnator/databases/iptvnator.db` diff --git a/libs/shared/database/project.json b/libs/shared/database/project.json new file mode 100644 index 000000000..98f65eb3d --- /dev/null +++ b/libs/shared/database/project.json @@ -0,0 +1,23 @@ +{ + "name": "database", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/shared/database/src", + "projectType": "library", + "tags": ["scope:shared", "type:data-access"], + "targets": { + "build": { + "executor": "@nx/esbuild:esbuild", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/shared/database", + "main": "libs/shared/database/src/index.ts", + "tsConfig": "libs/shared/database/tsconfig.lib.json", + "assets": ["libs/shared/database/*.md"], + "format": ["cjs"] + } + }, + "lint": { + "command": "eslint libs/shared/database/**/*.ts" + } + } +} diff --git a/libs/shared/database/src/index.ts b/libs/shared/database/src/index.ts new file mode 100644 index 000000000..3734f7440 --- /dev/null +++ b/libs/shared/database/src/index.ts @@ -0,0 +1,8 @@ +/** + * Shared Database module for IPTVnator + * Provides Drizzle ORM schema and connection utilities for SQLite database + * Used by both electron-backend (read-write) and agent-backend (read-only) + */ + +export * from './lib/schema'; +export * from './lib/connection'; diff --git a/libs/shared/database/src/lib/connection.ts b/libs/shared/database/src/lib/connection.ts new file mode 100644 index 000000000..5aabfd38a --- /dev/null +++ b/libs/shared/database/src/lib/connection.ts @@ -0,0 +1,189 @@ +/** + * Database connection and initialization for IPTVnator + * Uses Drizzle ORM with better-sqlite3 + * Stores database file under ~/.iptvnator/databases/ + * + * Provides two connection modes: + * - Full access (for electron-backend): creates tables, read-write + * - Read-only access (for agent-backend): no table creation, read-only queries + */ + +import Database from 'better-sqlite3'; +import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'; +import { drizzle } from 'drizzle-orm/better-sqlite3'; +import { existsSync, mkdirSync } from 'fs'; +import { homedir } from 'os'; +import { join } from 'path'; +import * as schema from './schema'; + +export type DatabaseInstance = BetterSQLite3Database; + +let db: DatabaseInstance | null = null; +let sqlite: Database.Database | null = null; +let initPromise: Promise | null = null; + +/** + * Get the database file path + */ +export function getDatabasePath(): string { + const dbDir = join(homedir(), '.iptvnator', 'databases'); + + // Ensure the directory exists + if (!existsSync(dbDir)) { + mkdirSync(dbDir, { recursive: true }); + } + + return join(dbDir, 'iptvnator.db'); +} + +/** + * SQL statements for creating all tables + */ +const CREATE_TABLE_STATEMENTS = [ + `CREATE TABLE IF NOT EXISTS playlists ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + serverUrl TEXT, + username TEXT, + password TEXT, + date_created TEXT DEFAULT (datetime('now')), + last_updated TEXT, + type TEXT NOT NULL CHECK (type IN ('xtream', 'stalker', 'm3u-file', 'm3u-text', 'm3u-url')), + userAgent TEXT, + origin TEXT, + referrer TEXT, + filePath TEXT, + autoRefresh INTEGER DEFAULT 0, + macAddress TEXT, + url TEXT, + last_usage TEXT + )`, + `CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + playlist_id TEXT NOT NULL, + name TEXT NOT NULL, + type TEXT NOT NULL CHECK (type IN ('live', 'movies', 'series')), + xtream_id INTEGER NOT NULL, + FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE + )`, + `CREATE TABLE IF NOT EXISTS content ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER NOT NULL, + title TEXT NOT NULL, + rating TEXT, + added TEXT, + poster_url TEXT, + xtream_id INTEGER NOT NULL, + type TEXT NOT NULL CHECK (type IN ('live', 'movie', 'series')), + FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE + )`, + `CREATE TABLE IF NOT EXISTS recently_viewed ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + content_id INTEGER NOT NULL, + playlist_id TEXT NOT NULL, + viewed_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (content_id) REFERENCES content (id) ON DELETE CASCADE, + FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE + )`, + `CREATE TABLE IF NOT EXISTS favorites ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + content_id INTEGER NOT NULL, + playlist_id TEXT NOT NULL, + added_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (content_id) REFERENCES content(id) ON DELETE CASCADE, + FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE + )`, + `CREATE INDEX IF NOT EXISTS idx_content_type ON content(type)`, + `CREATE INDEX IF NOT EXISTS idx_content_category ON content(category_id)`, + `CREATE INDEX IF NOT EXISTS idx_categories_playlist ON categories(playlist_id)`, + `CREATE INDEX IF NOT EXISTS idx_content_title ON content(title)`, + `CREATE INDEX IF NOT EXISTS idx_content_xtream ON content(xtream_id)`, + `CREATE INDEX IF NOT EXISTS idx_categories_type ON categories(type)`, + `CREATE UNIQUE INDEX IF NOT EXISTS favorites_content_playlist_unique ON favorites(content_id, playlist_id)`, + `CREATE INDEX IF NOT EXISTS favorites_playlist_idx ON favorites(playlist_id)`, + `CREATE INDEX IF NOT EXISTS favorites_content_idx ON favorites(content_id)`, + `CREATE UNIQUE INDEX IF NOT EXISTS recently_viewed_content_playlist_unique ON recently_viewed(content_id, playlist_id)`, + `CREATE INDEX IF NOT EXISTS recently_viewed_playlist_idx ON recently_viewed(playlist_id)`, + `CREATE INDEX IF NOT EXISTS recently_viewed_viewed_at_idx ON recently_viewed(viewed_at)`, +]; + +/** + * Create tables if they don't exist + */ +function createTables(sqliteDb: Database.Database): void { + for (const stmt of CREATE_TABLE_STATEMENTS) { + sqliteDb.exec(stmt); + } +} + +export interface DatabaseOptions { + /** Open database in read-only mode (for agent-backend) */ + readonly?: boolean; + /** Skip table creation (useful for read-only connections) */ + skipTableCreation?: boolean; +} + +/** + * Initialize the database connection with full access (read-write) + * Creates tables if they don't exist + * Used by electron-backend + */ +export async function initDatabase( + options: DatabaseOptions = {} +): Promise { + const { readonly = false, skipTableCreation = false } = options; + + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const filePath = getDatabasePath(); + sqlite = new Database(filePath, { readonly }); + + // Enable foreign keys + sqlite.pragma('foreign_keys = ON'); + + // Create tables only for read-write connections + if (!readonly && !skipTableCreation) { + createTables(sqlite); + } + + const database = drizzle(sqlite, { schema }); + db = database; + return database; + })(); + + return initPromise; +} + +/** + * Get the database instance + * Initializes the database if not already initialized (with defaults for full access) + * Used by electron-backend + */ +export async function getDatabase( + options?: DatabaseOptions +): Promise { + if (db) return db; + return initDatabase(options); +} + +/** + * Get a read-only database connection + * Used by agent-backend for safe read-only queries + */ +export async function getReadOnlyDatabase(): Promise { + return getDatabase({ readonly: true, skipTableCreation: true }); +} + +/** + * Close the database connection + */ +export function closeDatabase(): void { + if (sqlite) { + sqlite.close(); + sqlite = null; + db = null; + initPromise = null; + } +} diff --git a/libs/shared/database/src/lib/schema.ts b/libs/shared/database/src/lib/schema.ts new file mode 100644 index 000000000..968c96893 --- /dev/null +++ b/libs/shared/database/src/lib/schema.ts @@ -0,0 +1,140 @@ +/** + * Drizzle ORM schema for IPTVnator database + * This schema defines the structure for Xtream Codes API data storage + * + * Single source of truth for database schema - used by: + * - electron-backend (full read-write access) + * - agent-backend (read-only access for AI queries) + */ + +import { sql } from 'drizzle-orm'; +import { + index, + integer, + sqliteTable, + text, + uniqueIndex, +} from 'drizzle-orm/sqlite-core'; + +// Playlists table +export const playlists = sqliteTable('playlists', { + id: text('id').primaryKey(), + name: text('name').notNull(), + serverUrl: text('serverUrl'), + username: text('username'), + password: text('password'), + dateCreated: text('date_created').default(sql`CURRENT_TIMESTAMP`), + lastUpdated: text('last_updated'), + type: text('type', { + enum: ['xtream', 'stalker', 'm3u-file', 'm3u-text', 'm3u-url'], + }).notNull(), + userAgent: text('userAgent'), + origin: text('origin'), + referrer: text('referrer'), + filePath: text('filePath'), + autoRefresh: integer('autoRefresh', { mode: 'boolean' }).default(false), + macAddress: text('macAddress'), + url: text('url'), + lastUsage: text('last_usage'), +}); + +// Categories table +export const categories = sqliteTable( + 'categories', + { + id: integer('id').primaryKey({ autoIncrement: true }), + playlistId: text('playlist_id') + .notNull() + .references(() => playlists.id, { onDelete: 'cascade' }), + name: text('name').notNull(), + type: text('type', { enum: ['live', 'movies', 'series'] }).notNull(), + xtreamId: integer('xtream_id').notNull(), + }, + (table) => ({ + playlistIdx: index('idx_categories_playlist').on(table.playlistId), + typeIdx: index('idx_categories_type').on(table.type), + }) +); + +// Content table (streams/VODs/series) +export const content = sqliteTable( + 'content', + { + id: integer('id').primaryKey({ autoIncrement: true }), + categoryId: integer('category_id') + .notNull() + .references(() => categories.id, { onDelete: 'cascade' }), + title: text('title').notNull(), + rating: text('rating'), + added: text('added'), + posterUrl: text('poster_url'), + xtreamId: integer('xtream_id').notNull(), + type: text('type', { enum: ['live', 'movie', 'series'] }).notNull(), + }, + (table) => ({ + typeIdx: index('idx_content_type').on(table.type), + categoryIdx: index('idx_content_category').on(table.categoryId), + titleIdx: index('idx_content_title').on(table.title), + xtreamIdx: index('idx_content_xtream').on(table.xtreamId), + }) +); + +// Recently viewed table +export const recentlyViewed = sqliteTable( + 'recently_viewed', + { + id: integer('id').primaryKey({ autoIncrement: true }), + contentId: integer('content_id') + .notNull() + .references(() => content.id, { onDelete: 'cascade' }), + playlistId: text('playlist_id') + .notNull() + .references(() => playlists.id, { onDelete: 'cascade' }), + viewedAt: text('viewed_at').default(sql`CURRENT_TIMESTAMP`), + }, + (table) => ({ + contentPlaylistUnique: uniqueIndex( + 'recently_viewed_content_playlist_unique' + ).on(table.contentId, table.playlistId), + playlistIdx: index('recently_viewed_playlist_idx').on(table.playlistId), + viewedAtIdx: index('recently_viewed_viewed_at_idx').on(table.viewedAt), + }) +); + +// Favorites table +export const favorites = sqliteTable( + 'favorites', + { + id: integer('id').primaryKey({ autoIncrement: true }), + contentId: integer('content_id') + .notNull() + .references(() => content.id, { onDelete: 'cascade' }), + playlistId: text('playlist_id') + .notNull() + .references(() => playlists.id, { onDelete: 'cascade' }), + addedAt: text('added_at').default(sql`CURRENT_TIMESTAMP`), + }, + (table) => ({ + contentPlaylistUnique: uniqueIndex( + 'favorites_content_playlist_unique' + ).on(table.contentId, table.playlistId), + playlistIdx: index('favorites_playlist_idx').on(table.playlistId), + contentIdx: index('favorites_content_idx').on(table.contentId), + }) +); + +// Type exports for TypeScript +export type Playlist = typeof playlists.$inferSelect; +export type NewPlaylist = typeof playlists.$inferInsert; + +export type Category = typeof categories.$inferSelect; +export type NewCategory = typeof categories.$inferInsert; + +export type Content = typeof content.$inferSelect; +export type NewContent = typeof content.$inferInsert; + +export type RecentlyViewed = typeof recentlyViewed.$inferSelect; +export type NewRecentlyViewed = typeof recentlyViewed.$inferInsert; + +export type Favorite = typeof favorites.$inferSelect; +export type NewFavorite = typeof favorites.$inferInsert; diff --git a/libs/shared/database/tsconfig.json b/libs/shared/database/tsconfig.json new file mode 100644 index 000000000..106e941d7 --- /dev/null +++ b/libs/shared/database/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true, + "esModuleInterop": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/shared/database/tsconfig.lib.json b/libs/shared/database/tsconfig.lib.json new file mode 100644 index 000000000..2b245b099 --- /dev/null +++ b/libs/shared/database/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 2219622e4..a918f48fa 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -23,7 +23,8 @@ "services": ["libs/services/src/index.ts"], "shared-interfaces": ["libs/shared/interfaces/src/index.ts"], "shared-portals": ["libs/ui/shared-portals/src/index.ts"], - "remote-control": ["libs/ui/remote-control/src/index.ts"] + "remote-control": ["libs/ui/remote-control/src/index.ts"], + "database": ["libs/shared/database/src/index.ts"] } }, "exclude": ["node_modules", "tmp"], From 223ce7a0e9ff3c22312f2be68421c89adb123d77 Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 26 Dec 2025 00:27:52 +0100 Subject: [PATCH 2/2] chore(config): normalize coderabbit config and add labeling Normalize .coderabbit.yaml structure and add automatic issue labeling. Move auto_review under the top-level reviews key to match expected nesting and restore YAML schema comment. Introduce issue_enrichment.labeling with auto_apply_labels and detailed labeling_instructions for bug, enhancement, and documentation so issues are categorized automatically and consistently. Fix minor formatting and spacing to improve readability and tooling support. --- .coderabbit.yaml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 4f48028f9..33ded29fa 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,9 +1,22 @@ -# Minimal configuration for getting started +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json + language: 'en-US' reviews: profile: 'chill' high_level_summary: true -auto_review: - enabled: true - drafts: false + auto_review: + enabled: true + drafts: false + +issue_enrichment: + labeling: + auto_apply_labels: true + labeling_instructions: + - label: bug + instructions: Issues reporting bugs, errors, crashes, incorrect behavior, or unexpected results. This includes runtime errors, logic errors, broken functionality, regressions, and any deviation from expected or documented behavior. + - label: enhancement + instructions: Feature requests, improvements to existing functionality, performance optimizations, refactoring suggestions, UI/UX enhancements, and any suggestions to make the project better or add new capabilities. + - label: documentation + instructions: Documentation updates, additions, corrections, or clarifications needed. This includes missing docs, outdated information, unclear instructions, API documentation, code examples, README improvements, and any requests for better explanations or guides. + ​