mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-29 14:00:52 +01:00
refactor(pcd): better file structure, remove watcher utils, update deps using tags instead of branches
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
77
src/lib/server/pcd/core/errors.ts
Normal file
77
src/lib/server/pcd/core/errors.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
127
src/lib/server/pcd/core/types.ts
Normal file
127
src/lib/server/pcd/core/types.ts
Normal 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;
|
||||
}
|
||||
281
src/lib/server/pcd/database/cache.ts
Normal file
281
src/lib/server/pcd/database/cache.ts
Normal 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}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/lib/server/pcd/database/compiler.ts
Normal file
53
src/lib/server/pcd/database/compiler.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
56
src/lib/server/pcd/database/registry.ts
Normal file
56
src/lib/server/pcd/database/registry.ts
Normal 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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
235
src/lib/server/pcd/git/dependencies.ts
Normal file
235
src/lib/server/pcd/git/dependencies.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
75
src/lib/server/pcd/index.ts
Normal file
75
src/lib/server/pcd/index.ts
Normal 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';
|
||||
@@ -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 };
|
||||
@@ -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`;
|
||||
}
|
||||
64
src/lib/server/pcd/operations/sql.ts
Normal file
64
src/lib/server/pcd/operations/sql.ts
Normal 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, "''")}'`;
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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`;
|
||||
}
|
||||
@@ -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 }
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user