refactor(pcd): better file structure, remove watcher utils, update deps using tags instead of branches

This commit is contained in:
Sam Chau
2026-01-28 02:55:57 +10:30
parent bb5d4af476
commit 7fe06dcdb1
119 changed files with 1244 additions and 1106 deletions

View File

@@ -8,7 +8,7 @@ import { db } from '$db/db.ts';
import { runMigrations } from '$db/migrations.ts';
import { initializeJobs } from '$jobs/init.ts';
import { jobScheduler } from '$jobs/scheduler.ts';
import { pcdManager } from '$pcd/pcd.ts';
import { pcdManager } from '$pcd/index.ts';
import {
getAuthState,
isPublicPath,

View File

@@ -3,7 +3,7 @@
* Checks for databases that need syncing and pulls updates if auto_pull is enabled
*/
import { pcdManager } from '$pcd/pcd.ts';
import { pcdManager } from '$pcd/index.ts';
import { logger } from '$logger/logger.ts';
import { databaseInstancesQueries } from '$db/queries/databaseInstances.ts';

View File

@@ -1,561 +0,0 @@
/**
* PCD Cache - In-memory compiled view of PCD operations
*/
import { Database } from '@jsr/db__sqlite';
import { Kysely } from 'kysely';
// @ts-ignore - Deno JSR import not recognized by svelte-check
import { DenoSqlite3Dialect } from '@soapbox/kysely-deno-sqlite';
import { logger } from '$logger/logger.ts';
import { loadAllOperations, validateOperations } from './ops.ts';
import {
disableDatabaseInstance,
databaseInstancesQueries
} from '$db/queries/databaseInstances.ts';
import type { PCDDatabase } from '$shared/pcd/types.ts';
import { triggerSyncs } from '$sync/processor.ts';
/**
* Stats returned from cache build
*/
export interface CacheBuildStats {
schema: number;
base: number;
tweaks: number;
user: number;
timing: number;
}
/**
* 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;
constructor(pcdPath: string, databaseInstanceId: number) {
this.pcdPath = pcdPath;
this.databaseInstanceId = databaseInstanceId;
}
/**
* Build the cache by executing all operations in layer order
* Returns stats about what was loaded
*/
async build(): Promise<CacheBuildStats> {
const startTime = performance.now();
try {
// 1. Create in-memory database
// Enable int64 mode to properly handle large integers (e.g., file sizes in bytes)
this.db = new Database(':memory:', { int64: true });
// 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();
// 3. Load all operations
const operations = await loadAllOperations(this.pcdPath);
validateOperations(operations);
// Count ops per layer
const stats: CacheBuildStats = {
schema: operations.filter((o) => o.layer === 'schema').length,
base: operations.filter((o) => o.layer === 'base').length,
tweaks: operations.filter((o) => o.layer === 'tweaks').length,
user: operations.filter((o) => o.layer === 'user').length,
timing: 0
};
// 4. Execute operations in order
for (const operation of operations) {
try {
this.db.exec(operation.sql);
} catch (error) {
throw new Error(
`Failed to execute operation ${operation.filename} in ${operation.layer} layer: ${error}`
);
}
}
this.built = true;
stats.timing = Math.round(performance.now() - startTime);
return stats;
} catch (error) {
await logger.error('Failed to build PCD cache', {
source: 'PCDCache',
meta: { error: String(error), databaseInstanceId: this.databaseInstanceId }
});
// Disable the database instance
await disableDatabaseInstance(this.databaseInstanceId);
// Clean up
this.close();
throw error;
}
}
/**
* Register SQL helper functions (qp, cf, dp, tag)
*/
private registerHelperFunctions(): void {
if (!this.db) return;
// qp(name) - Quality profile lookup by name
this.db.function('qp', (name: string) => {
const result = this.db!.prepare('SELECT id FROM quality_profiles WHERE name = ?').get(
name
) as { id: number } | undefined;
if (!result) {
throw new Error(`Quality profile not found: ${name}`);
}
return result.id;
});
// cf(name) - Custom format lookup by name
this.db.function('cf', (name: string) => {
const result = this.db!.prepare('SELECT id FROM custom_formats WHERE name = ?').get(name) as
| { id: number }
| undefined;
if (!result) {
throw new Error(`Custom format not found: ${name}`);
}
return result.id;
});
// dp(name) - Delay profile lookup by name
this.db.function('dp', (name: string) => {
const result = this.db!.prepare('SELECT id FROM delay_profiles WHERE name = ?').get(name) as
| { id: number }
| undefined;
if (!result) {
throw new Error(`Delay profile not found: ${name}`);
}
return result.id;
});
// tag(name) - Tag lookup by name (creates if not exists)
this.db.function('tag', (name: string) => {
const result = this.db!.prepare('SELECT id FROM tags WHERE name = ?').get(name) as
| { id: number }
| undefined;
if (!result) {
throw new Error(`Tag not found: ${name}`);
}
return result.id;
});
}
/**
* Check if cache is built and ready
*/
isBuilt(): boolean {
return this.built && this.db !== null;
}
/**
* Close the database connection
*/
close(): void {
if (this.kysely) {
this.kysely.destroy();
this.kysely = null;
}
if (this.db) {
this.db.close();
this.db = null;
}
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
// ============================================================================
/**
* Execute a raw SQL query and return all rows
* Use this in your query functions in pcd/queries/*.ts
*/
query<T = unknown>(
sql: string,
...params: (string | number | null | boolean | Uint8Array)[]
): T[] {
if (!this.isBuilt()) {
throw new Error('Cache not built');
}
return this.db!.prepare(sql).all(...params) as T[];
}
/**
* Execute a raw SQL query and return a single row
* Use this in your query functions in pcd/queries/*.ts
*/
queryOne<T = unknown>(
sql: string,
...params: (string | number | null | boolean | Uint8Array)[]
): T | undefined {
if (!this.isBuilt()) {
throw new Error('Cache not built');
}
return this.db!.prepare(sql).get(...params) as T | undefined;
}
/**
* Validate SQL statements by doing a dry-run in a transaction
* Returns null if valid, or an error message if invalid
*
* This is a safety check before writing operations to files.
* It catches FK violations, constraint errors, etc.
*/
validateSql(sqlStatements: string[]): { valid: boolean; error?: string } {
if (!this.isBuilt()) {
return { valid: false, error: 'Cache not built' };
}
try {
// Start a savepoint (nested transaction)
this.db!.exec('SAVEPOINT validation_check');
try {
// Try to execute each statement
for (const sql of sqlStatements) {
this.db!.exec(sql);
}
// All statements executed successfully
return { valid: true };
} finally {
// Always rollback - this is just a validation check
this.db!.exec('ROLLBACK TO SAVEPOINT validation_check');
this.db!.exec('RELEASE SAVEPOINT validation_check');
}
} catch (error) {
// Parse the error to provide a helpful message
const errorStr = String(error);
// Common SQLite constraint errors
if (errorStr.includes('FOREIGN KEY constraint failed')) {
return {
valid: false,
error: `Foreign key constraint failed - referenced entity does not exist. ${errorStr}`
};
}
if (errorStr.includes('UNIQUE constraint failed')) {
return {
valid: false,
error: `Unique constraint failed - duplicate entry. ${errorStr}`
};
}
if (errorStr.includes('NOT NULL constraint failed')) {
return {
valid: false,
error: `Required field is missing. ${errorStr}`
};
}
if (errorStr.includes('CHECK constraint failed')) {
return {
valid: false,
error: `Value validation failed. ${errorStr}`
};
}
return {
valid: false,
error: `Database validation failed: ${errorStr}`
};
}
}
}
// ============================================================================
// MODULE-LEVEL REGISTRY AND FUNCTIONS
// ============================================================================
/**
* Cache registry - maps database instance ID to PCDCache
*/
const caches = new Map<number, PCDCache>();
/**
* File watchers - maps database instance ID to watcher
*/
const watchers = new Map<number, Deno.FsWatcher>();
/**
* Debounce timers - maps "databaseInstanceId:pcdPath" to timer
*/
const debounceTimers = new Map<string, number>();
/**
* Debounce delay in milliseconds
*/
const DEBOUNCE_DELAY = 500;
/**
* Compile a PCD into an in-memory cache
* Returns build stats for logging
*/
export async function compile(
pcdPath: string,
databaseInstanceId: number
): Promise<CacheBuildStats> {
// Stop any existing watchers
stopWatch(databaseInstanceId);
// Close existing cache if present
const existing = caches.get(databaseInstanceId);
if (existing) {
existing.close();
}
// Create and build new cache
const cache = new PCDCache(pcdPath, databaseInstanceId);
const stats = await cache.build();
// Store in registry
caches.set(databaseInstanceId, cache);
return stats;
}
/**
* Get a compiled cache by database instance ID
*/
export function getCache(databaseInstanceId: number): PCDCache | undefined {
return caches.get(databaseInstanceId);
}
/**
* Get all currently cached database instance IDs (for debugging)
*/
export function getCachedDatabaseIds(): number[] {
return Array.from(caches.keys());
}
/**
* Invalidate a cache (close and remove from registry)
*/
export function invalidate(databaseInstanceId: number): void {
const cache = caches.get(databaseInstanceId);
if (cache) {
cache.close();
caches.delete(databaseInstanceId);
}
// Stop file watcher and debounce timers
stopWatch(databaseInstanceId);
}
/**
* Invalidate all caches
*/
export function invalidateAll(): void {
const ids = Array.from(caches.keys());
for (const id of ids) {
invalidate(id);
}
}
// ============================================================================
// FILE WATCHING
// ============================================================================
/**
* Start watching PCD directories for changes
*/
export async function startWatch(pcdPath: string, databaseInstanceId: number): Promise<void> {
// Stop existing watcher if present
stopWatch(databaseInstanceId);
const pathsToWatch: string[] = [];
// Watch ops directory
const opsPath = `${pcdPath}/ops`;
try {
await Deno.stat(opsPath);
pathsToWatch.push(opsPath);
} catch {
// ops directory doesn't exist, skip
}
// Watch deps/schema/ops directory
const schemaOpsPath = `${pcdPath}/deps/schema/ops`;
try {
await Deno.stat(schemaOpsPath);
pathsToWatch.push(schemaOpsPath);
} catch {
// schema ops directory doesn't exist, skip
}
// Watch tweaks directory (optional)
const tweaksPath = `${pcdPath}/tweaks`;
try {
await Deno.stat(tweaksPath);
pathsToWatch.push(tweaksPath);
} catch {
// tweaks directory doesn't exist, that's ok
}
// Watch user_ops directory (create if doesn't exist)
const userOpsPath = `${pcdPath}/user_ops`;
try {
await Deno.mkdir(userOpsPath, { recursive: true });
} catch (error) {
if (!(error instanceof Deno.errors.AlreadyExists)) {
await logger.warn('Failed to create user_ops directory', {
source: 'PCDCache',
meta: { error: String(error), pcdPath }
});
}
}
pathsToWatch.push(userOpsPath);
if (pathsToWatch.length === 0) {
await logger.warn('No directories to watch for PCD', {
source: 'PCDCache',
meta: { pcdPath, databaseInstanceId }
});
return;
}
const watcher = Deno.watchFs(pathsToWatch);
watchers.set(databaseInstanceId, watcher);
// Process file system events in the background
(async () => {
try {
for await (const event of watcher) {
// Only rebuild on modify, create, or remove events
if (['modify', 'create', 'remove'].includes(event.kind)) {
// Only care about .sql files
const hasSqlFile = event.paths.some((path) => path.endsWith('.sql'));
if (!hasSqlFile) continue;
await logger.debug('File change detected, scheduling rebuild', {
source: 'PCDCache',
meta: { event: event.kind, paths: event.paths, databaseInstanceId }
});
// Debounce the rebuild
scheduleRebuild(pcdPath, databaseInstanceId);
}
}
} catch (error) {
// Watcher was closed or errored
if (error instanceof Deno.errors.BadResource) {
// This is expected when we close the watcher
return;
}
await logger.error('File watcher error', {
source: 'PCDCache',
meta: { error: String(error), databaseInstanceId }
});
}
})();
}
/**
* Stop watching a PCD for changes
*/
function stopWatch(databaseInstanceId: number, pcdPath?: string): void {
const watcher = watchers.get(databaseInstanceId);
if (watcher) {
watcher.close();
watchers.delete(databaseInstanceId);
}
// Clear any pending debounce timer for this specific path
if (pcdPath) {
const timerKey = `${databaseInstanceId}:${pcdPath}`;
const timer = debounceTimers.get(timerKey);
if (timer) {
clearTimeout(timer);
debounceTimers.delete(timerKey);
}
} else {
// Clear all timers for this databaseInstanceId (fallback)
for (const [key, timer] of debounceTimers.entries()) {
if (key.startsWith(`${databaseInstanceId}:`)) {
clearTimeout(timer);
debounceTimers.delete(key);
}
}
}
}
/**
* Schedule a rebuild with debouncing
*/
function scheduleRebuild(pcdPath: string, databaseInstanceId: number): void {
const timerKey = `${databaseInstanceId}:${pcdPath}`;
// Clear existing timer for this specific path
const existingTimer = debounceTimers.get(timerKey);
if (existingTimer) {
clearTimeout(existingTimer);
}
// Schedule new rebuild
const timer = setTimeout(async () => {
try {
const stats = await compile(pcdPath, databaseInstanceId);
// Restart watch after rebuild (compile clears watchers)
await startWatch(pcdPath, databaseInstanceId);
// Get database name for logging
const instance = databaseInstancesQueries.getById(databaseInstanceId);
const name = instance?.name ?? `ID:${databaseInstanceId}`;
await logger.debug(`Rebuild cache "${name}"`, {
source: 'PCDCache',
meta: {
schema: stats.schema,
base: stats.base,
tweaks: stats.tweaks,
user: stats.user,
timing: `${stats.timing}ms`
}
});
// Trigger arr syncs for configs with on_change trigger
await triggerSyncs({ event: 'on_change', databaseId: databaseInstanceId });
} catch (error) {
await logger.error('Failed to rebuild cache', {
source: 'PCDCache',
meta: { error: String(error), databaseInstanceId, pcdPath }
});
}
debounceTimers.delete(timerKey);
}, DEBOUNCE_DELAY) as unknown as number;
debounceTimers.set(timerKey, timer);
}

View File

@@ -0,0 +1,77 @@
/**
* PCD Error Classes
* Custom error types for the PCD system
*/
/**
* Base error class for PCD-related errors
*/
export class PCDError extends Error {
constructor(message: string) {
super(message);
this.name = 'PCDError';
}
}
/**
* Error during cache building
*/
export class CacheBuildError extends PCDError {
constructor(
message: string,
public readonly databaseInstanceId?: number
) {
super(message);
this.name = 'CacheBuildError';
}
}
/**
* Error during operation execution
*/
export class OperationError extends PCDError {
constructor(
message: string,
public readonly operation?: string,
public readonly layer?: string
) {
super(message);
this.name = 'OperationError';
}
}
/**
* Error during SQL validation
*/
export class ValidationError extends PCDError {
constructor(
message: string,
public readonly sql?: string[]
) {
super(message);
this.name = 'ValidationError';
}
}
/**
* Error during dependency resolution
*/
export class DependencyError extends PCDError {
constructor(
message: string,
public readonly dependency?: string
) {
super(message);
this.name = 'DependencyError';
}
}
/**
* Error during manifest validation
*/
export class ManifestValidationError extends PCDError {
constructor(message: string) {
super(message);
this.name = 'ManifestValidationError';
}
}

View File

@@ -5,27 +5,14 @@
import { Git, clone, type GitStatus, type UpdateInfo } from '$utils/git/index.ts';
import { databaseInstancesQueries } from '$db/queries/databaseInstances.ts';
import type { DatabaseInstance } from '$db/queries/databaseInstances.ts';
import { loadManifest, type Manifest } from './manifest.ts';
import { getPCDPath } from './paths.ts';
import { processDependencies, syncDependencies } from './deps.ts';
import { compile, invalidate, startWatch, getCache } from './cache.ts';
import { loadManifest, type Manifest } from '../manifest/manifest.ts';
import { getPCDPath } from '../operations/loader.ts';
import { processDependencies, syncDependencies, validateDependencies } from '../git/dependencies.ts';
import { compile, invalidate } from '../database/compiler.ts';
import { getCache } from '../database/registry.ts';
import { logger } from '$logger/logger.ts';
import { triggerSyncs } from '$sync/processor.ts';
export interface LinkOptions {
repositoryUrl: string;
name: string;
branch?: string;
syncStrategy?: number;
autoPull?: boolean;
personalAccessToken?: string;
}
export interface SyncResult {
success: boolean;
commitsBehind: number;
error?: string;
}
import type { LinkOptions, SyncResult } from './types.ts';
/**
* PCD Manager - Manages the lifecycle of Profilarr Compliant Databases
@@ -81,11 +68,10 @@ class PCDManager {
throw new Error('Failed to retrieve created database instance');
}
// Compile cache and start watching (only if enabled)
// Compile cache (only if enabled)
if (instance.enabled) {
try {
const stats = await compile(localPath, id);
await startWatch(localPath, id);
await logger.debug(`Cache compiled for "${options.name}"`, {
source: 'PCDManager',
@@ -175,11 +161,10 @@ class PCDManager {
// Update last_synced_at
databaseInstancesQueries.updateSyncedAt(id);
// Recompile cache and restart watching (only if enabled)
// Recompile cache (only if enabled)
if (instance.enabled) {
try {
await compile(instance.local_path, id);
await startWatch(instance.local_path, id);
} catch (error) {
await logger.error('Failed to recompile PCD cache after sync', {
source: 'PCDManager',
@@ -290,6 +275,18 @@ class PCDManager {
const instances = databaseInstancesQueries.getAll();
const enabledInstances = instances.filter((instance) => instance.enabled);
// Validate dependencies for all instances first
for (const instance of enabledInstances) {
try {
await validateDependencies(instance.local_path);
} catch (error) {
await logger.error(`Failed to validate dependencies for "${instance.name}"`, {
source: 'PCDManager',
meta: { error: String(error), databaseId: instance.id }
});
}
}
// Collect results for aggregate logging
const results: Array<{
name: string;
@@ -300,11 +297,10 @@ class PCDManager {
error?: string;
}> = [];
// Compile and watch all enabled instances
// Compile all enabled instances
for (const instance of enabledInstances) {
try {
const stats = await compile(instance.local_path, instance.id);
await startWatch(instance.local_path, instance.id);
results.push({
name: instance.name,

View File

@@ -0,0 +1,127 @@
/**
* PCD Core Types
* Consolidated type definitions for the PCD system
*/
import type { CompiledQuery } from 'kysely';
// ============================================================================
// OPERATION TYPES
// ============================================================================
/**
* Which layer an operation belongs to
*/
export type OperationLayer = 'base' | 'user';
/**
* Type of operation being performed
*/
export type OperationType = 'create' | 'update' | 'delete';
/**
* Writable layers (excludes schema which is read-only)
*/
export type WritableLayer = 'base' | 'tweaks' | 'user';
/**
* A loaded SQL operation from disk
*/
export interface Operation {
filename: string;
filepath: string;
sql: string;
order: number;
layer: 'schema' | 'base' | 'tweaks' | 'user';
}
/**
* Metadata for an operation - used for optimization and tracking
*/
export interface OperationMetadata {
/** The type of operation */
operation: OperationType;
/** The entity type (e.g., 'delay_profile', 'quality_profile') */
entity: string;
/** The entity name (current name for create/update, name being deleted for delete) */
name: string;
/** Previous name if this is a rename operation */
previousName?: string;
}
// ============================================================================
// CACHE TYPES
// ============================================================================
/**
* Stats returned from cache build
*/
export interface CacheBuildStats {
schema: number;
base: number;
tweaks: number;
user: number;
timing: number;
}
/**
* Result of SQL validation
*/
export interface ValidationResult {
valid: boolean;
error?: string;
}
// ============================================================================
// WRITER TYPES
// ============================================================================
/**
* Options for writing an operation
*/
export interface WriteOptions {
/** The database instance ID */
databaseId: number;
/** Which layer to write to */
layer: OperationLayer;
/** Description for the operation (used in filename) */
description: string;
/** The compiled Kysely queries to write */
queries: CompiledQuery[];
/** Metadata for optimization and tracking */
metadata?: OperationMetadata;
}
/**
* Result of a write operation
*/
export interface WriteResult {
success: boolean;
filepath?: string;
error?: string;
}
// ============================================================================
// MANAGER TYPES
// ============================================================================
/**
* Options for linking a new PCD repository
*/
export interface LinkOptions {
repositoryUrl: string;
name: string;
branch?: string;
syncStrategy?: number;
autoPull?: boolean;
personalAccessToken?: string;
}
/**
* Result of syncing a PCD repository
*/
export interface SyncResult {
success: boolean;
commitsBehind: number;
error?: string;
}

View File

@@ -0,0 +1,281 @@
/**
* PCD Cache - In-memory compiled view of PCD operations
*/
import { Database } from '@jsr/db__sqlite';
import { Kysely } from 'kysely';
// @ts-ignore - Deno JSR import not recognized by svelte-check
import { DenoSqlite3Dialect } from '@soapbox/kysely-deno-sqlite';
import { logger } from '$logger/logger.ts';
import { loadAllOperations, validateOperations } from '../operations/loader.ts';
import { disableDatabaseInstance } from '$db/queries/databaseInstances.ts';
import type { PCDDatabase } from '$shared/pcd/types.ts';
import type { CacheBuildStats, ValidationResult } from '../core/types.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;
constructor(pcdPath: string, databaseInstanceId: number) {
this.pcdPath = pcdPath;
this.databaseInstanceId = databaseInstanceId;
}
/**
* Build the cache by executing all operations in layer order
* Returns stats about what was loaded
*/
async build(): Promise<CacheBuildStats> {
const startTime = performance.now();
try {
// 1. Create in-memory database
// Enable int64 mode to properly handle large integers (e.g., file sizes in bytes)
this.db = new Database(':memory:', { int64: true });
// 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();
// 3. Load all operations
const operations = await loadAllOperations(this.pcdPath);
validateOperations(operations);
// Count ops per layer
const stats: CacheBuildStats = {
schema: operations.filter((o) => o.layer === 'schema').length,
base: operations.filter((o) => o.layer === 'base').length,
tweaks: operations.filter((o) => o.layer === 'tweaks').length,
user: operations.filter((o) => o.layer === 'user').length,
timing: 0
};
// 4. Execute operations in order
for (const operation of operations) {
try {
this.db.exec(operation.sql);
} catch (error) {
throw new Error(
`Failed to execute operation ${operation.filename} in ${operation.layer} layer: ${error}`
);
}
}
this.built = true;
stats.timing = Math.round(performance.now() - startTime);
return stats;
} catch (error) {
await logger.error('Failed to build PCD cache', {
source: 'PCDCache',
meta: { error: String(error), databaseInstanceId: this.databaseInstanceId }
});
// Disable the database instance
await disableDatabaseInstance(this.databaseInstanceId);
// Clean up
this.close();
throw error;
}
}
/**
* Register SQL helper functions (qp, cf, dp, tag)
*/
private registerHelperFunctions(): void {
if (!this.db) return;
// qp(name) - Quality profile lookup by name
this.db.function('qp', (name: string) => {
const result = this.db!.prepare('SELECT id FROM quality_profiles WHERE name = ?').get(
name
) as { id: number } | undefined;
if (!result) {
throw new Error(`Quality profile not found: ${name}`);
}
return result.id;
});
// cf(name) - Custom format lookup by name
this.db.function('cf', (name: string) => {
const result = this.db!.prepare('SELECT id FROM custom_formats WHERE name = ?').get(name) as
| { id: number }
| undefined;
if (!result) {
throw new Error(`Custom format not found: ${name}`);
}
return result.id;
});
// dp(name) - Delay profile lookup by name
this.db.function('dp', (name: string) => {
const result = this.db!.prepare('SELECT id FROM delay_profiles WHERE name = ?').get(name) as
| { id: number }
| undefined;
if (!result) {
throw new Error(`Delay profile not found: ${name}`);
}
return result.id;
});
// tag(name) - Tag lookup by name (creates if not exists)
this.db.function('tag', (name: string) => {
const result = this.db!.prepare('SELECT id FROM tags WHERE name = ?').get(name) as
| { id: number }
| undefined;
if (!result) {
throw new Error(`Tag not found: ${name}`);
}
return result.id;
});
}
/**
* Check if cache is built and ready
*/
isBuilt(): boolean {
return this.built && this.db !== null;
}
/**
* Close the database connection
*/
close(): void {
if (this.kysely) {
this.kysely.destroy();
this.kysely = null;
}
if (this.db) {
this.db.close();
this.db = null;
}
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
// ============================================================================
/**
* Execute a raw SQL query and return all rows
* Use this in your query functions in pcd/queries/*.ts
*/
query<T = unknown>(
sql: string,
...params: (string | number | null | boolean | Uint8Array)[]
): T[] {
if (!this.isBuilt()) {
throw new Error('Cache not built');
}
return this.db!.prepare(sql).all(...params) as T[];
}
/**
* Execute a raw SQL query and return a single row
* Use this in your query functions in pcd/queries/*.ts
*/
queryOne<T = unknown>(
sql: string,
...params: (string | number | null | boolean | Uint8Array)[]
): T | undefined {
if (!this.isBuilt()) {
throw new Error('Cache not built');
}
return this.db!.prepare(sql).get(...params) as T | undefined;
}
/**
* Validate SQL statements by doing a dry-run in a transaction
* Returns null if valid, or an error message if invalid
*
* This is a safety check before writing operations to files.
* It catches FK violations, constraint errors, etc.
*/
validateSql(sqlStatements: string[]): ValidationResult {
if (!this.isBuilt()) {
return { valid: false, error: 'Cache not built' };
}
try {
// Start a savepoint (nested transaction)
this.db!.exec('SAVEPOINT validation_check');
try {
// Try to execute each statement
for (const sql of sqlStatements) {
this.db!.exec(sql);
}
// All statements executed successfully
return { valid: true };
} finally {
// Always rollback - this is just a validation check
this.db!.exec('ROLLBACK TO SAVEPOINT validation_check');
this.db!.exec('RELEASE SAVEPOINT validation_check');
}
} catch (error) {
// Parse the error to provide a helpful message
const errorStr = String(error);
// Common SQLite constraint errors
if (errorStr.includes('FOREIGN KEY constraint failed')) {
return {
valid: false,
error: `Foreign key constraint failed - referenced entity does not exist. ${errorStr}`
};
}
if (errorStr.includes('UNIQUE constraint failed')) {
return {
valid: false,
error: `Unique constraint failed - duplicate entry. ${errorStr}`
};
}
if (errorStr.includes('NOT NULL constraint failed')) {
return {
valid: false,
error: `Required field is missing. ${errorStr}`
};
}
if (errorStr.includes('CHECK constraint failed')) {
return {
valid: false,
error: `Value validation failed. ${errorStr}`
};
}
return {
valid: false,
error: `Database validation failed: ${errorStr}`
};
}
}
}

View File

@@ -0,0 +1,53 @@
/**
* PCD Database Compiler
* Handles compiling and invalidating PCD caches
*/
import { PCDCache } from './cache.ts';
import { setCache, getCache, deleteCache, getCachedDatabaseIds } from './registry.ts';
import type { CacheBuildStats } from '../core/types.ts';
/**
* Compile a PCD into an in-memory cache
* Returns build stats for logging
*/
export async function compile(
pcdPath: string,
databaseInstanceId: number
): Promise<CacheBuildStats> {
// Close existing cache if present
const existing = getCache(databaseInstanceId);
if (existing) {
existing.close();
}
// Create and build new cache
const cache = new PCDCache(pcdPath, databaseInstanceId);
const stats = await cache.build();
// Store in registry
setCache(databaseInstanceId, cache);
return stats;
}
/**
* Invalidate a cache (close and remove from registry)
*/
export function invalidate(databaseInstanceId: number): void {
const cache = getCache(databaseInstanceId);
if (cache) {
cache.close();
deleteCache(databaseInstanceId);
}
}
/**
* Invalidate all caches
*/
export function invalidateAll(): void {
const ids = getCachedDatabaseIds();
for (const id of ids) {
invalidate(id);
}
}

View File

@@ -0,0 +1,56 @@
/**
* PCD Cache Registry
* Manages the global registry of compiled PCD caches
*/
import type { PCDCache } from './cache.ts';
/**
* Cache registry - maps database instance ID to PCDCache
*/
const caches = new Map<number, PCDCache>();
/**
* Set a cache in the registry
*/
export function setCache(databaseInstanceId: number, cache: PCDCache): void {
caches.set(databaseInstanceId, cache);
}
/**
* Get a compiled cache by database instance ID
*/
export function getCache(databaseInstanceId: number): PCDCache | undefined {
return caches.get(databaseInstanceId);
}
/**
* Check if a cache exists for a database instance
*/
export function hasCache(databaseInstanceId: number): boolean {
return caches.has(databaseInstanceId);
}
/**
* Delete a cache from the registry
*/
export function deleteCache(databaseInstanceId: number): boolean {
return caches.delete(databaseInstanceId);
}
/**
* Get all currently cached database instance IDs (for debugging)
*/
export function getCachedDatabaseIds(): number[] {
return Array.from(caches.keys());
}
/**
* Clear all caches from the registry
*/
export function clearAllCaches(): void {
for (const cache of caches.values()) {
cache.close();
}
caches.clear();
}

View File

@@ -1,167 +0,0 @@
/**
* PCD Dependency Resolution
* Handles cloning and managing PCD dependencies
*/
import { Git, clone } from '$utils/git/index.ts';
import { loadManifest } from './manifest.ts';
/**
* Extract repository name from GitHub URL
* https://github.com/Dictionarry-Hub/schema -> schema
*/
function getRepoName(repoUrl: string): string {
const parts = repoUrl.split('/');
return parts[parts.length - 1];
}
/**
* Get dependency path
*/
function getDependencyPath(pcdPath: string, repoName: string): string {
return `${pcdPath}/deps/${repoName}`;
}
/**
* Clone and checkout a single dependency
*/
async function cloneDependency(pcdPath: string, repoUrl: string, version: string): Promise<void> {
const repoName = getRepoName(repoUrl);
const depPath = getDependencyPath(pcdPath, repoName);
// Clone the dependency repository
await clone(repoUrl, depPath);
// Checkout the specific version tag
const git = new Git(depPath);
await git.checkout(version);
// Clean up dependency - keep only ops folder and pcd.json
const keepItems = new Set(['ops', 'pcd.json']);
for await (const entry of Deno.readDir(depPath)) {
if (!keepItems.has(entry.name)) {
const itemPath = `${depPath}/${entry.name}`;
await Deno.remove(itemPath, { recursive: true });
}
}
}
/**
* Process all dependencies for a PCD
* Clones dependencies and validates their manifests
*/
export async function processDependencies(pcdPath: string): Promise<void> {
// Load the PCD's manifest
const manifest = await loadManifest(pcdPath);
// Skip if no dependencies
if (!manifest.dependencies || Object.keys(manifest.dependencies).length === 0) {
return;
}
// Create deps directory
const depsDir = `${pcdPath}/deps`;
await Deno.mkdir(depsDir, { recursive: true });
// Process each dependency
for (const [repoUrl, version] of Object.entries(manifest.dependencies)) {
// Clone and checkout the dependency
await cloneDependency(pcdPath, repoUrl, version);
// Validate the dependency's manifest
const repoName = getRepoName(repoUrl);
const depPath = getDependencyPath(pcdPath, repoName);
await loadManifest(depPath);
// TODO (post-2.0): Recursively process nested dependencies
// For now, we only support one level of dependencies
}
}
/**
* Get the installed version of a dependency from its manifest
*/
async function getInstalledVersion(pcdPath: string, repoName: string): Promise<string | null> {
const depManifestPath = `${pcdPath}/deps/${repoName}/pcd.json`;
try {
const content = await Deno.readTextFile(depManifestPath);
const manifest = JSON.parse(content);
return manifest.version ?? null;
} catch {
return null;
}
}
/**
* Sync dependencies - update any that have changed versions in the manifest
* Called after pulling updates to ensure dependencies match manifest requirements
*/
export async function syncDependencies(pcdPath: string): Promise<void> {
const manifest = await loadManifest(pcdPath);
if (!manifest.dependencies || Object.keys(manifest.dependencies).length === 0) {
return;
}
// Ensure deps directory exists
const depsDir = `${pcdPath}/deps`;
await Deno.mkdir(depsDir, { recursive: true });
for (const [repoUrl, requiredVersion] of Object.entries(manifest.dependencies)) {
const repoName = getRepoName(repoUrl);
const depPath = getDependencyPath(pcdPath, repoName);
const installedVersion = await getInstalledVersion(pcdPath, repoName);
if (installedVersion === requiredVersion) {
// Already at correct version, skip
continue;
}
// Version changed or not installed - remove old and clone new
try {
await Deno.remove(depPath, { recursive: true });
} catch {
// Didn't exist, that's fine
}
// Clone and checkout the new version
await cloneDependency(pcdPath, repoUrl, requiredVersion);
// Validate the dependency's manifest
await loadManifest(depPath);
}
}
/**
* Check if all dependencies are present and valid
*/
export async function validateDependencies(pcdPath: string): Promise<boolean> {
try {
const manifest = await loadManifest(pcdPath);
// If no dependencies, validation passes
if (!manifest.dependencies || Object.keys(manifest.dependencies).length === 0) {
return true;
}
for (const [repoUrl] of Object.entries(manifest.dependencies)) {
const repoName = getRepoName(repoUrl);
const depPath = getDependencyPath(pcdPath, repoName);
// Check if dependency directory exists
try {
await Deno.stat(depPath);
} catch {
return false;
}
// Validate dependency manifest
await loadManifest(depPath);
}
return true;
} catch {
return false;
}
}

View File

@@ -2,7 +2,7 @@
* Custom format condition read queries for test evaluation
*/
import type { PCDCache } from '$pcd/cache.ts';
import type { PCDCache } from '$pcd/index.ts';
import type { ConditionData, ConditionListItem, CustomFormatWithConditions } from '$shared/pcd/display.ts';
/**

View File

@@ -7,8 +7,8 @@
* - Updating existing conditions
*/
import type { PCDCache } from '$pcd/cache.ts';
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { ConditionData } from '$shared/pcd/display.ts';
import { logger } from '$logger/logger.ts';

View File

@@ -2,8 +2,8 @@
* Create a custom format operation
*/
import type { PCDCache } from '$pcd/cache.ts';
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
interface CreateCustomFormatInput {
name: string;

View File

@@ -2,8 +2,8 @@
* Delete a custom format operation
*/
import type { PCDCache } from '$pcd/cache.ts';
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
interface DeleteCustomFormatOptions {
databaseId: number;

View File

@@ -2,7 +2,7 @@
* Custom format general read queries
*/
import type { PCDCache } from '$pcd/cache.ts';
import type { PCDCache } from '$pcd/index.ts';
import type { CustomFormatGeneral } from '$shared/pcd/display.ts';
/**

View File

@@ -2,8 +2,8 @@
* Update custom format general information
*/
import type { PCDCache } from '$pcd/cache.ts';
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { CustomFormatGeneral } from '$shared/pcd/display.ts';
import { logger } from '$logger/logger.ts';

View File

@@ -2,7 +2,7 @@
* Custom format list queries
*/
import type { PCDCache } from '$pcd/cache.ts';
import type { PCDCache } from '$pcd/index.ts';
import type { Tag, CustomFormatTableRow, ConditionRef } from '$shared/pcd/display.ts';
/**

View File

@@ -2,7 +2,7 @@
* Create a custom format test operation
*/
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
interface CreateTestInput {
title: string;

View File

@@ -2,7 +2,7 @@
* Delete a custom format test operation
*/
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { CustomFormatTest } from '$shared/pcd/display.ts';
interface DeleteTestOptions {

View File

@@ -2,7 +2,7 @@
* Custom format test read queries
*/
import type { PCDCache } from '$pcd/cache.ts';
import type { PCDCache } from '$pcd/index.ts';
import type { CustomFormatBasic, CustomFormatTest } from '$shared/pcd/display.ts';
/**

View File

@@ -2,7 +2,7 @@
* Update a custom format test operation
*/
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { CustomFormatTest } from '$shared/pcd/display.ts';
interface UpdateTestInput {

View File

@@ -2,8 +2,8 @@
* Create a delay profile operation
*/
import type { PCDCache } from '../../cache.ts';
import { writeOperation, type OperationLayer } from '../../writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { PreferredProtocol } from '$shared/pcd/display.ts';
interface CreateDelayProfileInput {

View File

@@ -2,8 +2,8 @@
* Delete a delay profile operation
*/
import type { PCDCache } from '../../cache.ts';
import { writeOperation, type OperationLayer } from '../../writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { DelayProfilesRow } from '$shared/pcd/display.ts';
interface DeleteDelayProfileOptions {

View File

@@ -6,7 +6,7 @@
* Delay profile read operations
*/
import type { PCDCache } from '../../cache.ts';
import type { PCDCache } from '$pcd/index.ts';
import type { DelayProfilesRow, PreferredProtocol } from '$shared/pcd/display.ts';
/**

View File

@@ -2,8 +2,8 @@
* Update a delay profile operation
*/
import type { PCDCache } from '../../cache.ts';
import { writeOperation, type OperationLayer } from '../../writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { DelayProfilesRow, PreferredProtocol } from '$shared/pcd/display.ts';
import { logger } from '$logger/logger.ts';

View File

@@ -2,8 +2,8 @@
* Create media settings config operations
*/
import type { PCDCache } from '../../../cache.ts';
import { writeOperation, type OperationLayer } from '../../../writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { RadarrMediaSettingsRow } from '$shared/pcd/display.ts';
export interface CreateMediaSettingsInput {

View File

@@ -2,8 +2,8 @@
* Remove media settings config operations
*/
import type { PCDCache } from '../../../cache.ts';
import { writeOperation, type OperationLayer } from '../../../writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
export interface RemoveMediaSettingsOptions {
databaseId: number;

View File

@@ -2,7 +2,7 @@
* Media settings read operations (list and get)
*/
import type { PCDCache } from '../../../cache.ts';
import type { PCDCache } from '$pcd/index.ts';
import type { RadarrMediaSettingsRow, SonarrMediaSettingsRow, MediaSettingsListItem } from '$shared/pcd/display.ts';
export async function list(cache: PCDCache): Promise<MediaSettingsListItem[]> {

View File

@@ -2,8 +2,8 @@
* Update media settings config operations
*/
import type { PCDCache } from '../../../cache.ts';
import { writeOperation, type OperationLayer } from '../../../writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { RadarrMediaSettingsRow } from '$shared/pcd/display.ts';
export interface UpdateMediaSettingsInput {

View File

@@ -2,8 +2,8 @@
* Create naming config operations
*/
import type { PCDCache } from '../../../cache.ts';
import { writeOperation, type OperationLayer } from '../../../writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { RadarrNamingRow, SonarrNamingRow } from '$shared/pcd/display.ts';
import { colonReplacementToDb, multiEpisodeStyleToDb } from '$shared/pcd/mediaManagement.ts';

View File

@@ -2,8 +2,8 @@
* Remove naming config operations
*/
import type { PCDCache } from '../../../cache.ts';
import { writeOperation, type OperationLayer } from '../../../writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
export interface RemoveRadarrNamingOptions {
databaseId: number;

View File

@@ -2,7 +2,7 @@
* Naming read operations (list and get)
*/
import type { PCDCache } from '../../../cache.ts';
import type { PCDCache } from '$pcd/index.ts';
import type { RadarrNamingRow, SonarrNamingRow, NamingListItem } from '$shared/pcd/display.ts';
import { colonReplacementFromDb, multiEpisodeStyleFromDb } from '$shared/pcd/mediaManagement.ts';

View File

@@ -2,8 +2,8 @@
* Update naming config operations
*/
import type { PCDCache } from '../../../cache.ts';
import { writeOperation, type OperationLayer } from '../../../writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { RadarrNamingRow, SonarrNamingRow } from '$shared/pcd/display.ts';
import { colonReplacementToDb, multiEpisodeStyleToDb } from '$shared/pcd/mediaManagement.ts';

View File

@@ -2,8 +2,8 @@
* Quality definitions create operations
*/
import type { PCDCache } from '../../../cache.ts';
import { writeOperation, type OperationLayer } from '../../../writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { QualityDefinitionEntry } from '$shared/pcd/display.ts';
export interface CreateQualityDefinitionsInput {

View File

@@ -2,8 +2,8 @@
* Quality definitions remove operations
*/
import type { PCDCache } from '../../../cache.ts';
import { writeOperation, type OperationLayer } from '../../../writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
export interface RemoveQualityDefinitionsOptions {
databaseId: number;

View File

@@ -2,7 +2,7 @@
* Quality definitions read operations
*/
import type { PCDCache } from '../../../cache.ts';
import type { PCDCache } from '$pcd/index.ts';
import type { ArrType } from '$shared/pcd/types.ts';
import type {
QualityDefinitionListItem,

View File

@@ -2,8 +2,8 @@
* Quality definitions update operations
*/
import type { PCDCache } from '../../../cache.ts';
import { writeOperation, type OperationLayer } from '../../../writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { QualityDefinitionEntry } from '$shared/pcd/display.ts';
export interface UpdateQualityDefinitionsInput {

View File

@@ -2,8 +2,8 @@
* Create a quality profile operation
*/
import type { PCDCache } from '$pcd/cache.ts';
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
// ============================================================================
// Input types

View File

@@ -2,8 +2,8 @@
* Delete a quality profile operation
*/
import type { PCDCache } from '$pcd/cache.ts';
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
// ============================================================================
// Input types

View File

@@ -2,8 +2,8 @@
* Create test entity operation
*/
import type { PCDCache } from '$pcd/cache.ts';
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
interface CreateEntityInput {
type: 'movie' | 'series';

View File

@@ -2,8 +2,8 @@
* Delete test entity operation
*/
import type { PCDCache } from '$pcd/cache.ts';
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
interface DeleteEntityOptions {
databaseId: number;

View File

@@ -2,7 +2,7 @@
* Entity test read queries
*/
import type { PCDCache } from '$pcd/cache.ts';
import type { PCDCache } from '$pcd/index.ts';
import type { TestEntity } from '$shared/pcd/display.ts';
/**

View File

@@ -2,8 +2,8 @@
* Create test release operations
*/
import type { PCDCache } from '$pcd/cache.ts';
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
interface CreateReleaseInput {
entityType: 'movie' | 'series';

View File

@@ -2,8 +2,8 @@
* Delete test release operation
*/
import type { PCDCache } from '$pcd/cache.ts';
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
interface DeleteReleaseOptions {
databaseId: number;

View File

@@ -2,8 +2,8 @@
* Update test release operation
*/
import type { PCDCache } from '$pcd/cache.ts';
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
interface UpdateReleaseInput {
id: number;

View File

@@ -2,7 +2,7 @@
* Quality profile general queries
*/
import type { PCDCache } from '$pcd/cache.ts';
import type { PCDCache } from '$pcd/index.ts';
import type { QualityProfileGeneral, QualityProfileLanguages } from '$shared/pcd/display.ts';
/**

View File

@@ -2,8 +2,8 @@
* Update quality profile general information and languages
*/
import type { PCDCache } from '$pcd/cache.ts';
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { QualityProfileGeneral } from '$shared/pcd/display.ts';
import { logger } from '$logger/logger.ts';

View File

@@ -2,7 +2,7 @@
* Quality profile list queries
*/
import type { PCDCache } from '$pcd/cache.ts';
import type { PCDCache } from '$pcd/index.ts';
import type {
Tag,
QualityProfileTableRow,

View File

@@ -2,7 +2,7 @@
* Quality profile qualities queries
*/
import type { PCDCache } from '$pcd/cache.ts';
import type { PCDCache } from '$pcd/index.ts';
import type { QualitiesPageData, OrderedItem, QualitiesGroup } from '$shared/pcd/display.ts';
/**

View File

@@ -2,8 +2,8 @@
* Update quality profile qualities
*/
import type { PCDCache } from '$pcd/cache.ts';
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { OrderedItem } from '$shared/pcd/display.ts';
import { logger } from '$logger/logger.ts';

View File

@@ -2,7 +2,7 @@
* Quality profile scoring queries
*/
import type { PCDCache } from '$pcd/cache.ts';
import type { PCDCache } from '$pcd/index.ts';
import type { QualityProfileScoring, ProfileCfScores, AllCfScoresResult } from '$shared/pcd/display.ts';
/**

View File

@@ -2,8 +2,8 @@
* Update quality profile scoring settings
*/
import type { PCDCache } from '$pcd/cache.ts';
import { writeOperation, type OperationLayer } from '$pcd/writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import { logger } from '$logger/logger.ts';
// ============================================================================

View File

@@ -2,8 +2,8 @@
* Create a regular expression operation
*/
import type { PCDCache } from '../../cache.ts';
import { writeOperation, type OperationLayer } from '../../writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
interface CreateRegularExpressionInput {
name: string;

View File

@@ -2,8 +2,8 @@
* Delete a regular expression operation
*/
import type { PCDCache } from '../../cache.ts';
import { writeOperation, type OperationLayer } from '../../writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { RegularExpressionWithTags } from '$shared/pcd/display.ts';
interface DeleteRegularExpressionOptions {

View File

@@ -2,7 +2,7 @@
* Regular expression read operations
*/
import type { PCDCache } from '../../cache.ts';
import type { PCDCache } from '$pcd/index.ts';
import type { Tag, RegularExpressionWithTags } from '$shared/pcd/display.ts';
/**

View File

@@ -2,8 +2,8 @@
* Update a regular expression operation
*/
import type { PCDCache } from '../../cache.ts';
import { writeOperation, type OperationLayer } from '../../writer.ts';
import type { PCDCache } from '$pcd/index.ts';
import { writeOperation, type OperationLayer } from '$pcd/index.ts';
import type { RegularExpressionWithTags } from '$shared/pcd/display.ts';
import { logger } from '$logger/logger.ts';

View File

@@ -0,0 +1,235 @@
/**
* PCD Dependency Resolution
* Handles cloning and managing PCD dependencies using git tags
*/
import { Git, clone } from '$utils/git/index.ts';
import { loadManifest } from '../manifest/manifest.ts';
import { logger } from '$logger/logger.ts';
/**
* Extract repository name from GitHub URL
* https://github.com/Dictionarry-Hub/schema -> schema
*/
function getRepoName(repoUrl: string): string {
const parts = repoUrl.split('/');
return parts[parts.length - 1];
}
/**
* Get dependency path
*/
function getDependencyPath(pcdPath: string, repoName: string): string {
return `${pcdPath}/deps/${repoName}`;
}
/**
* Check if a directory exists
*/
async function dirExists(path: string): Promise<boolean> {
try {
const stat = await Deno.stat(path);
return stat.isDirectory;
} catch {
return false;
}
}
/**
* Clone and checkout a dependency at a specific tag
* Keeps .git, ops, and pcd.json - removes everything else
*/
async function cloneDependency(pcdPath: string, repoUrl: string, version: string): Promise<void> {
const repoName = getRepoName(repoUrl);
const depPath = getDependencyPath(pcdPath, repoName);
// Clone the dependency repository
await clone(repoUrl, depPath);
// Checkout the specific version tag
const git = new Git(depPath);
await git.checkout(version);
// Clean up dependency - keep only .git, ops folder and pcd.json
const keepItems = new Set(['.git', 'ops', 'pcd.json']);
for await (const entry of Deno.readDir(depPath)) {
if (!keepItems.has(entry.name)) {
const itemPath = `${depPath}/${entry.name}`;
await Deno.remove(itemPath, { recursive: true });
}
}
}
/**
* Get the installed version of a dependency from its manifest
*/
async function getInstalledVersion(pcdPath: string, repoName: string): Promise<string | null> {
const depManifestPath = `${pcdPath}/deps/${repoName}/pcd.json`;
try {
const content = await Deno.readTextFile(depManifestPath);
const manifest = JSON.parse(content);
return manifest.version ?? null;
} catch {
return null;
}
}
/**
* Update a dependency to a new version using fetch + checkout
*/
async function updateDependency(depPath: string, version: string): Promise<void> {
const git = new Git(depPath);
await git.fetchTags();
await git.checkout(version);
}
/**
* Process all dependencies for a PCD (initial clone)
* Called when linking a new database
*/
export async function processDependencies(pcdPath: string): Promise<void> {
const manifest = await loadManifest(pcdPath);
if (!manifest.dependencies || Object.keys(manifest.dependencies).length === 0) {
return;
}
// Create deps directory
const depsDir = `${pcdPath}/deps`;
await Deno.mkdir(depsDir, { recursive: true });
for (const [repoUrl, version] of Object.entries(manifest.dependencies)) {
const repoName = getRepoName(repoUrl);
const depPath = getDependencyPath(pcdPath, repoName);
// Clone and checkout the dependency
await cloneDependency(pcdPath, repoUrl, version);
// Validate the dependency's manifest
await loadManifest(depPath);
await logger.debug(`Installed dependency ${repoName}@${version}`, {
source: 'PCDDependencies',
meta: { pcdPath, repoName, version }
});
}
}
/**
* Sync dependencies - update any that have changed versions
* Uses fetch + checkout instead of re-cloning
*/
export async function syncDependencies(pcdPath: string): Promise<void> {
const manifest = await loadManifest(pcdPath);
if (!manifest.dependencies || Object.keys(manifest.dependencies).length === 0) {
return;
}
const depsDir = `${pcdPath}/deps`;
await Deno.mkdir(depsDir, { recursive: true });
for (const [repoUrl, requiredVersion] of Object.entries(manifest.dependencies)) {
const repoName = getRepoName(repoUrl);
const depPath = getDependencyPath(pcdPath, repoName);
const installedVersion = await getInstalledVersion(pcdPath, repoName);
// Already at correct version
if (installedVersion === requiredVersion) {
continue;
}
// Check if dependency exists with .git folder
const hasGitFolder = await dirExists(`${depPath}/.git`);
if (hasGitFolder) {
// Fetch tags and checkout new version
await updateDependency(depPath, requiredVersion);
await logger.info(`Updated dependency ${repoName}: ${installedVersion} -> ${requiredVersion}`, {
source: 'PCDDependencies',
meta: { pcdPath, repoName, from: installedVersion, to: requiredVersion }
});
} else {
// No .git folder (legacy or corrupted) - re-clone
try {
await Deno.remove(depPath, { recursive: true });
} catch {
// Didn't exist
}
await cloneDependency(pcdPath, repoUrl, requiredVersion);
await logger.info(`Re-cloned dependency ${repoName}@${requiredVersion}`, {
source: 'PCDDependencies',
meta: { pcdPath, repoName, version: requiredVersion }
});
}
// Validate the dependency's manifest
await loadManifest(depPath);
}
}
/**
* Validate and fix dependencies on startup
* Ensures all deps exist and are at the correct version
*/
export async function validateDependencies(pcdPath: string): Promise<boolean> {
try {
const manifest = await loadManifest(pcdPath);
if (!manifest.dependencies || Object.keys(manifest.dependencies).length === 0) {
return true;
}
let allValid = true;
for (const [repoUrl, requiredVersion] of Object.entries(manifest.dependencies)) {
const repoName = getRepoName(repoUrl);
const depPath = getDependencyPath(pcdPath, repoName);
// Check if dependency exists
if (!(await dirExists(depPath))) {
await logger.warn(`Missing dependency ${repoName}, will install`, {
source: 'PCDDependencies',
meta: { pcdPath, repoName }
});
allValid = false;
continue;
}
// Check version
const installedVersion = await getInstalledVersion(pcdPath, repoName);
if (installedVersion !== requiredVersion) {
await logger.warn(`Dependency ${repoName} version mismatch: ${installedVersion} != ${requiredVersion}`, {
source: 'PCDDependencies',
meta: { pcdPath, repoName, installed: installedVersion, required: requiredVersion }
});
allValid = false;
}
// Validate manifest
try {
await loadManifest(depPath);
} catch {
await logger.warn(`Dependency ${repoName} has invalid manifest`, {
source: 'PCDDependencies',
meta: { pcdPath, repoName }
});
allValid = false;
}
}
// If any issues found, run sync to fix them
if (!allValid) {
await syncDependencies(pcdPath);
}
return true;
} catch (error) {
await logger.error('Failed to validate dependencies', {
source: 'PCDDependencies',
meta: { pcdPath, error: String(error) }
});
return false;
}
}

View File

@@ -0,0 +1,75 @@
/**
* PCD Public API
* Re-exports for external consumers
*/
// ============================================================================
// MANAGER
// ============================================================================
export { pcdManager } from './core/manager.ts';
// ============================================================================
// CACHE
// ============================================================================
export { PCDCache } from './database/cache.ts';
export { getCache, getCachedDatabaseIds } from './database/registry.ts';
export { compile, invalidate, invalidateAll } from './database/compiler.ts';
// ============================================================================
// WRITER
// ============================================================================
export { writeOperation, canWriteToBase } from './operations/writer.ts';
// ============================================================================
// MANIFEST
// ============================================================================
export { loadManifest, readManifest, validateManifest, writeManifest, readReadme, writeReadme } from './manifest/manifest.ts';
export type { Manifest } from './manifest/manifest.ts';
// ============================================================================
// DEPENDENCIES
// ============================================================================
export { processDependencies, syncDependencies, validateDependencies } from './git/dependencies.ts';
// ============================================================================
// OPERATIONS
// ============================================================================
export { loadAllOperations, loadOperationsFromDir, validateOperations, getPCDPath, getUserOpsPath, getBaseOpsPath } from './operations/loader.ts';
export { compiledQueryToSql, formatValue } from './operations/sql.ts';
// ============================================================================
// TYPES
// ============================================================================
export type {
CacheBuildStats,
Operation,
OperationLayer,
OperationType,
OperationMetadata,
WritableLayer,
WriteOptions,
WriteResult,
ValidationResult,
LinkOptions,
SyncResult
} from './core/types.ts';
// ============================================================================
// ERRORS
// ============================================================================
export {
PCDError,
CacheBuildError,
OperationError,
ValidationError,
DependencyError,
ManifestValidationError
} from './core/errors.ts';

View File

@@ -4,6 +4,7 @@
*/
import { logger } from '$logger/logger.ts';
import { ManifestValidationError } from '../core/errors.ts';
export interface Manifest {
name: string;
@@ -25,13 +26,6 @@ export interface Manifest {
};
}
export class ManifestValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ManifestValidationError';
}
}
/**
* Read manifest from a PCD repository
*/
@@ -165,3 +159,32 @@ export async function writeManifest(pcdPath: string, manifest: Manifest): Promis
meta: { path: pcdPath, manifest }
});
}
// ============================================================================
// README HELPERS (merged from readme.ts)
// ============================================================================
/**
* Read README from a PCD repository
*/
export async function readReadme(pcdPath: string): Promise<string | null> {
try {
return await Deno.readTextFile(`${pcdPath}/README.md`);
} catch {
return null;
}
}
/**
* Write README to a PCD repository
*/
export async function writeReadme(pcdPath: string, content: string): Promise<void> {
await Deno.writeTextFile(`${pcdPath}/README.md`, content);
await logger.info('Wrote README', {
source: 'PCDManifest',
meta: { path: pcdPath, content }
});
}
// Re-export error for convenience
export { ManifestValidationError };

View File

@@ -1,14 +1,10 @@
/**
* PCD Operations Loader
* Utilities for loading and managing SQL operations from PCD layers
*/
export interface Operation {
filename: string;
filepath: string;
sql: string;
order: number; // Extracted from numeric prefix
layer: 'schema' | 'base' | 'tweaks' | 'user';
}
import { config } from '$config';
import type { Operation } from '../core/types.ts';
/**
* Check if a path exists
@@ -69,7 +65,7 @@ export async function loadOperationsFromDir(
* "10.advanced.sql" -> 10
* "allow-DV.sql" -> Infinity (no prefix)
*/
function extractOrderFromFilename(filename: string): number {
export function extractOrderFromFilename(filename: string): number {
const match = filename.match(/^(\d+)\./);
if (match) {
return parseInt(match[1], 10);
@@ -110,20 +106,6 @@ export async function loadAllOperations(pcdPath: string): Promise<Operation[]> {
return allOperations;
}
/**
* Get the user ops directory path for a PCD
*/
export function getUserOpsPath(pcdPath: string): string {
return `${pcdPath}/user_ops`;
}
/**
* Get the base ops directory path for a PCD
*/
export function getBaseOpsPath(pcdPath: string): string {
return `${pcdPath}/ops`;
}
/**
* Validate that operations can be executed
* - Check for empty SQL
@@ -152,3 +134,28 @@ export function validateOperations(operations: Operation[]): void {
orders.add(op.order);
}
}
// ============================================================================
// PATH HELPERS
// ============================================================================
/**
* Get the filesystem path for a PCD repository
*/
export function getPCDPath(uuid: string): string {
return `${config.paths.databases}/${uuid}`;
}
/**
* Get the user ops directory path for a PCD
*/
export function getUserOpsPath(pcdPath: string): string {
return `${pcdPath}/user_ops`;
}
/**
* Get the base ops directory path for a PCD
*/
export function getBaseOpsPath(pcdPath: string): string {
return `${pcdPath}/ops`;
}

View File

@@ -0,0 +1,64 @@
/**
* PCD SQL Utilities
* SQL compilation and formatting utilities
*/
import type { CompiledQuery } from 'kysely';
/**
* Convert a compiled Kysely query to executable SQL
* Replaces ? placeholders with actual values
*
* Note: We can't use simple string.replace() because parameter values
* might contain '?' characters (e.g., regex patterns like '(?<=...)')
* which would get incorrectly replaced on subsequent iterations.
*/
export function compiledQueryToSql(compiled: CompiledQuery): string {
const sql = compiled.sql;
const params = compiled.parameters as unknown[];
if (params.length === 0) {
return sql;
}
// Build result by finding each ? placeholder and replacing with the next param
// We track our position to avoid replacing ? inside already-substituted values
const result: string[] = [];
let paramIndex = 0;
let i = 0;
while (i < sql.length) {
if (sql[i] === '?' && paramIndex < params.length) {
// Replace this placeholder with the formatted parameter value
result.push(formatValue(params[paramIndex]));
paramIndex++;
i++;
} else {
result.push(sql[i]);
i++;
}
}
return result.join('');
}
/**
* Format a value for SQL insertion
*/
export function formatValue(value: unknown): string {
if (value === null || value === undefined) {
return 'NULL';
}
if (typeof value === 'number') {
return String(value);
}
if (typeof value === 'boolean') {
return value ? '1' : '0';
}
if (typeof value === 'string') {
// Escape single quotes by doubling them
return `'${value.replace(/'/g, "''")}'`;
}
// For other types, convert to string and quote
return `'${String(value).replace(/'/g, "''")}'`;
}

View File

@@ -1,107 +1,16 @@
/**
* PCD Operation Writer - Write operations to PCD layers using Kysely
* PCD Operation Writer
* Write operations to PCD layers using Kysely
*/
import type { CompiledQuery } from 'kysely';
import { getBaseOpsPath, getUserOpsPath } from './ops.ts';
import { databaseInstancesQueries } from '$db/queries/databaseInstances.ts';
import { logger } from '$logger/logger.ts';
import { compile, getCache } from './cache.ts';
import { isFileUncommitted } from '$utils/git/status.ts';
export type OperationLayer = 'base' | 'user';
export type OperationType = 'create' | 'update' | 'delete';
/**
* Metadata for an operation - used for optimization and tracking
*/
export interface OperationMetadata {
/** The type of operation */
operation: OperationType;
/** The entity type (e.g., 'delay_profile', 'quality_profile') */
entity: string;
/** The entity name (current name for create/update, name being deleted for delete) */
name: string;
/** Previous name if this is a rename operation */
previousName?: string;
}
export interface WriteOptions {
/** The database instance ID */
databaseId: number;
/** Which layer to write to */
layer: OperationLayer;
/** Description for the operation (used in filename) */
description: string;
/** The compiled Kysely queries to write */
queries: CompiledQuery[];
/** Metadata for optimization and tracking */
metadata?: OperationMetadata;
}
export interface WriteResult {
success: boolean;
filepath?: string;
error?: string;
}
/**
* Convert a compiled Kysely query to executable SQL
* Replaces ? placeholders with actual values
*
* Note: We can't use simple string.replace() because parameter values
* might contain '?' characters (e.g., regex patterns like '(?<=...)')
* which would get incorrectly replaced on subsequent iterations.
*/
function compiledQueryToSql(compiled: CompiledQuery): string {
const sql = compiled.sql;
const params = compiled.parameters as unknown[];
if (params.length === 0) {
return sql;
}
// Build result by finding each ? placeholder and replacing with the next param
// We track our position to avoid replacing ? inside already-substituted values
const result: string[] = [];
let paramIndex = 0;
let i = 0;
while (i < sql.length) {
if (sql[i] === '?' && paramIndex < params.length) {
// Replace this placeholder with the formatted parameter value
result.push(formatValue(params[paramIndex]));
paramIndex++;
i++;
} else {
result.push(sql[i]);
i++;
}
}
return result.join('');
}
/**
* Format a value for SQL insertion
*/
function formatValue(value: unknown): string {
if (value === null || value === undefined) {
return 'NULL';
}
if (typeof value === 'number') {
return String(value);
}
if (typeof value === 'boolean') {
return value ? '1' : '0';
}
if (typeof value === 'string') {
// Escape single quotes by doubling them
return `'${value.replace(/'/g, "''")}'`;
}
// For other types, convert to string and quote
return `'${String(value).replace(/'/g, "''")}'`;
}
import { getBaseOpsPath, getUserOpsPath } from './loader.ts';
import { compiledQueryToSql } from './sql.ts';
import { compile } from '../database/compiler.ts';
import { getCache } from '../database/registry.ts';
import type { OperationLayer, OperationMetadata, OperationType, WriteOptions, WriteResult } from '../core/types.ts';
/**
* Get the next available operation number for a directory
@@ -390,3 +299,6 @@ export function canWriteToBase(databaseId: number): boolean {
const instance = databaseInstancesQueries.getById(databaseId);
return !!instance?.personal_access_token;
}
// Re-export types for convenience
export type { OperationLayer, OperationType, OperationMetadata, WriteOptions, WriteResult };

View File

@@ -1,19 +0,0 @@
/**
* Helper functions for PCD paths
*/
import { config } from '$config';
/**
* Get the filesystem path for a PCD repository
*/
export function getPCDPath(uuid: string): string {
return `${config.paths.databases}/${uuid}`;
}
/**
* Get the manifest file path for a PCD repository
*/
export function getManifestPath(uuid: string): string {
return `${getPCDPath(uuid)}/pcd.json`;
}

View File

@@ -1,28 +0,0 @@
/**
* PCD README Handler
* Handles reading and writing README.md files for PCD repositories
*/
import { logger } from '$logger/logger.ts';
/**
* Read README from a PCD repository
*/
export async function readReadme(pcdPath: string): Promise<string | null> {
try {
return await Deno.readTextFile(`${pcdPath}/README.md`);
} catch {
return null;
}
}
/**
* Write README to a PCD repository
*/
export async function writeReadme(pcdPath: string, content: string): Promise<void> {
await Deno.writeTextFile(`${pcdPath}/README.md`, content);
await logger.info('Wrote README', {
source: 'PCDReadme',
meta: { path: pcdPath, content }
});
}

View File

@@ -3,7 +3,7 @@
* Transforms PCD custom format data to arr API format
*/
import type { PCDCache } from '$pcd/cache.ts';
import type { PCDCache } from '$pcd/index.ts';
import {
type SyncArrType,
getSource,

View File

@@ -7,7 +7,7 @@
import { BaseSyncer, type SyncResult } from '../base.ts';
import { arrSyncQueries } from '$db/queries/arrSync.ts';
import { getCache } from '$pcd/cache.ts';
import { getCache } from '$pcd/index.ts';
import { get as getDelayProfile } from '$pcd/entities/delayProfiles/index.ts';
import type { DelayProfilesRow } from '$shared/pcd/display.ts';
import type { ArrDelayProfile } from '$arr/types.ts';

View File

@@ -16,7 +16,7 @@
import { BaseSyncer, type SyncResult } from '../base.ts';
import { arrSyncQueries } from '$db/queries/arrSync.ts';
import { getCache, type PCDCache } from '$pcd/cache.ts';
import { getCache, type PCDCache } from '$pcd/index.ts';
import { getRadarrByName as getRadarrMediaSettings, getSonarrByName as getSonarrMediaSettings } from '$pcd/entities/mediaManagement/media-settings/read.ts';
import { getRadarrByName as getRadarrNaming, getSonarrByName as getSonarrNaming } from '$pcd/entities/mediaManagement/naming/read.ts';
import { getRadarrByName as getRadarrQualityDefs, getSonarrByName as getSonarrQualityDefs } from '$pcd/entities/mediaManagement/quality-definitions/read.ts';

View File

@@ -13,7 +13,7 @@
import { BaseSyncer, type SyncResult } from '../base.ts';
import { arrSyncQueries } from '$db/queries/arrSync.ts';
import { getCache, getCachedDatabaseIds } from '$pcd/cache.ts';
import { getCache, getCachedDatabaseIds } from '$pcd/index.ts';
import { databaseInstancesQueries } from '$db/queries/databaseInstances.ts';
import { logger } from '$logger/logger.ts';
import type { SyncArrType } from '../mappings.ts';

View File

@@ -3,7 +3,7 @@
* Transforms PCD quality profile data to arr API format
*/
import type { PCDCache } from '$pcd/cache.ts';
import type { PCDCache } from '$pcd/index.ts';
import type {
ArrQualityProfileItem,
ArrQualityProfilePayload,

View File

@@ -19,6 +19,7 @@ export class Git {
// Repo commands
fetch = () => repo.fetch(this.repoPath);
fetchTags = () => repo.fetchTags(this.repoPath);
pull = () => repo.pull(this.repoPath);
push = () => repo.push(this.repoPath);
checkout = (branch: string) => repo.checkout(this.repoPath, branch);

View File

@@ -125,6 +125,13 @@ export async function fetch(repoPath: string): Promise<void> {
await execGitSafe(['fetch', '--quiet'], repoPath);
}
/**
* Fetch tags from remote
*/
export async function fetchTags(repoPath: string): Promise<void> {
await execGitSafe(['fetch', '--tags', '--quiet'], repoPath);
}
/**
* Pull from remote
*/

View File

@@ -1,7 +1,7 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { arrInstancesQueries } from '$db/queries/arrInstances.ts';
import { pcdManager } from '$pcd/pcd.ts';
import { pcdManager } from '$pcd/index.ts';
import * as qualityProfileQueries from '$pcd/entities/qualityProfiles/index.ts';
import { cache } from '$cache/cache.ts';
import { RadarrClient } from '$utils/arr/clients/radarr.ts';

View File

@@ -1,6 +1,6 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import { pcdManager } from '$pcd/index.ts';
/**
* GET /api/databases

View File

@@ -5,7 +5,7 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { pcdManager } from '$pcd/pcd.ts';
import { pcdManager } from '$pcd/index.ts';
import {
parseWithCacheBatch,
isParserHealthy,

View File

@@ -6,7 +6,7 @@ import { databaseInstancesQueries } from '$db/queries/databaseInstances.ts';
import { jobsQueries } from '$db/queries/jobs.ts';
import { backupSettingsQueries } from '$db/queries/backupSettings.ts';
import { appInfoQueries } from '$db/queries/appInfo.ts';
import { getCache } from '$pcd/cache.ts';
import { getCache } from '$pcd/index.ts';
import { config } from '$config';
type ComponentStatus = 'healthy' | 'degraded' | 'unhealthy';

View File

@@ -2,7 +2,7 @@ import { error, fail } from '@sveltejs/kit';
import type { ServerLoad, Actions } from '@sveltejs/kit';
import { arrInstancesQueries } from '$db/queries/arrInstances.ts';
import { arrSyncQueries, type SyncTrigger, type ProfileSelection } from '$db/queries/arrSync.ts';
import { pcdManager } from '$pcd/pcd.ts';
import { pcdManager } from '$pcd/index.ts';
import { logger } from '$logger/logger.ts';
import * as qualityProfileQueries from '$pcd/entities/qualityProfiles/index.ts';
import * as delayProfileQueries from '$pcd/entities/delayProfiles/index.ts';

View File

@@ -1,6 +1,6 @@
import { redirect } from '@sveltejs/kit';
import type { ServerLoad } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import { pcdManager } from '$pcd/index.ts';
export const load: ServerLoad = () => {
// Get all databases

View File

@@ -1,6 +1,6 @@
import { error } from '@sveltejs/kit';
import type { ServerLoad } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import { pcdManager } from '$pcd/index.ts';
import * as customFormatQueries from '$pcd/entities/customFormats/index.ts';
export const load: ServerLoad = async ({ params }) => {

View File

@@ -1,11 +1,11 @@
import { error, redirect, fail } from '@sveltejs/kit';
import type { ServerLoad, Actions } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import { canWriteToBase } from '$pcd/writer.ts';
import { pcdManager } from '$pcd/index.ts';
import { canWriteToBase } from '$pcd/index.ts';
import * as customFormatQueries from '$pcd/entities/customFormats/index.ts';
import * as regularExpressionQueries from '$pcd/entities/regularExpressions/index.ts';
import { getLanguagesWithSupport } from '$lib/server/sync/mappings.ts';
import type { OperationLayer } from '$pcd/writer.ts';
import type { OperationLayer } from '$pcd/index.ts';
import type { ConditionData } from '$shared/pcd/display.ts';
export const load: ServerLoad = async ({ params }) => {

View File

@@ -1,9 +1,9 @@
import { error, redirect, fail } from '@sveltejs/kit';
import type { ServerLoad, Actions } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import { canWriteToBase } from '$pcd/writer.ts';
import { pcdManager } from '$pcd/index.ts';
import { canWriteToBase } from '$pcd/index.ts';
import * as customFormatQueries from '$pcd/entities/customFormats/index.ts';
import type { OperationLayer } from '$pcd/writer.ts';
import type { OperationLayer } from '$pcd/index.ts';
export const load: ServerLoad = async ({ params }) => {
const { databaseId, id } = params;

View File

@@ -1,7 +1,7 @@
import { error, fail } from '@sveltejs/kit';
import type { ServerLoad, Actions } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import { canWriteToBase, type OperationLayer } from '$pcd/writer.ts';
import { pcdManager } from '$pcd/index.ts';
import { canWriteToBase, type OperationLayer } from '$pcd/index.ts';
import * as customFormatQueries from '$pcd/entities/customFormats/index.ts';
import type { ConditionResult, ParsedInfo } from '$shared/pcd/display.ts';
import { parse, isParserHealthy } from '$lib/server/utils/arr/parser/client.ts';

View File

@@ -1,7 +1,7 @@
import { error, redirect, fail } from '@sveltejs/kit';
import type { ServerLoad, Actions } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import { canWriteToBase, type OperationLayer } from '$pcd/writer.ts';
import { pcdManager } from '$pcd/index.ts';
import { canWriteToBase, type OperationLayer } from '$pcd/index.ts';
import * as customFormatQueries from '$pcd/entities/customFormats/index.ts';
export const load: ServerLoad = async ({ params, url }) => {

View File

@@ -1,7 +1,7 @@
import { error, redirect, fail } from '@sveltejs/kit';
import type { ServerLoad, Actions } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import { canWriteToBase, type OperationLayer } from '$pcd/writer.ts';
import { pcdManager } from '$pcd/index.ts';
import { canWriteToBase, type OperationLayer } from '$pcd/index.ts';
import * as customFormatQueries from '$pcd/entities/customFormats/index.ts';
export const load: ServerLoad = async ({ params }) => {

View File

@@ -1,9 +1,9 @@
import { error, redirect, fail } from '@sveltejs/kit';
import type { ServerLoad, Actions } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import { canWriteToBase } from '$pcd/writer.ts';
import { pcdManager } from '$pcd/index.ts';
import { canWriteToBase } from '$pcd/index.ts';
import * as customFormatQueries from '$pcd/entities/customFormats/index.ts';
import type { OperationLayer } from '$pcd/writer.ts';
import type { OperationLayer } from '$pcd/index.ts';
export const load: ServerLoad = ({ params }) => {
const { databaseId } = params;

View File

@@ -1,6 +1,6 @@
import { fail, redirect } from '@sveltejs/kit';
import type { Actions, ServerLoad } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import { pcdManager } from '$pcd/index.ts';
import { logger } from '$logger/logger.ts';
export const load: ServerLoad = () => {

View File

@@ -2,8 +2,7 @@ import type { PageServerLoad, Actions } from './$types';
import { databaseInstancesQueries } from '$db/queries/databaseInstances.ts';
import { Git } from '$utils/git/index.ts';
import { logger } from '$logger/logger.ts';
import { compile, startWatch } from '$lib/server/pcd/cache.ts';
import { pcdManager } from '$pcd/pcd.ts';
import { compile, pcdManager } from '$pcd/index.ts';
export const load: PageServerLoad = async ({ parent }) => {
const { database } = await parent();
@@ -32,11 +31,10 @@ export const actions: Actions = {
const git = new Git(database.local_path);
await git.discardOps(files);
// Recompile cache directly instead of relying on file watcher
// Recompile cache after discarding changes
if (database.enabled) {
try {
await compile(database.local_path, id);
await startWatch(database.local_path, id);
} catch (err) {
await logger.error('Failed to recompile cache after discard', {
source: 'changes',

View File

@@ -4,9 +4,10 @@ import {
readManifest,
writeManifest,
validateManifest,
readReadme,
writeReadme,
type Manifest
} from '$lib/server/pcd/manifest.ts';
import { readReadme, writeReadme } from '$lib/server/pcd/readme.ts';
} from '$pcd/index.ts';
import { parseMarkdown } from '$utils/markdown/markdown.ts';
import { databaseInstancesQueries } from '$db/queries/databaseInstances.ts';

View File

@@ -1,7 +1,7 @@
import { redirect, fail } from '@sveltejs/kit';
import type { Actions } from '@sveltejs/kit';
import { databaseInstancesQueries } from '$db/queries/databaseInstances.ts';
import { pcdManager } from '$pcd/pcd.ts';
import { pcdManager } from '$pcd/index.ts';
import { logger } from '$logger/logger.ts';
export const actions: Actions = {

View File

@@ -1,6 +1,6 @@
import { fail, redirect } from '@sveltejs/kit';
import type { Actions, ServerLoad } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import { pcdManager } from '$pcd/index.ts';
import { databaseInstancesQueries } from '$db/queries/databaseInstances.ts';
import { logger } from '$logger/logger.ts';

View File

@@ -1,6 +1,6 @@
import { redirect } from '@sveltejs/kit';
import type { ServerLoad } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import { pcdManager } from '$pcd/index.ts';
export const load: ServerLoad = () => {
// Get all databases

View File

@@ -1,6 +1,6 @@
import { error } from '@sveltejs/kit';
import type { ServerLoad } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import { pcdManager } from '$pcd/index.ts';
import * as delayProfileQueries from '$pcd/entities/delayProfiles/index.ts';
export const load: ServerLoad = async ({ params }) => {

View File

@@ -1,9 +1,9 @@
import { error, redirect, fail } from '@sveltejs/kit';
import type { ServerLoad, Actions } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import { canWriteToBase } from '$pcd/writer.ts';
import { pcdManager } from '$pcd/index.ts';
import { canWriteToBase } from '$pcd/index.ts';
import * as delayProfileQueries from '$pcd/entities/delayProfiles/index.ts';
import type { OperationLayer } from '$pcd/writer.ts';
import type { OperationLayer } from '$pcd/index.ts';
import type { PreferredProtocol } from '$shared/pcd/display.ts';
import { logger } from '$logger/logger.ts';

View File

@@ -1,9 +1,9 @@
import { error, redirect, fail } from '@sveltejs/kit';
import type { ServerLoad, Actions } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import { canWriteToBase } from '$pcd/writer.ts';
import { pcdManager } from '$pcd/index.ts';
import { canWriteToBase } from '$pcd/index.ts';
import * as delayProfileQueries from '$pcd/entities/delayProfiles/index.ts';
import type { OperationLayer } from '$pcd/writer.ts';
import type { OperationLayer } from '$pcd/index.ts';
import type { PreferredProtocol } from '$shared/pcd/display.ts';
import { logger } from '$logger/logger.ts';

View File

@@ -1,6 +1,6 @@
import { redirect } from '@sveltejs/kit';
import type { ServerLoad } from '@sveltejs/kit';
import { pcdManager } from '$pcd/pcd.ts';
import { pcdManager } from '$pcd/index.ts';
export const load: ServerLoad = ({ url }) => {
// Get all databases

View File

@@ -1,7 +1,7 @@
import { error } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
import { pcdManager } from '$pcd/pcd.ts';
import { canWriteToBase } from '$pcd/writer.ts';
import { pcdManager } from '$pcd/index.ts';
import { canWriteToBase } from '$pcd/index.ts';
export const load: LayoutServerLoad = async ({ params }) => {
const { databaseId } = params;

View File

@@ -1,6 +1,6 @@
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { pcdManager } from '$pcd/pcd.ts';
import { pcdManager } from '$pcd/index.ts';
import { list } from '$pcd/entities/mediaManagement/media-settings/read.ts';
export const load: PageServerLoad = async ({ params }) => {

View File

@@ -1,8 +1,8 @@
import { error, redirect, fail } from '@sveltejs/kit';
import type { PageServerLoad, Actions } from './$types';
import { pcdManager } from '$pcd/pcd.ts';
import { canWriteToBase } from '$pcd/writer.ts';
import type { OperationLayer } from '$pcd/writer.ts';
import { pcdManager } from '$pcd/index.ts';
import { canWriteToBase } from '$pcd/index.ts';
import type { OperationLayer } from '$pcd/index.ts';
import type { ArrType } from '$shared/pcd/types.ts';
import type { PropersRepacks } from '$shared/pcd/mediaManagement.ts';
import { createRadarrMediaSettings, createSonarrMediaSettings } from '$pcd/entities/mediaManagement/media-settings/index.ts';

View File

@@ -1,8 +1,8 @@
import { error, redirect, fail } from '@sveltejs/kit';
import type { PageServerLoad, Actions } from './$types';
import { pcdManager } from '$pcd/pcd.ts';
import { canWriteToBase } from '$pcd/writer.ts';
import type { OperationLayer } from '$pcd/writer.ts';
import { pcdManager } from '$pcd/index.ts';
import { canWriteToBase } from '$pcd/index.ts';
import type { OperationLayer } from '$pcd/index.ts';
import { getRadarrByName, updateRadarrMediaSettings, removeRadarrMediaSettings } from '$pcd/entities/mediaManagement/media-settings/index.ts';
import type { PropersRepacks } from '$shared/pcd/mediaManagement.ts';

View File

@@ -1,8 +1,8 @@
import { error, redirect, fail } from '@sveltejs/kit';
import type { PageServerLoad, Actions } from './$types';
import { pcdManager } from '$pcd/pcd.ts';
import { canWriteToBase } from '$pcd/writer.ts';
import type { OperationLayer } from '$pcd/writer.ts';
import { pcdManager } from '$pcd/index.ts';
import { canWriteToBase } from '$pcd/index.ts';
import type { OperationLayer } from '$pcd/index.ts';
import { getSonarrByName, updateSonarrMediaSettings, removeSonarrMediaSettings } from '$pcd/entities/mediaManagement/media-settings/index.ts';
import type { PropersRepacks } from '$shared/pcd/mediaManagement.ts';

View File

@@ -1,6 +1,6 @@
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { pcdManager } from '$pcd/pcd.ts';
import { pcdManager } from '$pcd/index.ts';
import { list } from '$pcd/entities/mediaManagement/naming/read.ts';
export const load: PageServerLoad = async ({ params }) => {

Some files were not shown because too many files have changed in this diff Show More