diff --git a/deno.json b/deno.json index 98df548..33b35ec 100644 --- a/deno.json +++ b/deno.json @@ -46,6 +46,7 @@ "test": "deno run -A scripts/test.ts", "test:watch": "APP_BASE_PATH=./dist/test deno test src/tests --allow-read --allow-write --allow-env --watch", "generate:api-types": "npx openapi-typescript docs/api/v1/openapi.yaml -o src/lib/api/v1.d.ts", + "generate:pcd-types": "deno run -A scripts/generate-pcd-types.ts", "docker:build": "docker compose -f compose.dev.yml build --no-cache", "docker:up": "docker compose -f compose.dev.yml up --build", "docker:down": "docker compose -f compose.dev.yml down", diff --git a/scripts/generate-pcd-types.ts b/scripts/generate-pcd-types.ts new file mode 100644 index 0000000..8b26309 --- /dev/null +++ b/scripts/generate-pcd-types.ts @@ -0,0 +1,542 @@ +/** + * PCD Type Generator + * + * Generates TypeScript types from the PCD schema SQL. + * Uses SQLite introspection to ensure types match the actual schema. + * + * Usage: + * deno task generate:pcd-types # Uses default version (1.0.0) + * deno task generate:pcd-types --version=1.1.0 # Uses specific version + * deno task generate:pcd-types --local=/path/to/schema.sql # Uses local file + */ + +import { Database } from '@jsr/db__sqlite'; + +// ============================================================================ +// CONFIGURATION +// ============================================================================ + +const SCHEMA_REPO = 'Dictionarry-Hub/schema'; +const DEFAULT_VERSION = '1.0.0'; // Schema versions are branch names (e.g., 1.0.0, 1.1.0) +const SCHEMA_PATH = 'ops/0.schema.sql'; +const OUTPUT_DIR = './src/lib/shared/pcd'; +const OUTPUT_PATH = `${OUTPUT_DIR}/types.ts`; + +// ============================================================================ +// CLI ARGUMENT PARSING +// ============================================================================ + +interface CliArgs { + version: string; + localPath?: string; + help: boolean; +} + +function parseArgs(): CliArgs { + const args: CliArgs = { + version: DEFAULT_VERSION, + help: false + }; + + for (const arg of Deno.args) { + if (arg === '--help' || arg === '-h') { + args.help = true; + } else if (arg.startsWith('--version=')) { + args.version = arg.slice('--version='.length); + } else if (arg.startsWith('--local=')) { + args.localPath = arg.slice('--local='.length); + } + } + + return args; +} + +function printHelp(): void { + console.log(` +PCD Type Generator + +Generates TypeScript types from the PCD schema SQL. + +USAGE: + deno task generate:pcd-types [OPTIONS] + +OPTIONS: + --version= Use specific schema version/branch (default: ${DEFAULT_VERSION}) + --local= Use local schema file instead of fetching from GitHub + --help, -h Show this help message + +EXAMPLES: + deno task generate:pcd-types # Fetch version ${DEFAULT_VERSION} + deno task generate:pcd-types --version=1.1.0 # Fetch version 1.1.0 + deno task generate:pcd-types --local=./schema.sql # Use local file + +OUTPUT: + ${OUTPUT_PATH} +`); +} + +// ============================================================================ +// SCHEMA FETCHING +// ============================================================================ + +async function fetchSchemaFromGitHub(version: string): Promise { + const url = `https://raw.githubusercontent.com/${SCHEMA_REPO}/${version}/${SCHEMA_PATH}`; + console.log(`Fetching schema from: ${url}`); + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch schema: ${response.status} ${response.statusText}`); + } + + return await response.text(); +} + +async function loadSchemaFromFile(path: string): Promise { + console.log(`Loading schema from: ${path}`); + return await Deno.readTextFile(path); +} + +// ============================================================================ +// SQLITE INTROSPECTION +// ============================================================================ + +interface ColumnInfo { + cid: number; + name: string; + type: string; + notnull: number; + dflt_value: string | null; + pk: number; +} + +interface ForeignKeyInfo { + id: number; + seq: number; + table: string; + from: string; + to: string; + on_update: string; + on_delete: string; +} + +interface TableInfo { + name: string; + columns: ColumnInfo[]; + foreignKeys: ForeignKeyInfo[]; +} + +function introspectDatabase(db: Database): TableInfo[] { + // Get all table names + const tables = db + .prepare( + `SELECT name FROM sqlite_master + WHERE type='table' AND name NOT LIKE 'sqlite_%' + ORDER BY name` + ) + .all() as { name: string }[]; + + const tableInfos: TableInfo[] = []; + + for (const { name } of tables) { + // Get column info + const columns = db.prepare(`PRAGMA table_info('${name}')`).all() as ColumnInfo[]; + + // Get foreign key info + const foreignKeys = db.prepare(`PRAGMA foreign_key_list('${name}')`).all() as ForeignKeyInfo[]; + + tableInfos.push({ name, columns, foreignKeys }); + } + + return tableInfos; +} + +// ============================================================================ +// TYPE GENERATION +// ============================================================================ + +/** + * Map SQLite types to TypeScript types + */ +function sqliteTypeToTs(sqliteType: string, nullable: boolean): string { + const type = sqliteType.toUpperCase(); + + let tsType: string; + + if (type.includes('INT')) { + tsType = 'number'; + } else if (type.includes('CHAR') || type.includes('TEXT') || type.includes('CLOB')) { + tsType = 'string'; + } else if (type.includes('REAL') || type.includes('FLOA') || type.includes('DOUB')) { + tsType = 'number'; + } else if (type.includes('BLOB')) { + tsType = 'Uint8Array'; + } else if (type === '' || type === 'NUMERIC') { + // SQLite allows untyped columns + tsType = 'unknown'; + } else { + // Default to string for VARCHAR, etc. + tsType = 'string'; + } + + return nullable ? `${tsType} | null` : tsType; +} + +/** + * Convert snake_case to PascalCase + */ +function toPascalCase(str: string): string { + return str + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(''); +} + +/** + * Check if a column has a default value or is auto-generated + */ +function isGenerated(column: ColumnInfo): boolean { + // Primary key with INTEGER (autoincrement in SQLite) + if (column.pk === 1 && column.type.toUpperCase().includes('INTEGER')) { + return true; + } + + // Has a default value + if (column.dflt_value !== null) { + return true; + } + + return false; +} + +/** + * Check if a column is actually nullable + * Primary key INTEGER columns are never nullable in practice + */ +function isNullable(column: ColumnInfo): boolean { + // Primary key INTEGER columns are autoincrement and never null + if (column.pk === 1 && column.type.toUpperCase().includes('INTEGER')) { + return false; + } + + return column.notnull === 0; +} + +/** + * Generate TypeScript interface for a table + */ +function generateTableInterface(table: TableInfo): string { + const interfaceName = `${toPascalCase(table.name)}Table`; + const lines: string[] = []; + + lines.push(`export interface ${interfaceName} {`); + + for (const column of table.columns) { + const nullable = isNullable(column); + const tsType = sqliteTypeToTs(column.type, nullable); + const generated = isGenerated(column); + + if (generated) { + // Wrap in Generated for auto-generated columns + const baseType = nullable ? tsType.replace(' | null', '') : tsType; + lines.push(`\t${column.name}: Generated<${baseType}>${nullable ? ' | null' : ''};`); + } else { + lines.push(`\t${column.name}: ${tsType};`); + } + } + + lines.push('}'); + + return lines.join('\n'); +} + +/** + * Generate the database interface that maps table names to interfaces + */ +function generateDatabaseInterface(tables: TableInfo[]): string { + const lines: string[] = []; + + lines.push('export interface PCDDatabase {'); + + for (const table of tables) { + const interfaceName = `${toPascalCase(table.name)}Table`; + lines.push(`\t${table.name}: ${interfaceName};`); + } + + lines.push('}'); + + return lines.join('\n'); +} + +/** + * Generate row types (non-Generated versions for query results) + */ +function generateRowType(table: TableInfo): string { + const rowTypeName = `${toPascalCase(table.name)}Row`; + const lines: string[] = []; + + lines.push(`export interface ${rowTypeName} {`); + + for (const column of table.columns) { + const nullable = isNullable(column); + const tsType = sqliteTypeToTs(column.type, nullable); + lines.push(`\t${column.name}: ${tsType};`); + } + + lines.push('}'); + + return lines.join('\n'); +} + +/** + * Generate the complete types file + */ +function generateTypesFile(tables: TableInfo[], version: string): string { + const lines: string[] = []; + + // Header + lines.push(`/** + * PCD Database Schema Types + * + * AUTO-GENERATED - DO NOT EDIT MANUALLY + * + * Generated from: https://github.com/${SCHEMA_REPO}/blob/${version}/${SCHEMA_PATH} + * Generated at: ${new Date().toISOString()} + * + * To regenerate: deno task generate:pcd-types --version=${version} + */ + +import type { Generated } from 'kysely'; +`); + + // Group tables by entity - ordered by importance/usage + const qualityProfileTables = [ + 'quality_profiles', + 'quality_profile_tags', + 'quality_groups', + 'quality_group_members', + 'quality_profile_qualities', + 'quality_profile_languages', + 'quality_profile_custom_formats', + 'test_entities', + 'test_releases' + ]; + + const customFormatTables = [ + 'custom_formats', + 'custom_format_tags', + 'custom_format_conditions', + 'custom_format_tests' + // condition_* tables added dynamically below + ]; + + const regexTables = ['regular_expressions', 'regular_expression_tags']; + + const delayProfileTables = ['delay_profiles', 'delay_profile_tags']; + + const mediaManagementTables = [ + 'radarr_naming', + 'sonarr_naming', + 'radarr_media_settings', + 'sonarr_media_settings', + 'radarr_quality_definitions', + 'sonarr_quality_definitions' + ]; + + const coreTables = ['tags', 'languages', 'qualities', 'quality_api_mappings']; + + // Categories in display order + const categories = [ + 'QUALITY PROFILES', + 'CUSTOM FORMATS', + 'REGULAR EXPRESSIONS', + 'DELAY PROFILES', + 'MEDIA MANAGEMENT', + 'CORE' + ] as const; + + const categorized = new Map(); + for (const cat of categories) { + categorized.set(cat, []); + } + + for (const table of tables) { + if (qualityProfileTables.includes(table.name)) { + categorized.get('QUALITY PROFILES')!.push(table); + } else if (customFormatTables.includes(table.name) || table.name.startsWith('condition_')) { + categorized.get('CUSTOM FORMATS')!.push(table); + } else if (regexTables.includes(table.name)) { + categorized.get('REGULAR EXPRESSIONS')!.push(table); + } else if (delayProfileTables.includes(table.name)) { + categorized.get('DELAY PROFILES')!.push(table); + } else if (mediaManagementTables.includes(table.name)) { + categorized.get('MEDIA MANAGEMENT')!.push(table); + } else if (coreTables.includes(table.name)) { + categorized.get('CORE')!.push(table); + } else { + // Unknown tables go to CORE as fallback + console.warn(`Unknown table: ${table.name} - adding to CORE`); + categorized.get('CORE')!.push(table); + } + } + + // Sort tables within each category by the predefined order + const sortByOrder = (tables: TableInfo[], order: string[]): TableInfo[] => { + return tables.sort((a, b) => { + const aIndex = order.indexOf(a.name); + const bIndex = order.indexOf(b.name); + // Tables in the order list come first, sorted by their position + // Tables not in the list (like condition_*) come after, sorted alphabetically + if (aIndex === -1 && bIndex === -1) return a.name.localeCompare(b.name); + if (aIndex === -1) return 1; + if (bIndex === -1) return -1; + return aIndex - bIndex; + }); + }; + + categorized.set('QUALITY PROFILES', sortByOrder(categorized.get('QUALITY PROFILES')!, qualityProfileTables)); + categorized.set('CUSTOM FORMATS', sortByOrder(categorized.get('CUSTOM FORMATS')!, customFormatTables)); + categorized.set('REGULAR EXPRESSIONS', sortByOrder(categorized.get('REGULAR EXPRESSIONS')!, regexTables)); + categorized.set('DELAY PROFILES', sortByOrder(categorized.get('DELAY PROFILES')!, delayProfileTables)); + categorized.set('MEDIA MANAGEMENT', sortByOrder(categorized.get('MEDIA MANAGEMENT')!, mediaManagementTables)); + categorized.set('CORE', sortByOrder(categorized.get('CORE')!, coreTables)); + + // Generate Kysely table interfaces + lines.push('// ============================================================================'); + lines.push('// KYSELY TABLE INTERFACES'); + lines.push('// ============================================================================'); + lines.push('// Use these with Kysely for type-safe queries with Generated support'); + lines.push(''); + + for (const [category, categoryTables] of categorized) { + if (categoryTables.length === 0) continue; + + lines.push(`// ${category}`); + lines.push(''); + + for (const table of categoryTables) { + lines.push(generateTableInterface(table)); + lines.push(''); + } + } + + // Generate database interface + lines.push('// ============================================================================'); + lines.push('// DATABASE INTERFACE'); + lines.push('// ============================================================================'); + lines.push(''); + lines.push(generateDatabaseInterface(tables)); + lines.push(''); + + // Generate row types + lines.push('// ============================================================================'); + lines.push('// ROW TYPES (Query Results)'); + lines.push('// ============================================================================'); + lines.push('// Use these for query result types (no Generated wrapper)'); + lines.push(''); + + for (const [category, categoryTables] of categorized) { + if (categoryTables.length === 0) continue; + + lines.push(`// ${category}`); + lines.push(''); + + for (const table of categoryTables) { + lines.push(generateRowType(table)); + lines.push(''); + } + } + + // Generate helper types + lines.push('// ============================================================================'); + lines.push('// HELPER TYPES'); + lines.push('// ============================================================================'); + lines.push(''); + lines.push('/** Extract insertable type from a table (Generated fields become optional) */'); + lines.push('export type Insertable = {'); + lines.push('\t[K in keyof T]: T[K] extends Generated'); + lines.push('\t\t? U | undefined'); + lines.push('\t\t: T[K] extends Generated | null'); + lines.push('\t\t\t? U | null | undefined'); + lines.push('\t\t\t: T[K];'); + lines.push('};'); + lines.push(''); + lines.push('/** Extract selectable type from a table (Generated becomes T) */'); + lines.push('export type Selectable = {'); + lines.push('\t[K in keyof T]: T[K] extends Generated'); + lines.push('\t\t? U'); + lines.push('\t\t: T[K] extends Generated | null'); + lines.push('\t\t\t? U | null'); + lines.push('\t\t\t: T[K];'); + lines.push('};'); + lines.push(''); + + return lines.join('\n'); +} + +// ============================================================================ +// MAIN +// ============================================================================ + +async function main(): Promise { + const args = parseArgs(); + + if (args.help) { + printHelp(); + Deno.exit(0); + } + + try { + // Load schema + let schemaSql: string; + let sourceVersion = args.version; + + if (args.localPath) { + schemaSql = await loadSchemaFromFile(args.localPath); + sourceVersion = 'local'; + } else { + schemaSql = await fetchSchemaFromGitHub(args.version); + } + + console.log(`Schema loaded (${schemaSql.length} bytes)`); + + // Create in-memory database and run schema + console.log('Creating database and applying schema...'); + const db = new Database(':memory:'); + + try { + db.exec(schemaSql); + } catch (error) { + console.error('Failed to execute schema SQL:', error); + Deno.exit(1); + } + + // Introspect database + console.log('Introspecting database structure...'); + const tables = introspectDatabase(db); + console.log(`Found ${tables.length} tables`); + + // Generate types + console.log('Generating TypeScript types...'); + const typesContent = generateTypesFile(tables, sourceVersion); + + // Ensure output directory exists + await Deno.mkdir(OUTPUT_DIR, { recursive: true }); + + // Write output + await Deno.writeTextFile(OUTPUT_PATH, typesContent); + console.log(`\nTypes written to: ${OUTPUT_PATH}`); + + // Summary + console.log('\nGenerated types for:'); + for (const table of tables) { + console.log(` - ${table.name} (${table.columns.length} columns)`); + } + + db.close(); + } catch (error) { + console.error('Error:', error); + Deno.exit(1); + } +} + +main(); diff --git a/src/lib/shared/pcd/types.ts b/src/lib/shared/pcd/types.ts new file mode 100644 index 0000000..347b6eb --- /dev/null +++ b/src/lib/shared/pcd/types.ts @@ -0,0 +1,688 @@ +/** + * PCD Database Schema Types + * + * AUTO-GENERATED - DO NOT EDIT MANUALLY + * + * Generated from: https://github.com/Dictionarry-Hub/schema/blob/1.0.0/ops/0.schema.sql + * Generated at: 2026-01-27T11:00:15.108Z + * + * To regenerate: deno task generate:pcd-types --version=1.0.0 + */ + +import type { Generated } from 'kysely'; + +// ============================================================================ +// KYSELY TABLE INTERFACES +// ============================================================================ +// Use these with Kysely for type-safe queries with Generated support + +// QUALITY PROFILES + +export interface QualityProfilesTable { + id: Generated; + name: string; + description: string | null; + upgrades_allowed: Generated; + minimum_custom_format_score: Generated; + upgrade_until_score: Generated; + upgrade_score_increment: Generated; + created_at: Generated; + updated_at: Generated; +} + +export interface QualityProfileTagsTable { + quality_profile_name: string; + tag_name: string; +} + +export interface QualityGroupsTable { + id: Generated; + quality_profile_name: string; + name: string; + created_at: Generated; + updated_at: Generated; +} + +export interface QualityGroupMembersTable { + quality_profile_name: string; + quality_group_name: string; + quality_name: string; +} + +export interface QualityProfileQualitiesTable { + id: Generated; + quality_profile_name: string; + quality_name: string | null; + quality_group_name: string | null; + position: number; + enabled: Generated; + upgrade_until: Generated; +} + +export interface QualityProfileLanguagesTable { + quality_profile_name: string; + language_name: string; + type: Generated; +} + +export interface QualityProfileCustomFormatsTable { + quality_profile_name: string; + custom_format_name: string; + arr_type: string; + score: number; +} + +export interface TestEntitiesTable { + id: Generated; + type: string; + tmdb_id: number; + title: string; + year: number | null; + poster_path: string | null; + created_at: Generated; + updated_at: Generated; +} + +export interface TestReleasesTable { + id: Generated; + entity_type: string; + entity_tmdb_id: number; + title: string; + size_bytes: number | null; + languages: Generated; + indexers: Generated; + flags: Generated; + created_at: Generated; + updated_at: Generated; +} + +// CUSTOM FORMATS + +export interface CustomFormatsTable { + id: Generated; + name: string; + description: string | null; + include_in_rename: Generated; + created_at: Generated; + updated_at: Generated; +} + +export interface CustomFormatTagsTable { + custom_format_name: string; + tag_name: string; +} + +export interface CustomFormatConditionsTable { + id: Generated; + custom_format_name: string; + name: string; + type: string; + arr_type: Generated; + negate: Generated; + required: Generated; + created_at: Generated; + updated_at: Generated; +} + +export interface CustomFormatTestsTable { + id: Generated; + custom_format_name: string; + title: string; + type: string; + should_match: number; + description: string | null; + created_at: Generated; +} + +export interface ConditionIndexerFlagsTable { + custom_format_name: string; + condition_name: string; + flag: string; +} + +export interface ConditionLanguagesTable { + custom_format_name: string; + condition_name: string; + language_name: string; + except_language: Generated; +} + +export interface ConditionPatternsTable { + custom_format_name: string; + condition_name: string; + regular_expression_name: string; +} + +export interface ConditionQualityModifiersTable { + custom_format_name: string; + condition_name: string; + quality_modifier: string; +} + +export interface ConditionReleaseTypesTable { + custom_format_name: string; + condition_name: string; + release_type: string; +} + +export interface ConditionResolutionsTable { + custom_format_name: string; + condition_name: string; + resolution: string; +} + +export interface ConditionSizesTable { + custom_format_name: string; + condition_name: string; + min_bytes: number | null; + max_bytes: number | null; +} + +export interface ConditionSourcesTable { + custom_format_name: string; + condition_name: string; + source: string; +} + +export interface ConditionYearsTable { + custom_format_name: string; + condition_name: string; + min_year: number | null; + max_year: number | null; +} + +// REGULAR EXPRESSIONS + +export interface RegularExpressionsTable { + id: Generated; + name: string; + pattern: string; + regex101_id: string | null; + description: string | null; + created_at: Generated; + updated_at: Generated; +} + +export interface RegularExpressionTagsTable { + regular_expression_name: string; + tag_name: string; +} + +// DELAY PROFILES + +export interface DelayProfilesTable { + id: Generated; + name: string; + preferred_protocol: string; + usenet_delay: number | null; + torrent_delay: number | null; + bypass_if_highest_quality: Generated; + bypass_if_above_custom_format_score: Generated; + minimum_custom_format_score: number | null; + created_at: Generated; + updated_at: Generated; +} + +// MEDIA MANAGEMENT + +export interface RadarrNamingTable { + name: string | null; + rename: Generated; + movie_format: string; + movie_folder_format: string; + replace_illegal_characters: Generated; + colon_replacement_format: Generated; + created_at: Generated; + updated_at: Generated; +} + +export interface SonarrNamingTable { + name: string | null; + rename: Generated; + standard_episode_format: string; + daily_episode_format: string; + anime_episode_format: string; + series_folder_format: string; + season_folder_format: string; + replace_illegal_characters: Generated; + colon_replacement_format: Generated; + custom_colon_replacement_format: string | null; + multi_episode_style: Generated; + created_at: Generated; + updated_at: Generated; +} + +export interface RadarrMediaSettingsTable { + name: string | null; + propers_repacks: Generated; + enable_media_info: Generated; + created_at: Generated; + updated_at: Generated; +} + +export interface SonarrMediaSettingsTable { + name: string | null; + propers_repacks: Generated; + enable_media_info: Generated; + created_at: Generated; + updated_at: Generated; +} + +export interface RadarrQualityDefinitionsTable { + name: string; + quality_name: string; + min_size: Generated; + max_size: number; + preferred_size: number; + created_at: Generated; + updated_at: Generated; +} + +export interface SonarrQualityDefinitionsTable { + name: string; + quality_name: string; + min_size: Generated; + max_size: number; + preferred_size: number; + created_at: Generated; + updated_at: Generated; +} + +// CORE + +export interface TagsTable { + id: Generated; + name: string; + created_at: Generated; +} + +export interface LanguagesTable { + id: Generated; + name: string; + created_at: Generated; + updated_at: Generated; +} + +export interface QualitiesTable { + id: Generated; + name: string; + created_at: Generated; + updated_at: Generated; +} + +export interface QualityApiMappingsTable { + quality_name: string; + arr_type: string; + api_name: string; + created_at: Generated; +} + +// ============================================================================ +// DATABASE INTERFACE +// ============================================================================ + +export interface PCDDatabase { + condition_indexer_flags: ConditionIndexerFlagsTable; + condition_languages: ConditionLanguagesTable; + condition_patterns: ConditionPatternsTable; + condition_quality_modifiers: ConditionQualityModifiersTable; + condition_release_types: ConditionReleaseTypesTable; + condition_resolutions: ConditionResolutionsTable; + condition_sizes: ConditionSizesTable; + condition_sources: ConditionSourcesTable; + condition_years: ConditionYearsTable; + custom_format_conditions: CustomFormatConditionsTable; + custom_format_tags: CustomFormatTagsTable; + custom_format_tests: CustomFormatTestsTable; + custom_formats: CustomFormatsTable; + delay_profiles: DelayProfilesTable; + languages: LanguagesTable; + qualities: QualitiesTable; + quality_api_mappings: QualityApiMappingsTable; + quality_group_members: QualityGroupMembersTable; + quality_groups: QualityGroupsTable; + quality_profile_custom_formats: QualityProfileCustomFormatsTable; + quality_profile_languages: QualityProfileLanguagesTable; + quality_profile_qualities: QualityProfileQualitiesTable; + quality_profile_tags: QualityProfileTagsTable; + quality_profiles: QualityProfilesTable; + radarr_media_settings: RadarrMediaSettingsTable; + radarr_naming: RadarrNamingTable; + radarr_quality_definitions: RadarrQualityDefinitionsTable; + regular_expression_tags: RegularExpressionTagsTable; + regular_expressions: RegularExpressionsTable; + sonarr_media_settings: SonarrMediaSettingsTable; + sonarr_naming: SonarrNamingTable; + sonarr_quality_definitions: SonarrQualityDefinitionsTable; + tags: TagsTable; + test_entities: TestEntitiesTable; + test_releases: TestReleasesTable; +} + +// ============================================================================ +// ROW TYPES (Query Results) +// ============================================================================ +// Use these for query result types (no Generated wrapper) + +// QUALITY PROFILES + +export interface QualityProfilesRow { + id: number; + name: string; + description: string | null; + upgrades_allowed: number; + minimum_custom_format_score: number; + upgrade_until_score: number; + upgrade_score_increment: number; + created_at: string; + updated_at: string; +} + +export interface QualityProfileTagsRow { + quality_profile_name: string; + tag_name: string; +} + +export interface QualityGroupsRow { + id: number; + quality_profile_name: string; + name: string; + created_at: string; + updated_at: string; +} + +export interface QualityGroupMembersRow { + quality_profile_name: string; + quality_group_name: string; + quality_name: string; +} + +export interface QualityProfileQualitiesRow { + id: number; + quality_profile_name: string; + quality_name: string | null; + quality_group_name: string | null; + position: number; + enabled: number; + upgrade_until: number; +} + +export interface QualityProfileLanguagesRow { + quality_profile_name: string; + language_name: string; + type: string; +} + +export interface QualityProfileCustomFormatsRow { + quality_profile_name: string; + custom_format_name: string; + arr_type: string; + score: number; +} + +export interface TestEntitiesRow { + id: number; + type: string; + tmdb_id: number; + title: string; + year: number | null; + poster_path: string | null; + created_at: string; + updated_at: string; +} + +export interface TestReleasesRow { + id: number; + entity_type: string; + entity_tmdb_id: number; + title: string; + size_bytes: number | null; + languages: string; + indexers: string; + flags: string; + created_at: string; + updated_at: string; +} + +// CUSTOM FORMATS + +export interface CustomFormatsRow { + id: number; + name: string; + description: string | null; + include_in_rename: number; + created_at: string; + updated_at: string; +} + +export interface CustomFormatTagsRow { + custom_format_name: string; + tag_name: string; +} + +export interface CustomFormatConditionsRow { + id: number; + custom_format_name: string; + name: string; + type: string; + arr_type: string; + negate: number; + required: number; + created_at: string; + updated_at: string; +} + +export interface CustomFormatTestsRow { + id: number; + custom_format_name: string; + title: string; + type: string; + should_match: number; + description: string | null; + created_at: string; +} + +export interface ConditionIndexerFlagsRow { + custom_format_name: string; + condition_name: string; + flag: string; +} + +export interface ConditionLanguagesRow { + custom_format_name: string; + condition_name: string; + language_name: string; + except_language: number; +} + +export interface ConditionPatternsRow { + custom_format_name: string; + condition_name: string; + regular_expression_name: string; +} + +export interface ConditionQualityModifiersRow { + custom_format_name: string; + condition_name: string; + quality_modifier: string; +} + +export interface ConditionReleaseTypesRow { + custom_format_name: string; + condition_name: string; + release_type: string; +} + +export interface ConditionResolutionsRow { + custom_format_name: string; + condition_name: string; + resolution: string; +} + +export interface ConditionSizesRow { + custom_format_name: string; + condition_name: string; + min_bytes: number | null; + max_bytes: number | null; +} + +export interface ConditionSourcesRow { + custom_format_name: string; + condition_name: string; + source: string; +} + +export interface ConditionYearsRow { + custom_format_name: string; + condition_name: string; + min_year: number | null; + max_year: number | null; +} + +// REGULAR EXPRESSIONS + +export interface RegularExpressionsRow { + id: number; + name: string; + pattern: string; + regex101_id: string | null; + description: string | null; + created_at: string; + updated_at: string; +} + +export interface RegularExpressionTagsRow { + regular_expression_name: string; + tag_name: string; +} + +// DELAY PROFILES + +export interface DelayProfilesRow { + id: number; + name: string; + preferred_protocol: string; + usenet_delay: number | null; + torrent_delay: number | null; + bypass_if_highest_quality: number; + bypass_if_above_custom_format_score: number; + minimum_custom_format_score: number | null; + created_at: string; + updated_at: string; +} + +// MEDIA MANAGEMENT + +export interface RadarrNamingRow { + name: string | null; + rename: number; + movie_format: string; + movie_folder_format: string; + replace_illegal_characters: number; + colon_replacement_format: string; + created_at: string; + updated_at: string; +} + +export interface SonarrNamingRow { + name: string | null; + rename: number; + standard_episode_format: string; + daily_episode_format: string; + anime_episode_format: string; + series_folder_format: string; + season_folder_format: string; + replace_illegal_characters: number; + colon_replacement_format: number; + custom_colon_replacement_format: string | null; + multi_episode_style: number; + created_at: string; + updated_at: string; +} + +export interface RadarrMediaSettingsRow { + name: string | null; + propers_repacks: string; + enable_media_info: number; + created_at: string; + updated_at: string; +} + +export interface SonarrMediaSettingsRow { + name: string | null; + propers_repacks: string; + enable_media_info: number; + created_at: string; + updated_at: string; +} + +export interface RadarrQualityDefinitionsRow { + name: string; + quality_name: string; + min_size: number; + max_size: number; + preferred_size: number; + created_at: string; + updated_at: string; +} + +export interface SonarrQualityDefinitionsRow { + name: string; + quality_name: string; + min_size: number; + max_size: number; + preferred_size: number; + created_at: string; + updated_at: string; +} + +// CORE + +export interface TagsRow { + id: number; + name: string; + created_at: string; +} + +export interface LanguagesRow { + id: number; + name: string; + created_at: string; + updated_at: string; +} + +export interface QualitiesRow { + id: number; + name: string; + created_at: string; + updated_at: string; +} + +export interface QualityApiMappingsRow { + quality_name: string; + arr_type: string; + api_name: string; + created_at: string; +} + +// ============================================================================ +// HELPER TYPES +// ============================================================================ + +/** Extract insertable type from a table (Generated fields become optional) */ +export type Insertable = { + [K in keyof T]: T[K] extends Generated + ? U | undefined + : T[K] extends Generated | null + ? U | null | undefined + : T[K]; +}; + +/** Extract selectable type from a table (Generated becomes T) */ +export type Selectable = { + [K in keyof T]: T[K] extends Generated + ? U + : T[K] extends Generated | null + ? U | null + : T[K]; +};