mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-29 05:50:51 +01:00
feat(pcd): enhance type generator with semantic types - check constraints, booleans
This commit is contained in:
@@ -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};`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user