feat: Add Kysely integration for quality profile queries and refactor database interactions

- Updated package.json to include Kysely and Deno Vite plugin dependencies.
- Introduced new types for sorting in table component.
- Refactored PCDCache to utilize Kysely for type-safe database queries.
- Created new query files for quality profiles, including general information, languages, and list queries.
- Removed outdated qualityProfiles.ts file and replaced it with modular query structure.
- Updated routes to use new query functions for loading quality profile data.
- Enhanced Vite configuration to include Deno plugin and improved watch settings.
This commit is contained in:
Sam Chau
2025-11-09 05:07:48 +11:00
parent d69064803a
commit e1de8f88cf
18 changed files with 703 additions and 541 deletions

View File

@@ -3,6 +3,13 @@ import type { ComponentType } from 'svelte';
/**
* Column definition for table
*/
export type SortDirection = 'asc' | 'desc';
export interface SortState {
key: string;
direction: SortDirection;
}
export interface Column<T> {
/** Unique key for the column */
key: string;
@@ -16,6 +23,12 @@ export interface Column<T> {
align?: 'left' | 'center' | 'right';
/** Whether column is sortable */
sortable?: boolean;
/** Optional accessor used for sorting (defaults to column key lookup) */
sortAccessor?: (row: T) => string | number | boolean | Date | null | undefined;
/** Optional comparator when sorter needs full row context */
sortComparator?: (a: T, b: T) => number;
/** Default sort direction when column is first sorted */
defaultSortDirection?: SortDirection;
/** Custom cell renderer - receives the full row object */
cell?: (row: T) => string | ComponentType | { html: string };
}

View File

@@ -3,15 +3,19 @@
*/
import { Database } from '@jsr/db__sqlite';
import { Kysely } from 'kysely';
import { DenoSqlite3Dialect } from 'jsr:@soapbox/kysely-deno-sqlite';
import { logger } from '$logger/logger.ts';
import { loadAllOperations, validateOperations } from './ops.ts';
import { disableDatabaseInstance } from '$db/queries/databaseInstances.ts';
import type { PCDDatabase } from './schema.ts';
/**
* PCDCache - Manages an in-memory compiled database for a single PCD
*/
export class PCDCache {
private db: Database | null = null;
private kysely: Kysely<PCDDatabase> | null = null;
private pcdPath: string;
private databaseInstanceId: number;
private built = false;
@@ -37,6 +41,13 @@ export class PCDCache {
// Enable foreign keys
this.db.exec('PRAGMA foreign_keys = ON');
// Initialize Kysely query builder
this.kysely = new Kysely<PCDDatabase>({
dialect: new DenoSqlite3Dialect({
database: this.db
})
});
// 2. Register helper functions
this.registerHelperFunctions();
@@ -125,6 +136,10 @@ export class PCDCache {
* Close the database connection
*/
close(): void {
if (this.kysely) {
this.kysely.destroy();
this.kysely = null;
}
if (this.db) {
this.db.close();
this.db = null;
@@ -132,6 +147,17 @@ export class PCDCache {
this.built = false;
}
/**
* Get the Kysely query builder
* Use this for type-safe queries
*/
get kb(): Kysely<PCDDatabase> {
if (!this.kysely) {
throw new Error('Cache not built');
}
return this.kysely;
}
// ============================================================================
// QUERY API
// ============================================================================

View File

@@ -35,6 +35,15 @@ class PCDManager {
* Link a new PCD repository
*/
async link(options: LinkOptions): Promise<DatabaseInstance> {
await logger.debug('Starting database link operation', {
source: 'PCDManager',
meta: {
name: options.name,
repositoryUrl: options.repositoryUrl,
branch: options.branch
}
});
// Generate UUID for storage
const uuid = crypto.randomUUID();
const localPath = getPCDPath(uuid);

View File

@@ -1,472 +0,0 @@
/**
* Quality Profile queries for PCD cache
*/
import type { PCDCache } from '../cache.ts';
import type { Tag } from '../types.ts';
import { parseMarkdown } from '$utils/markdown/markdown.ts';
// Type for quality/group items in the hierarchy
interface QualityItem {
position: number;
type: 'quality' | 'group';
id: number;
name: string;
is_upgrade_until: boolean;
}
// Type for language configuration
interface ProfileLanguage {
id: number;
name: string;
type: 'must' | 'only' | 'not' | 'simple';
}
// Type for custom format counts
interface CustomFormatCounts {
all: number;
radarr: number;
sonarr: number;
total: number;
}
/**
* Quality profile general information
*/
export interface QualityProfileGeneral {
id: number;
name: string;
description: string; // Raw markdown
tags: Tag[];
}
/**
* Language configuration for a quality profile
*/
export interface QualityProfileLanguage {
id: number;
name: string;
type: 'must' | 'only' | 'not' | 'simple';
}
/**
* Quality profile languages information
*/
export interface QualityProfileLanguages {
languages: QualityProfileLanguage[];
}
/**
* Single quality item
*/
export interface QualitySingle {
id: number;
name: string;
position: number;
enabled: boolean;
isUpgradeUntil: boolean;
}
/**
* Quality group with members
*/
export interface QualityGroup {
id: number;
name: string;
position: number;
enabled: boolean;
isUpgradeUntil: boolean;
members: {
id: number;
name: string;
}[];
}
/**
* Quality profile qualities information
*/
export interface QualityProfileQualities {
singles: QualitySingle[];
groups: QualityGroup[];
}
/**
* Quality profile data for table view with all relationships
*/
export interface QualityProfileTableRow {
// Basic info
id: number;
name: string;
description: string; // Parsed HTML from markdown
// Tags
tags: Tag[];
// Upgrade settings
upgrades_allowed: boolean;
minimum_custom_format_score: number;
upgrade_until_score?: number; // Only if upgrades_allowed
upgrade_score_increment?: number; // Only if upgrades_allowed
// Custom format counts by arr type
custom_formats: CustomFormatCounts;
// Quality hierarchy (in order)
qualities: QualityItem[];
// Single language configuration
language?: ProfileLanguage;
}
/**
* Get quality profiles with full data for table/card views
* Optimized to minimize database queries
*/
export function list(cache: PCDCache): QualityProfileTableRow[] {
// 1. Get all quality profiles
const profiles = cache.query<{
id: number;
name: string;
description: string | null;
upgrades_allowed: number;
minimum_custom_format_score: number;
upgrade_until_score: number;
upgrade_score_increment: number;
}>(`
SELECT
id,
name,
description,
upgrades_allowed,
minimum_custom_format_score,
upgrade_until_score,
upgrade_score_increment
FROM quality_profiles
ORDER BY name
`);
if (profiles.length === 0) return [];
const profileIds = profiles.map(p => p.id);
const idPlaceholders = profileIds.map(() => '?').join(',');
// 2. Get all tags for all profiles in one query
const allTags = cache.query<{
quality_profile_id: number;
tag_id: number;
tag_name: string;
tag_created_at: string;
}>(`
SELECT
qpt.quality_profile_id,
t.id as tag_id,
t.name as tag_name,
t.created_at as tag_created_at
FROM quality_profile_tags qpt
JOIN tags t ON qpt.tag_id = t.id
WHERE qpt.quality_profile_id IN (${idPlaceholders})
ORDER BY qpt.quality_profile_id, t.name
`, ...profileIds);
// 3. Get custom format counts grouped by arr_type for all profiles
const formatCounts = cache.query<{
quality_profile_id: number;
arr_type: string;
count: number;
}>(`
SELECT
quality_profile_id,
arr_type,
COUNT(*) as count
FROM quality_profile_custom_formats
WHERE quality_profile_id IN (${idPlaceholders})
GROUP BY quality_profile_id, arr_type
`, ...profileIds);
// 4. Get all qualities for all profiles with names
const allQualities = cache.query<{
quality_profile_id: number;
position: number;
upgrade_until: number;
quality_id: number | null;
quality_group_id: number | null;
quality_name: string | null;
group_name: string | null;
}>(`
SELECT
qpq.quality_profile_id,
qpq.position,
qpq.upgrade_until,
qpq.quality_id,
qpq.quality_group_id,
q.name as quality_name,
qg.name as group_name
FROM quality_profile_qualities qpq
LEFT JOIN qualities q ON qpq.quality_id = q.id
LEFT JOIN quality_groups qg ON qpq.quality_group_id = qg.id
WHERE qpq.quality_profile_id IN (${idPlaceholders})
ORDER BY qpq.quality_profile_id, qpq.position
`, ...profileIds);
// 5. Get languages for all profiles (one per profile)
const allLanguages = cache.query<{
quality_profile_id: number;
language_id: number;
language_name: string;
type: string;
}>(`
SELECT
qpl.quality_profile_id,
l.id as language_id,
l.name as language_name,
qpl.type
FROM quality_profile_languages qpl
JOIN languages l ON qpl.language_id = l.id
WHERE qpl.quality_profile_id IN (${idPlaceholders})
`, ...profileIds);
// Build maps for efficient lookup
const tagsMap = new Map<number, Tag[]>();
for (const tag of allTags) {
if (!tagsMap.has(tag.quality_profile_id)) {
tagsMap.set(tag.quality_profile_id, []);
}
tagsMap.get(tag.quality_profile_id)!.push({
id: tag.tag_id,
name: tag.tag_name,
created_at: tag.tag_created_at
});
}
const formatCountsMap = new Map<number, Omit<CustomFormatCounts, 'total'>>();
for (const fc of formatCounts) {
if (!formatCountsMap.has(fc.quality_profile_id)) {
formatCountsMap.set(fc.quality_profile_id, {all: 0, radarr: 0, sonarr: 0});
}
const counts = formatCountsMap.get(fc.quality_profile_id)!;
if (fc.arr_type === 'all') counts.all = fc.count;
else if (fc.arr_type === 'radarr') counts.radarr = fc.count;
else if (fc.arr_type === 'sonarr') counts.sonarr = fc.count;
}
const qualitiesMap = new Map<number, QualityItem[]>();
for (const qual of allQualities) {
if (!qualitiesMap.has(qual.quality_profile_id)) {
qualitiesMap.set(qual.quality_profile_id, []);
}
qualitiesMap.get(qual.quality_profile_id)!.push({
position: qual.position,
type: qual.quality_id ? 'quality' : 'group',
id: qual.quality_id || qual.quality_group_id!,
name: qual.quality_name || qual.group_name!,
is_upgrade_until: qual.upgrade_until === 1
});
}
const languagesMap = new Map<number, ProfileLanguage>();
for (const lang of allLanguages) {
languagesMap.set(lang.quality_profile_id, {
id: lang.language_id,
name: lang.language_name,
type: lang.type as 'must' | 'only' | 'not' | 'simple'
});
}
// Build the final result
return profiles.map(profile => {
const counts = formatCountsMap.get(profile.id) || {all: 0, radarr: 0, sonarr: 0};
const result: QualityProfileTableRow = {
id: profile.id,
name: profile.name,
description: parseMarkdown(profile.description),
tags: tagsMap.get(profile.id) || [],
upgrades_allowed: profile.upgrades_allowed === 1,
minimum_custom_format_score: profile.minimum_custom_format_score,
custom_formats: {
all: counts.all,
radarr: counts.radarr,
sonarr: counts.sonarr,
total: counts.all + counts.radarr + counts.sonarr
},
qualities: qualitiesMap.get(profile.id) || [],
language: languagesMap.get(profile.id)
};
// Only include upgrade settings if upgrades are allowed
if (profile.upgrades_allowed === 1) {
result.upgrade_until_score = profile.upgrade_until_score;
result.upgrade_score_increment = profile.upgrade_score_increment;
}
return result;
});
}
/**
* Get general information for a single quality profile
*/
export function general(cache: PCDCache, profileId: number): QualityProfileGeneral | null {
// Get the quality profile
const profiles = cache.query<{
id: number;
name: string;
description: string | null;
}>(`
SELECT
id,
name,
description
FROM quality_profiles
WHERE id = ?
`, profileId);
if (profiles.length === 0) return null;
const profile = profiles[0];
// Get tags for this profile
const tags = cache.query<{
tag_id: number;
tag_name: string;
tag_created_at: string;
}>(`
SELECT
t.id as tag_id,
t.name as tag_name,
t.created_at as tag_created_at
FROM quality_profile_tags qpt
JOIN tags t ON qpt.tag_id = t.id
WHERE qpt.quality_profile_id = ?
ORDER BY t.name
`, profileId);
return {
id: profile.id,
name: profile.name,
description: profile.description || '',
tags: tags.map(tag => ({
id: tag.tag_id,
name: tag.tag_name,
created_at: tag.tag_created_at
}))
};
}
/**
* Get languages for a quality profile
*/
export function languages(cache: PCDCache, profileId: number): QualityProfileLanguages {
const profileLanguages = cache.query<{
language_id: number;
language_name: string;
type: string;
}>(`
SELECT
l.id as language_id,
l.name as language_name,
qpl.type
FROM quality_profile_languages qpl
JOIN languages l ON qpl.language_id = l.id
WHERE qpl.quality_profile_id = ?
ORDER BY l.name
`, profileId);
return {
languages: profileLanguages.map(lang => ({
id: lang.language_id,
name: lang.language_name,
type: lang.type as 'must' | 'only' | 'not' | 'simple'
}))
};
}
/**
* Get qualities for a quality profile (singles, groups, and available)
*/
export function qualities(cache: PCDCache, profileId: number): QualityProfileQualities {
// 1. Get single qualities
const singles = cache.query<{
quality_id: number;
quality_name: string;
position: number;
enabled: number;
upgrade_until: number;
}>(`
SELECT
qpq.quality_id,
q.name as quality_name,
qpq.position,
qpq.enabled,
qpq.upgrade_until
FROM quality_profile_qualities qpq
JOIN qualities q ON qpq.quality_id = q.id
WHERE qpq.quality_profile_id = ? AND qpq.quality_id IS NOT NULL
ORDER BY qpq.position
`, profileId);
// 2. Get groups with their IDs
const groups = cache.query<{
group_id: number;
group_name: string;
position: number;
enabled: number;
upgrade_until: number;
}>(`
SELECT
qpq.quality_group_id as group_id,
qg.name as group_name,
qpq.position,
qpq.enabled,
qpq.upgrade_until
FROM quality_profile_qualities qpq
JOIN quality_groups qg ON qpq.quality_group_id = qg.id
WHERE qpq.quality_profile_id = ? AND qpq.quality_group_id IS NOT NULL
ORDER BY qpq.position
`, profileId);
// 3. Get all group members for all groups in this profile
const groupIds = groups.map(g => g.group_id);
const groupMembers = groupIds.length > 0 ? cache.query<{
group_id: number;
quality_id: number;
quality_name: string;
}>(`
SELECT
qgm.quality_group_id as group_id,
qgm.quality_id,
q.name as quality_name
FROM quality_group_members qgm
JOIN qualities q ON qgm.quality_id = q.id
WHERE qgm.quality_group_id IN (${groupIds.map(() => '?').join(',')})
ORDER BY q.name
`, ...groupIds) : [];
// Build groups with members
const groupsWithMembers: QualityGroup[] = groups.map(g => {
const members = groupMembers
.filter(gm => gm.group_id === g.group_id)
.map(gm => ({ id: gm.quality_id, name: gm.quality_name }));
return {
id: g.group_id,
name: g.group_name,
position: g.position,
enabled: g.enabled === 1,
isUpgradeUntil: g.upgrade_until === 1,
members
};
});
return {
singles: singles.map(s => ({
id: s.quality_id,
name: s.quality_name,
position: s.position,
enabled: s.enabled === 1,
isUpgradeUntil: s.upgrade_until === 1
})),
groups: groupsWithMembers
};
}

View File

@@ -0,0 +1,42 @@
/**
* Quality profile general queries
*/
import type { PCDCache } from '../../cache.ts';
import type { QualityProfileGeneral } from './types.ts';
/**
* Get general information for a single quality profile
*/
export async function general(cache: PCDCache, profileId: number): Promise<QualityProfileGeneral | null> {
const db = cache.kb;
// Get the quality profile
const profile = await db
.selectFrom('quality_profiles')
.select(['id', 'name', 'description'])
.where('id', '=', profileId)
.executeTakeFirst();
if (!profile) return null;
// Get tags for this profile
const tags = await db
.selectFrom('quality_profile_tags as qpt')
.innerJoin('tags as t', 't.id', 'qpt.tag_id')
.select(['t.id as tag_id', 't.name as tag_name', 't.created_at as tag_created_at'])
.where('qpt.quality_profile_id', '=', profileId)
.orderBy('t.name')
.execute();
return {
id: profile.id,
name: profile.name,
description: profile.description || '',
tags: tags.map((tag) => ({
id: tag.tag_id,
name: tag.tag_name,
created_at: tag.tag_created_at
}))
};
}

View File

@@ -0,0 +1,24 @@
/**
* Quality Profile queries
*/
// Export all types
export type {
QualityItem,
ProfileLanguage,
CustomFormatCounts,
QualityProfileGeneral,
QualityProfileLanguage,
QualityProfileLanguages,
QualitySingle,
QualityGroup,
QualityProfileQualities,
QualityProfileTableRow
} from './types.ts';
// Export query functions
export { list } from './list.ts';
export { general } from './general.ts';
export { languages } from './languages.ts';
export { scoring } from './scoring.ts';
// TODO: qualities function needs to be rewritten

View File

@@ -0,0 +1,29 @@
/**
* Quality profile languages queries
*/
import type { PCDCache } from '../../cache.ts';
import type { QualityProfileLanguages } from './types.ts';
/**
* Get languages for a quality profile
*/
export async function languages(cache: PCDCache, profileId: number): Promise<QualityProfileLanguages> {
const db = cache.kb;
const profileLanguages = await db
.selectFrom('quality_profile_languages as qpl')
.innerJoin('languages as l', 'qpl.language_id', 'l.id')
.select(['l.id as language_id', 'l.name as language_name', 'qpl.type'])
.where('qpl.quality_profile_id', '=', profileId)
.orderBy('l.name')
.execute();
return {
languages: profileLanguages.map((lang) => ({
id: lang.language_id,
name: lang.language_name,
type: lang.type as 'must' | 'only' | 'not' | 'simple'
}))
};
}

View File

@@ -0,0 +1,171 @@
/**
* Quality profile list queries
*/
import type { PCDCache } from '../../cache.ts';
import type { Tag } from '../../types.ts';
import type { QualityProfileTableRow, QualityItem, ProfileLanguage, CustomFormatCounts } from './types.ts';
import { parseMarkdown } from '$utils/markdown/markdown.ts';
import { logger } from '$logger/logger.ts';
/**
* Get quality profiles with full data for table/card views
* Optimized to minimize database queries
*/
export async function list(cache: PCDCache): Promise<QualityProfileTableRow[]> {
const db = cache.kb;
// 1. Get all quality profiles
const profiles = await db
.selectFrom('quality_profiles')
.select([
'id',
'name',
'description',
'upgrades_allowed',
'minimum_custom_format_score',
'upgrade_until_score',
'upgrade_score_increment'
])
.orderBy('name')
.execute();
if (profiles.length === 0) return [];
const profileIds = profiles.map(p => p.id);
// 2. Get all tags for all profiles
const allTags = await db
.selectFrom('quality_profile_tags as qpt')
.innerJoin('tags as t', 't.id', 'qpt.tag_id')
.select([
'qpt.quality_profile_id',
't.id as tag_id',
't.name as tag_name',
't.created_at as tag_created_at'
])
.where('qpt.quality_profile_id', 'in', profileIds)
.orderBy(['qpt.quality_profile_id', 't.name'])
.execute();
// 3. Get custom format counts grouped by arr_type
const formatCounts = await db
.selectFrom('quality_profile_custom_formats')
.select(['quality_profile_id', 'arr_type'])
.select((eb) => eb.fn.count('quality_profile_id').as('count'))
.where('quality_profile_id', 'in', profileIds)
.groupBy(['quality_profile_id', 'arr_type'])
.execute();
// 4. Get all qualities for all profiles with names
const allQualities = await db
.selectFrom('quality_profile_qualities as qpq')
.leftJoin('qualities as q', 'qpq.quality_id', 'q.id')
.leftJoin('quality_groups as qg', 'qpq.quality_group_id', 'qg.id')
.select([
'qpq.quality_profile_id',
'qpq.position',
'qpq.upgrade_until',
'qpq.quality_id',
'qpq.quality_group_id',
'q.name as quality_name',
'qg.name as group_name'
])
.where('qpq.quality_profile_id', 'in', profileIds)
.orderBy(['qpq.quality_profile_id', 'qpq.position'])
.execute();
// 5. Get languages for all profiles
const allLanguages = await db
.selectFrom('quality_profile_languages as qpl')
.innerJoin('languages as l', 'qpl.language_id', 'l.id')
.select([
'qpl.quality_profile_id',
'l.id as language_id',
'l.name as language_name',
'qpl.type'
])
.where('qpl.quality_profile_id', 'in', profileIds)
.execute();
// Build maps for efficient lookup
const tagsMap = new Map<number, Tag[]>();
for (const tag of allTags) {
if (!tagsMap.has(tag.quality_profile_id)) {
tagsMap.set(tag.quality_profile_id, []);
}
tagsMap.get(tag.quality_profile_id)!.push({
id: tag.tag_id,
name: tag.tag_name,
created_at: tag.tag_created_at
});
}
const formatCountsMap = new Map<number, Omit<CustomFormatCounts, 'total'>>();
for (const fc of formatCounts) {
if (!formatCountsMap.has(fc.quality_profile_id)) {
formatCountsMap.set(fc.quality_profile_id, { all: 0, radarr: 0, sonarr: 0 });
}
const counts = formatCountsMap.get(fc.quality_profile_id)!;
const count = Number(fc.count);
if (fc.arr_type === 'all') counts.all = count;
else if (fc.arr_type === 'radarr') counts.radarr = count;
else if (fc.arr_type === 'sonarr') counts.sonarr = count;
}
const qualitiesMap = new Map<number, QualityItem[]>();
for (const qual of allQualities) {
if (!qualitiesMap.has(qual.quality_profile_id)) {
qualitiesMap.set(qual.quality_profile_id, []);
}
qualitiesMap.get(qual.quality_profile_id)!.push({
position: qual.position,
type: qual.quality_id ? 'quality' : 'group',
id: qual.quality_id || qual.quality_group_id!,
name: qual.quality_name || qual.group_name!,
is_upgrade_until: qual.upgrade_until === 1
});
}
const languagesMap = new Map<number, ProfileLanguage>();
for (const lang of allLanguages) {
languagesMap.set(lang.quality_profile_id, {
id: lang.language_id,
name: lang.language_name,
type: lang.type as 'must' | 'only' | 'not' | 'simple'
});
}
// Build the final result
const results = profiles.map((profile) => {
const counts = formatCountsMap.get(profile.id) || { all: 0, radarr: 0, sonarr: 0 };
const result: QualityProfileTableRow = {
id: profile.id,
name: profile.name,
description: parseMarkdown(profile.description),
tags: tagsMap.get(profile.id) || [],
upgrades_allowed: profile.upgrades_allowed === 1,
minimum_custom_format_score: profile.minimum_custom_format_score,
custom_formats: {
all: counts.all,
radarr: counts.radarr,
sonarr: counts.sonarr,
total: counts.all + counts.radarr + counts.sonarr
},
qualities: qualitiesMap.get(profile.id) || [],
language: languagesMap.get(profile.id)
};
// Only include upgrade settings if upgrades are allowed
if (profile.upgrades_allowed === 1) {
result.upgrade_until_score = profile.upgrade_until_score;
result.upgrade_score_increment = profile.upgrade_score_increment;
}
return result;
});
return results;
}

View File

@@ -0,0 +1,111 @@
/**
* Quality Profile query-specific types
*/
import type { Tag } from '../../types.ts';
// ============================================================================
// INTERNAL TYPES (used within queries)
// ============================================================================
/** Quality/group item in the hierarchy */
export interface QualityItem {
position: number;
type: 'quality' | 'group';
id: number;
name: string;
is_upgrade_until: boolean;
}
/** Language configuration */
export interface ProfileLanguage {
id: number;
name: string;
type: 'must' | 'only' | 'not' | 'simple';
}
/** Custom format counts by arr type */
export interface CustomFormatCounts {
all: number;
radarr: number;
sonarr: number;
total: number;
}
// ============================================================================
// QUERY RESULT TYPES
// ============================================================================
/** Quality profile general information */
export interface QualityProfileGeneral {
id: number;
name: string;
description: string; // Raw markdown
tags: Tag[];
}
/** Language configuration for a quality profile */
export interface QualityProfileLanguage {
id: number;
name: string;
type: 'must' | 'only' | 'not' | 'simple';
}
/** Quality profile languages information */
export interface QualityProfileLanguages {
languages: QualityProfileLanguage[];
}
/** Single quality item */
export interface QualitySingle {
id: number;
name: string;
position: number;
enabled: boolean;
isUpgradeUntil: boolean;
}
/** Quality group with members */
export interface QualityGroup {
id: number;
name: string;
position: number;
enabled: boolean;
isUpgradeUntil: boolean;
members: {
id: number;
name: string;
}[];
}
/** Quality profile qualities information */
export interface QualityProfileQualities {
singles: QualitySingle[];
groups: QualityGroup[];
}
/** Quality profile data for table view with all relationships */
export interface QualityProfileTableRow {
// Basic info
id: number;
name: string;
description: string; // Parsed HTML from markdown
// Tags
tags: Tag[];
// Upgrade settings
upgrades_allowed: boolean;
minimum_custom_format_score: number;
upgrade_until_score?: number; // Only if upgrades_allowed
upgrade_score_increment?: number; // Only if upgrades_allowed
// Custom format counts by arr type
custom_formats: CustomFormatCounts;
// Quality hierarchy (in order)
qualities: QualityItem[];
// Single language configuration
language?: ProfileLanguage;
}

View File

@@ -0,0 +1,214 @@
/**
* Kysely Database Schema Types for PCD (Profilarr Compliant Database)
* Auto-generated columns use Generated<T>
*/
import type { Generated } from 'kysely';
// ============================================================================
// CORE ENTITY TABLES
// ============================================================================
export interface TagsTable {
id: Generated<number>;
name: string;
created_at: Generated<string>;
}
export interface LanguagesTable {
id: Generated<number>;
name: string;
created_at: Generated<string>;
updated_at: Generated<string>;
}
export interface RegularExpressionsTable {
id: Generated<number>;
name: string;
pattern: string;
regex101_id: string | null;
description: string | null;
created_at: Generated<string>;
updated_at: Generated<string>;
}
export interface QualitiesTable {
id: Generated<number>;
name: string;
created_at: Generated<string>;
updated_at: Generated<string>;
}
export interface CustomFormatsTable {
id: Generated<number>;
name: string;
description: string | null;
created_at: Generated<string>;
updated_at: Generated<string>;
}
// ============================================================================
// DEPENDENT ENTITY TABLES
// ============================================================================
export interface QualityProfilesTable {
id: Generated<number>;
name: string;
description: string | null;
upgrades_allowed: number;
minimum_custom_format_score: number;
upgrade_until_score: number;
upgrade_score_increment: number;
created_at: Generated<string>;
updated_at: Generated<string>;
}
export interface QualityGroupsTable {
id: Generated<number>;
quality_profile_id: number;
name: string;
created_at: Generated<string>;
updated_at: Generated<string>;
}
export interface CustomFormatConditionsTable {
id: Generated<number>;
custom_format_id: number;
name: string;
type: string;
arr_type: string;
negate: number;
required: number;
created_at: Generated<string>;
updated_at: Generated<string>;
}
// ============================================================================
// JUNCTION TABLES
// ============================================================================
export interface RegularExpressionTagsTable {
regular_expression_id: number;
tag_id: number;
}
export interface CustomFormatTagsTable {
custom_format_id: number;
tag_id: number;
}
export interface QualityProfileTagsTable {
quality_profile_id: number;
tag_id: number;
}
export interface QualityProfileLanguagesTable {
quality_profile_id: number;
language_id: number;
type: string;
}
export interface QualityGroupMembersTable {
quality_group_id: number;
quality_id: number;
}
export interface QualityProfileQualitiesTable {
id: Generated<number>;
quality_profile_id: number;
quality_id: number | null;
quality_group_id: number | null;
position: number;
enabled: number;
upgrade_until: number;
}
export interface QualityProfileCustomFormatsTable {
quality_profile_id: number;
custom_format_id: number;
arr_type: string;
score: number;
}
// ============================================================================
// CUSTOM FORMAT CONDITION TYPE TABLES
// ============================================================================
export interface ConditionPatternsTable {
custom_format_condition_id: number;
regular_expression_id: number;
}
export interface ConditionLanguagesTable {
custom_format_condition_id: number;
language_id: number;
except_language: number;
}
export interface ConditionIndexerFlagsTable {
custom_format_condition_id: number;
flag: string;
}
export interface ConditionSourcesTable {
custom_format_condition_id: number;
source: string;
}
export interface ConditionResolutionsTable {
custom_format_condition_id: number;
resolution: string;
}
export interface ConditionQualityModifiersTable {
custom_format_condition_id: number;
quality_modifier: string;
}
export interface ConditionSizesTable {
custom_format_condition_id: number;
min_bytes: number | null;
max_bytes: number | null;
}
export interface ConditionReleaseTypesTable {
custom_format_condition_id: number;
release_type: string;
}
export interface ConditionYearsTable {
custom_format_condition_id: number;
min_year: number | null;
max_year: number | null;
}
// ============================================================================
// DATABASE INTERFACE
// ============================================================================
export interface PCDDatabase {
tags: TagsTable;
languages: LanguagesTable;
regular_expressions: RegularExpressionsTable;
qualities: QualitiesTable;
custom_formats: CustomFormatsTable;
quality_profiles: QualityProfilesTable;
quality_groups: QualityGroupsTable;
custom_format_conditions: CustomFormatConditionsTable;
regular_expression_tags: RegularExpressionTagsTable;
custom_format_tags: CustomFormatTagsTable;
quality_profile_tags: QualityProfileTagsTable;
quality_profile_languages: QualityProfileLanguagesTable;
quality_group_members: QualityGroupMembersTable;
quality_profile_qualities: QualityProfileQualitiesTable;
quality_profile_custom_formats: QualityProfileCustomFormatsTable;
condition_patterns: ConditionPatternsTable;
condition_languages: ConditionLanguagesTable;
condition_indexer_flags: ConditionIndexerFlagsTable;
condition_sources: ConditionSourcesTable;
condition_resolutions: ConditionResolutionsTable;
condition_quality_modifiers: ConditionQualityModifiersTable;
condition_sizes: ConditionSizesTable;
condition_release_types: ConditionReleaseTypesTable;
condition_years: ConditionYearsTable;
}

View File

@@ -254,3 +254,22 @@ export interface QualityGroupMemberRow {
quality_group_id: number;
quality_id: number;
}
// ============================================================================
// QUALITY PROFILE SCORING TYPES
// ============================================================================
export interface CustomFormatScoring {
id: number; // custom format ID
name: string; // custom format name
scores: Record<string, number | null>; // arr_type -> score mapping
}
export interface QualityProfileScoring {
databaseId: number; // PCD database ID (for linking)
arrTypes: string[]; // Display arr types: ['radarr', 'sonarr']. Note: 'all' scores are applied to each type
customFormats: CustomFormatScoring[]; // All custom formats with their scores
minimum_custom_format_score: number; // Minimum score to accept
upgrade_until_score: number; // Stop upgrading when reached
upgrade_score_increment: number; // Minimum score improvement needed
}

View File

@@ -1,9 +1,9 @@
import { error } from '@sveltejs/kit';
import type { ServerLoad } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import * as qualityProfileQueries from '$pcd/queries/qualityProfiles.ts';
import * as qualityProfileQueries from '$pcd/queries/qualityProfiles/index.ts';
export const load: ServerLoad = ({ params }) => {
export const load: ServerLoad = async ({ params }) => {
const { databaseId } = params;
// Validate params exist
@@ -34,7 +34,7 @@ export const load: ServerLoad = ({ params }) => {
}
// Load quality profiles for the current database
const qualityProfiles = qualityProfileQueries.list(cache);
const qualityProfiles = await qualityProfileQueries.list(cache);
return {
databases,

View File

@@ -1,9 +1,9 @@
import { error } from '@sveltejs/kit';
import type { ServerLoad } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import * as qualityProfileQueries from '$pcd/queries/qualityProfiles.ts';
import * as qualityProfileQueries from '$pcd/queries/qualityProfiles/index.ts';
export const load: ServerLoad = ({ params }) => {
export const load: ServerLoad = async ({ params }) => {
const { databaseId, id } = params;
// Validate params exist
@@ -30,7 +30,7 @@ export const load: ServerLoad = ({ params }) => {
}
// Load general information for the quality profile
const profile = qualityProfileQueries.general(cache, profileId);
const profile = await qualityProfileQueries.general(cache, profileId);
if (!profile) {
throw error(404, 'Quality profile not found');

View File

@@ -1,10 +1,10 @@
import { error } from '@sveltejs/kit';
import type { ServerLoad } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import * as qualityProfileQueries from '$pcd/queries/qualityProfiles.ts';
import * as qualityProfileQueries from '$pcd/queries/qualityProfiles/index.ts';
import * as languageQueries from '$pcd/queries/languages.ts';
export const load: ServerLoad = ({ params }) => {
export const load: ServerLoad = async ({ params }) => {
const { databaseId, id } = params;
// Validate params exist
@@ -31,7 +31,7 @@ export const load: ServerLoad = ({ params }) => {
}
// Load languages for the quality profile
const languagesData = qualityProfileQueries.languages(cache, profileId);
const languagesData = await qualityProfileQueries.languages(cache, profileId);
// Load all available languages
const availableLanguages = languageQueries.list(cache);