diff --git a/scripts/generate-pcd-types.ts b/scripts/generate-pcd-types.ts index 31ae89b..00eaa11 100644 --- a/scripts/generate-pcd-types.ts +++ b/scripts/generate-pcd-types.ts @@ -119,32 +119,106 @@ interface ForeignKeyInfo { on_delete: string; } +interface CheckConstraint { + column: string; + values: string[]; +} + interface TableInfo { name: string; columns: ColumnInfo[]; foreignKeys: ForeignKeyInfo[]; + checkConstraints: CheckConstraint[]; + createSql: string; +} + +/** + * Parse CHECK constraints from CREATE TABLE SQL to extract enum values + * Looks for patterns like: CHECK (column IN ('val1', 'val2', 'val3')) + */ +function parseCheckConstraints(createSql: string): CheckConstraint[] { + const constraints: CheckConstraint[] = []; + + // Match CHECK (column_name IN ('val1', 'val2', ...)) patterns + // Handles both quoted strings and unquoted identifiers + const checkPattern = /CHECK\s*\(\s*(\w+)\s+IN\s*\(\s*([^)]+)\s*\)\s*\)/gi; + + let match; + while ((match = checkPattern.exec(createSql)) !== null) { + const column = match[1]; + const valuesStr = match[2]; + + // Extract individual values (handles 'quoted' and unquoted) + const values: string[] = []; + const valuePattern = /'([^']+)'/g; + let valueMatch; + while ((valueMatch = valuePattern.exec(valuesStr)) !== null) { + values.push(valueMatch[1]); + } + + if (values.length > 0) { + constraints.push({ column, values }); + } + } + + return constraints; +} + +/** + * Check if a column name matches boolean naming patterns + */ +function isBooleanColumn(columnName: string, columnType: string): boolean { + // Must be INTEGER type + if (!columnType.toUpperCase().includes('INT')) { + return false; + } + + const name = columnName.toLowerCase(); + + // Prefix patterns + const booleanPrefixes = ['is_', 'has_', 'bypass_', 'enable_', 'include_', 'except_', 'replace_']; + if (booleanPrefixes.some((prefix) => name.startsWith(prefix))) { + return true; + } + + // Suffix patterns + const booleanSuffixes = ['_allowed', '_enabled']; + if (booleanSuffixes.some((suffix) => name.endsWith(suffix))) { + return true; + } + + // Exact matches + const booleanExactNames = ['negate', 'required', 'enabled', 'rename', 'should_match', 'upgrades_allowed']; + if (booleanExactNames.includes(name)) { + return true; + } + + return false; } function introspectDatabase(db: Database): TableInfo[] { - // Get all table names + // Get all table names and their CREATE statements const tables = db .prepare( - `SELECT name FROM sqlite_master + `SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name` ) - .all() as { name: string }[]; + .all() as { name: string; sql: string }[]; const tableInfos: TableInfo[] = []; - for (const { name } of tables) { + for (const { name, sql } 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 }); + // Parse CHECK constraints from CREATE TABLE SQL + const checkConstraints = parseCheckConstraints(sql || ''); + + tableInfos.push({ name, columns, foreignKeys, checkConstraints, createSql: sql || '' }); } return tableInfos; @@ -181,6 +255,31 @@ function sqliteTypeToTs(sqliteType: string, nullable: boolean): string { return nullable ? `${tsType} | null` : tsType; } +/** + * Get the semantic TypeScript type for a column + * Uses CHECK constraints for union types and naming patterns for booleans + */ +function getSemanticType( + column: ColumnInfo, + checkConstraints: CheckConstraint[], + nullable: boolean +): string { + // Check if this column has a CHECK IN constraint (union type) + const constraint = checkConstraints.find((c) => c.column === column.name); + if (constraint && constraint.values.length > 0) { + const unionType = constraint.values.map((v) => `'${v}'`).join(' | '); + return nullable ? `(${unionType}) | null` : unionType; + } + + // Check if this is a boolean column based on naming patterns + if (isBooleanColumn(column.name, column.type)) { + return nullable ? 'boolean | null' : 'boolean'; + } + + // Fall back to standard SQLite type mapping + return sqliteTypeToTs(column.type, nullable); +} + /** * Convert snake_case to PascalCase */ @@ -269,6 +368,7 @@ function generateDatabaseInterface(tables: TableInfo[]): string { /** * Generate row types (non-Generated versions for query results) + * Uses semantic types: booleans as boolean, CHECK IN as union types */ function generateRowType(table: TableInfo): string { const rowTypeName = `${toPascalCase(table.name)}Row`; @@ -278,7 +378,7 @@ function generateRowType(table: TableInfo): string { for (const column of table.columns) { const nullable = isNullable(column); - const tsType = sqliteTypeToTs(column.type, nullable); + const tsType = getSemanticType(column, table.checkConstraints, nullable); lines.push(`\t${column.name}: ${tsType};`); } diff --git a/src/lib/shared/pcd/types.ts b/src/lib/shared/pcd/types.ts index c8cd5be..7d7c5e0 100644 --- a/src/lib/shared/pcd/types.ts +++ b/src/lib/shared/pcd/types.ts @@ -3,10 +3,10 @@ * * 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:08:44.693Z + * Generated from: https://github.com/Dictionarry-Hub/schema/blob/local/ops/0.schema.sql + * Generated at: 2026-01-27T11:52:25.907Z * - * To regenerate: deno task generate:pcd-types --version=1.0.0 + * To regenerate: deno task generate:pcd-types --version=local */ import type { Generated } from 'kysely'; @@ -370,7 +370,7 @@ export interface QualityProfilesRow { id: number; name: string; description: string | null; - upgrades_allowed: number; + upgrades_allowed: boolean; minimum_custom_format_score: number; upgrade_until_score: number; upgrade_score_increment: number; @@ -403,7 +403,7 @@ export interface QualityProfileQualitiesRow { quality_name: string | null; quality_group_name: string | null; position: number; - enabled: number; + enabled: boolean; upgrade_until: number; } @@ -422,7 +422,7 @@ export interface QualityProfileCustomFormatsRow { export interface TestEntitiesRow { id: number; - type: string; + type: 'movie' | 'series'; tmdb_id: number; title: string; year: number | null; @@ -433,7 +433,7 @@ export interface TestEntitiesRow { export interface TestReleasesRow { id: number; - entity_type: string; + entity_type: 'movie' | 'series'; entity_tmdb_id: number; title: string; size_bytes: number | null; @@ -450,7 +450,7 @@ export interface CustomFormatsRow { id: number; name: string; description: string | null; - include_in_rename: number; + include_in_rename: boolean; created_at: string; updated_at: string; } @@ -466,8 +466,8 @@ export interface CustomFormatConditionsRow { name: string; type: string; arr_type: string; - negate: number; - required: number; + negate: boolean; + required: boolean; created_at: string; updated_at: string; } @@ -477,7 +477,7 @@ export interface CustomFormatTestsRow { custom_format_name: string; title: string; type: string; - should_match: number; + should_match: boolean; description: string | null; created_at: string; } @@ -492,7 +492,7 @@ export interface ConditionLanguagesRow { custom_format_name: string; condition_name: string; language_name: string; - except_language: number; + except_language: boolean; } export interface ConditionPatternsRow { @@ -561,11 +561,11 @@ export interface RegularExpressionTagsRow { export interface DelayProfilesRow { id: number; name: string; - preferred_protocol: string; + preferred_protocol: 'prefer_usenet' | 'prefer_torrent' | 'only_usenet' | 'only_torrent'; usenet_delay: number | null; torrent_delay: number | null; - bypass_if_highest_quality: number; - bypass_if_above_custom_format_score: number; + bypass_if_highest_quality: boolean; + bypass_if_above_custom_format_score: boolean; minimum_custom_format_score: number | null; created_at: string; updated_at: string; @@ -575,10 +575,10 @@ export interface DelayProfilesRow { export interface RadarrNamingRow { name: string | null; - rename: number; + rename: boolean; movie_format: string; movie_folder_format: string; - replace_illegal_characters: number; + replace_illegal_characters: boolean; colon_replacement_format: string; created_at: string; updated_at: string; @@ -586,13 +586,13 @@ export interface RadarrNamingRow { export interface SonarrNamingRow { name: string | null; - rename: number; + rename: boolean; standard_episode_format: string; daily_episode_format: string; anime_episode_format: string; series_folder_format: string; season_folder_format: string; - replace_illegal_characters: number; + replace_illegal_characters: boolean; colon_replacement_format: number; custom_colon_replacement_format: string | null; multi_episode_style: number; @@ -603,7 +603,7 @@ export interface SonarrNamingRow { export interface RadarrMediaSettingsRow { name: string | null; propers_repacks: string; - enable_media_info: number; + enable_media_info: boolean; created_at: string; updated_at: string; } @@ -611,7 +611,7 @@ export interface RadarrMediaSettingsRow { export interface SonarrMediaSettingsRow { name: string | null; propers_repacks: string; - enable_media_info: number; + enable_media_info: boolean; created_at: string; updated_at: string; }