diff --git a/deno.json b/deno.json index 0463a8c..e0a35de 100644 --- a/deno.json +++ b/deno.json @@ -15,6 +15,7 @@ "$http/": "./src/lib/server/utils/http/", "$utils/": "./src/lib/server/utils/", "$notifications/": "./src/lib/server/notifications/", + "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.2.0", "@std/assert": "jsr:@std/assert@^1.0.0", "marked": "npm:marked@^15.0.6", "simple-icons": "npm:simple-icons@^15.17.0" diff --git a/deno.lock b/deno.lock index 05d0b61..8208517 100644 --- a/deno.lock +++ b/deno.lock @@ -1,9 +1,12 @@ { "version": "5", "specifiers": { + "jsr:@soapbox/kysely-deno-sqlite@*": "2.2.0", + "jsr:@soapbox/kysely-deno-sqlite@^2.2.0": "2.2.0", "jsr:@std/assert@*": "1.0.15", "jsr:@std/assert@1": "1.0.15", "jsr:@std/internal@^1.0.12": "1.0.12", + "npm:@deno/vite-plugin@^1.0.5": "1.0.5_vite@7.1.12__@types+node@22.19.0__picomatch@4.0.3_@types+node@22.19.0", "npm:@eslint/compat@^1.4.0": "1.4.1_eslint@9.39.1", "npm:@eslint/js@^9.36.0": "9.39.1", "npm:@jsr/db__sqlite@0.12": "0.12.0", @@ -16,6 +19,8 @@ "npm:eslint-plugin-svelte@^3.12.4": "3.13.0_eslint@9.39.1_svelte@5.43.3__acorn@8.15.0_postcss@8.5.6", "npm:eslint@^9.36.0": "9.39.1", "npm:globals@^16.4.0": "16.5.0", + "npm:kysely@~0.27.2": "0.27.6", + "npm:kysely@~0.28.8": "0.28.8", "npm:lucide-svelte@0.546": "0.546.0_svelte@5.43.3__acorn@8.15.0", "npm:marked@^15.0.6": "15.0.12", "npm:prettier-plugin-svelte@^3.4.0": "3.4.0_prettier@3.6.2_svelte@5.43.3__acorn@8.15.0", @@ -31,6 +36,12 @@ "npm:vite@^7.1.7": "7.1.12_@types+node@22.19.0_picomatch@4.0.3" }, "jsr": { + "@soapbox/kysely-deno-sqlite@2.2.0": { + "integrity": "668ec94600bc4b4d7bd618dd7ca65d4ef30ee61c46ffcb379b6f45203c08517a", + "dependencies": [ + "npm:kysely@~0.27.2" + ] + }, "@std/assert@1.0.15": { "integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b", "dependencies": [ @@ -42,6 +53,12 @@ } }, "npm": { + "@deno/vite-plugin@1.0.5_vite@7.1.12__@types+node@22.19.0__picomatch@4.0.3_@types+node@22.19.0": { + "integrity": "sha512-tLja5n4dyMhcze1NzvSs2iiriBymfBlDCZIrjMTxb9O2ru0gvmV6mn5oBD2teNw5Sd92cj3YJzKwsAs8tMJXlg==", + "dependencies": [ + "vite" + ] + }, "@esbuild/aix-ppc64@0.24.2": { "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", "os": ["aix"], @@ -991,31 +1008,6 @@ "devalue@5.4.2": { "integrity": "sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw==" }, - "dom-serializer@2.0.0": { - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dependencies": [ - "domelementtype", - "domhandler", - "entities" - ] - }, - "domelementtype@2.3.0": { - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" - }, - "domhandler@5.0.3": { - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": [ - "domelementtype" - ] - }, - "domutils@3.2.2": { - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dependencies": [ - "dom-serializer", - "domelementtype", - "domhandler" - ] - }, "enhanced-resolve@5.18.3": { "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dependencies": [ @@ -1023,9 +1015,6 @@ "tapable" ] }, - "entities@4.5.0": { - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" - }, "esbuild@0.24.2": { "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", "optionalDependencies": [ @@ -1304,15 +1293,6 @@ "has-flag@4.0.0": { "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, - "htmlparser2@8.0.2": { - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "dependencies": [ - "domelementtype", - "domhandler", - "domutils", - "entities" - ] - }, "ignore@5.3.2": { "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" }, @@ -1341,9 +1321,6 @@ "is-number@7.0.0": { "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, - "is-plain-object@5.0.0": { - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" - }, "is-reference@3.0.3": { "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "dependencies": [ @@ -1385,6 +1362,12 @@ "known-css-properties@0.37.0": { "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==" }, + "kysely@0.27.6": { + "integrity": "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==" + }, + "kysely@0.28.8": { + "integrity": "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA==" + }, "levn@0.4.1": { "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dependencies": [ @@ -1493,10 +1476,6 @@ "@jridgewell/sourcemap-codec" ] }, - "marked@12.0.2": { - "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", - "bin": true - }, "marked@15.0.12": { "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "bin": true @@ -1572,9 +1551,6 @@ "callsites" ] }, - "parse-srcset@1.0.2": { - "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" - }, "path-exists@4.0.0": { "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" }, @@ -1711,17 +1687,6 @@ "mri" ] }, - "sanitize-html@2.17.0": { - "integrity": "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==", - "dependencies": [ - "deepmerge", - "escape-string-regexp", - "htmlparser2", - "is-plain-object", - "parse-srcset", - "postcss" - ] - }, "semver@7.7.3": { "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "bin": true @@ -1926,12 +1891,14 @@ }, "workspace": { "dependencies": [ + "jsr:@soapbox/kysely-deno-sqlite@^2.2.0", "jsr:@std/assert@1", "npm:marked@^15.0.6", "npm:simple-icons@^15.17.0" ], "packageJson": { "dependencies": [ + "npm:@deno/vite-plugin@^1.0.5", "npm:@eslint/compat@^1.4.0", "npm:@eslint/js@^9.36.0", "npm:@jsr/db__sqlite@0.12", @@ -1944,6 +1911,7 @@ "npm:eslint-plugin-svelte@^3.12.4", "npm:eslint@^9.36.0", "npm:globals@^16.4.0", + "npm:kysely@~0.28.8", "npm:lucide-svelte@0.546", "npm:marked@^15.0.6", "npm:prettier-plugin-svelte@^3.4.0", diff --git a/package.json b/package.json index 9dbaab0..43a982e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "version": "2.0.0", "type": "module", "dependencies": { + "@deno/vite-plugin": "^1.0.5", "@jsr/db__sqlite": "^0.12.0", + "kysely": "^0.28.8", "lucide-svelte": "^0.546.0", "marked": "^15.0.6", "simple-icons": "^15.17.0", diff --git a/src/lib/client/ui/table/types.ts b/src/lib/client/ui/table/types.ts index 8758873..02b248f 100644 --- a/src/lib/client/ui/table/types.ts +++ b/src/lib/client/ui/table/types.ts @@ -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 { /** Unique key for the column */ key: string; @@ -16,6 +23,12 @@ export interface Column { 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 }; } diff --git a/src/lib/server/pcd/cache.ts b/src/lib/server/pcd/cache.ts index 7b3e498..8f7934c 100644 --- a/src/lib/server/pcd/cache.ts +++ b/src/lib/server/pcd/cache.ts @@ -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 | 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({ + 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 { + if (!this.kysely) { + throw new Error('Cache not built'); + } + return this.kysely; + } + // ============================================================================ // QUERY API // ============================================================================ diff --git a/src/lib/server/pcd/pcd.ts b/src/lib/server/pcd/pcd.ts index e73cf63..6d306ae 100644 --- a/src/lib/server/pcd/pcd.ts +++ b/src/lib/server/pcd/pcd.ts @@ -35,6 +35,15 @@ class PCDManager { * Link a new PCD repository */ async link(options: LinkOptions): Promise { + 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); diff --git a/src/lib/server/pcd/queries/qualityProfiles.ts b/src/lib/server/pcd/queries/qualityProfiles.ts deleted file mode 100644 index 0fb6257..0000000 --- a/src/lib/server/pcd/queries/qualityProfiles.ts +++ /dev/null @@ -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(); - 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>(); - 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(); - 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(); - 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 - }; -} \ No newline at end of file diff --git a/src/lib/server/pcd/queries/qualityProfiles/general.ts b/src/lib/server/pcd/queries/qualityProfiles/general.ts new file mode 100644 index 0000000..8048e60 --- /dev/null +++ b/src/lib/server/pcd/queries/qualityProfiles/general.ts @@ -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 { + 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 + })) + }; +} diff --git a/src/lib/server/pcd/queries/qualityProfiles/index.ts b/src/lib/server/pcd/queries/qualityProfiles/index.ts new file mode 100644 index 0000000..28d6cbb --- /dev/null +++ b/src/lib/server/pcd/queries/qualityProfiles/index.ts @@ -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 diff --git a/src/lib/server/pcd/queries/qualityProfiles/languages.ts b/src/lib/server/pcd/queries/qualityProfiles/languages.ts new file mode 100644 index 0000000..6c91baf --- /dev/null +++ b/src/lib/server/pcd/queries/qualityProfiles/languages.ts @@ -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 { + 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' + })) + }; +} diff --git a/src/lib/server/pcd/queries/qualityProfiles/list.ts b/src/lib/server/pcd/queries/qualityProfiles/list.ts new file mode 100644 index 0000000..4e3b43b --- /dev/null +++ b/src/lib/server/pcd/queries/qualityProfiles/list.ts @@ -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 { + 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(); + 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>(); + 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(); + 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(); + 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; +} diff --git a/src/lib/server/pcd/queries/qualityProfiles/types.ts b/src/lib/server/pcd/queries/qualityProfiles/types.ts new file mode 100644 index 0000000..c1e1daa --- /dev/null +++ b/src/lib/server/pcd/queries/qualityProfiles/types.ts @@ -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; +} diff --git a/src/lib/server/pcd/schema.ts b/src/lib/server/pcd/schema.ts new file mode 100644 index 0000000..d027a5a --- /dev/null +++ b/src/lib/server/pcd/schema.ts @@ -0,0 +1,214 @@ +/** + * Kysely Database Schema Types for PCD (Profilarr Compliant Database) + * Auto-generated columns use Generated + */ + +import type { Generated } from 'kysely'; + +// ============================================================================ +// CORE ENTITY TABLES +// ============================================================================ + +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 RegularExpressionsTable { + id: Generated; + name: string; + pattern: string; + regex101_id: string | null; + description: string | null; + created_at: Generated; + updated_at: Generated; +} + +export interface QualitiesTable { + id: Generated; + name: string; + created_at: Generated; + updated_at: Generated; +} + +export interface CustomFormatsTable { + id: Generated; + name: string; + description: string | null; + created_at: Generated; + updated_at: Generated; +} + +// ============================================================================ +// DEPENDENT ENTITY TABLES +// ============================================================================ + +export interface QualityProfilesTable { + id: Generated; + name: string; + description: string | null; + upgrades_allowed: number; + minimum_custom_format_score: number; + upgrade_until_score: number; + upgrade_score_increment: number; + created_at: Generated; + updated_at: Generated; +} + +export interface QualityGroupsTable { + id: Generated; + quality_profile_id: number; + name: string; + created_at: Generated; + updated_at: Generated; +} + +export interface CustomFormatConditionsTable { + id: Generated; + custom_format_id: number; + name: string; + type: string; + arr_type: string; + negate: number; + required: number; + created_at: Generated; + updated_at: Generated; +} + +// ============================================================================ +// 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; + 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; +} diff --git a/src/lib/server/pcd/types.ts b/src/lib/server/pcd/types.ts index 820c8c7..58f1590 100644 --- a/src/lib/server/pcd/types.ts +++ b/src/lib/server/pcd/types.ts @@ -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; // 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 +} diff --git a/src/routes/quality-profiles/[databaseId]/+page.server.ts b/src/routes/quality-profiles/[databaseId]/+page.server.ts index eb44d6d..d73e3a0 100644 --- a/src/routes/quality-profiles/[databaseId]/+page.server.ts +++ b/src/routes/quality-profiles/[databaseId]/+page.server.ts @@ -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, diff --git a/src/routes/quality-profiles/[databaseId]/[id]/general/+page.server.ts b/src/routes/quality-profiles/[databaseId]/[id]/general/+page.server.ts index c96e0ac..6e5b0e1 100644 --- a/src/routes/quality-profiles/[databaseId]/[id]/general/+page.server.ts +++ b/src/routes/quality-profiles/[databaseId]/[id]/general/+page.server.ts @@ -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'); diff --git a/src/routes/quality-profiles/[databaseId]/[id]/languages/+page.server.ts b/src/routes/quality-profiles/[databaseId]/[id]/languages/+page.server.ts index 97267f2..7a56ee9 100644 --- a/src/routes/quality-profiles/[databaseId]/[id]/languages/+page.server.ts +++ b/src/routes/quality-profiles/[databaseId]/[id]/languages/+page.server.ts @@ -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); diff --git a/vite.config.ts b/vite.config.ts index f8cbad3..111070d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,13 +2,18 @@ import tailwindcss from '@tailwindcss/vite'; import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; import { readFileSync } from 'node:fs'; +import deno from '@deno/vite-plugin'; const packageJson = JSON.parse(readFileSync('./package.json', 'utf-8')); export default defineConfig({ - plugins: [tailwindcss(), sveltekit()], + plugins: [deno(), tailwindcss(), sveltekit()], server: { - port: 6969 + port: 6969, + watch: { + // Ignore temporary files created by editors + ignored: ['**/*.tmp.*', '**/*~', '**/.#*'] + } }, define: { __APP_VERSION__: JSON.stringify(packageJson.version)