feat(pcd): enhance generator with type overrides for semantic enums (media management)

This commit is contained in:
Sam Chau
2026-01-27 23:10:16 +10:30
parent 1c7e063b9b
commit 745b1531cb
3 changed files with 79 additions and 11 deletions

View File

@@ -11,6 +11,7 @@
*/
import { Database } from '@jsr/db__sqlite';
import { columnTypeOverrides } from './pcd-type-overrides.ts';
// ============================================================================
// CONFIGURATION
@@ -257,26 +258,34 @@ function sqliteTypeToTs(sqliteType: string, nullable: boolean): string {
/**
* Get the semantic TypeScript type for a column
* Uses CHECK constraints for union types and naming patterns for booleans
* Priority: 1) Manual overrides, 2) CHECK constraints, 3) Boolean patterns, 4) SQLite type
*/
function getSemanticType(
tableName: string,
column: ColumnInfo,
checkConstraints: CheckConstraint[],
nullable: boolean
): string {
// Check if this column has a CHECK IN constraint (union type)
// 1. Check for manual type override (for columns that store numbers but need string types)
const overrideKey = `${tableName}.${column.name}`;
const override = columnTypeOverrides[overrideKey];
if (override) {
return nullable ? `(${override}) | null` : override;
}
// 2. 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
// 3. 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
// 4. Fall back to standard SQLite type mapping
return sqliteTypeToTs(column.type, nullable);
}
@@ -378,7 +387,7 @@ function generateRowType(table: TableInfo): string {
for (const column of table.columns) {
const nullable = isNullable(column);
const tsType = getSemanticType(column, table.checkConstraints, nullable);
const tsType = getSemanticType(table.name, column, table.checkConstraints, nullable);
lines.push(`\t${column.name}: ${tsType};`);
}

View File

@@ -0,0 +1,59 @@
/**
* PCD Type Overrides
*
* Manual type overrides for columns that store numeric values in the DB
* but need semantic string types (because that's what the API expects).
*
* Format: 'table_name.column_name': 'TypeScript type'
*
* Note: Columns with CHECK constraints in the schema don't need overrides -
* the generator parses those automatically.
*/
export const columnTypeOverrides: Record<string, string> = {
// Sonarr stores these as integers (0-5) but API expects semantic strings
'sonarr_naming.colon_replacement_format':
"'delete' | 'dash' | 'spaceDash' | 'spaceDashSpace' | 'smart' | 'custom'",
'sonarr_naming.multi_episode_style':
"'extend' | 'duplicate' | 'repeat' | 'scene' | 'range' | 'prefixedRange'"
};
/**
* DB value mappings for converting between numeric DB values and semantic strings.
* Used by query read/write functions.
*/
export const colonReplacementFromDb: Record<number, string> = {
0: 'delete',
1: 'dash',
2: 'spaceDash',
3: 'spaceDashSpace',
4: 'smart',
5: 'custom'
};
export const colonReplacementToDb: Record<string, number> = {
delete: 0,
dash: 1,
spaceDash: 2,
spaceDashSpace: 3,
smart: 4,
custom: 5
};
export const multiEpisodeStyleFromDb: Record<number, string> = {
0: 'extend',
1: 'duplicate',
2: 'repeat',
3: 'scene',
4: 'range',
5: 'prefixedRange'
};
export const multiEpisodeStyleToDb: Record<string, number> = {
extend: 0,
duplicate: 1,
repeat: 2,
scene: 3,
range: 4,
prefixedRange: 5
};

View File

@@ -4,7 +4,7 @@
* AUTO-GENERATED - DO NOT EDIT MANUALLY
*
* Generated from: https://github.com/Dictionarry-Hub/schema/blob/local/ops/0.schema.sql
* Generated at: 2026-01-27T11:52:25.907Z
* Generated at: 2026-01-27T12:38:46.562Z
*
* To regenerate: deno task generate:pcd-types --version=local
*/
@@ -579,7 +579,7 @@ export interface RadarrNamingRow {
movie_format: string;
movie_folder_format: string;
replace_illegal_characters: boolean;
colon_replacement_format: string;
colon_replacement_format: 'delete' | 'dash' | 'spaceDash' | 'spaceDashSpace' | 'smart';
created_at: string;
updated_at: string;
}
@@ -593,16 +593,16 @@ export interface SonarrNamingRow {
series_folder_format: string;
season_folder_format: string;
replace_illegal_characters: boolean;
colon_replacement_format: number;
colon_replacement_format: 'delete' | 'dash' | 'spaceDash' | 'spaceDashSpace' | 'smart' | 'custom';
custom_colon_replacement_format: string | null;
multi_episode_style: number;
multi_episode_style: 'extend' | 'duplicate' | 'repeat' | 'scene' | 'range' | 'prefixedRange';
created_at: string;
updated_at: string;
}
export interface RadarrMediaSettingsRow {
name: string | null;
propers_repacks: string;
propers_repacks: 'doNotPrefer' | 'preferAndUpgrade' | 'doNotUpgradeAutomatically';
enable_media_info: boolean;
created_at: string;
updated_at: string;
@@ -610,7 +610,7 @@ export interface RadarrMediaSettingsRow {
export interface SonarrMediaSettingsRow {
name: string | null;
propers_repacks: string;
propers_repacks: 'doNotPrefer' | 'preferAndUpgrade' | 'doNotUpgradeAutomatically';
enable_media_info: boolean;
created_at: string;
updated_at: string;