Files
profilarr/src/lib/server/pcd/pcd.ts

358 lines
8.9 KiB
TypeScript

/**
* PCD Manager - High-level orchestration for PCD lifecycle
*/
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 { notify } from '$notifications/builder.ts';
import { NotificationTypes } from '$notifications/types.ts';
import { compile, invalidate, startWatch, getCache } from './cache.ts';
import { logger } from '$logger/logger.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;
}
/**
* PCD Manager - Manages the lifecycle of Profilarr Compliant Databases
*/
class PCDManager {
/**
* Link a new PCD repository
*/
async link(options: LinkOptions): Promise<DatabaseInstance> {
await logger.debug('Starting database link operation', {
source: 'PCDManager',
meta: {
name: options.name,
repositoryUrl: options.repositoryUrl,
branch: options.branch
}
});
// Generate UUID for storage
const uuid = crypto.randomUUID();
const localPath = getPCDPath(uuid);
try {
// Clone the repository and detect if it's private
const isPrivate = await clone(options.repositoryUrl, localPath, options.branch, options.personalAccessToken);
// Validate manifest (loadManifest throws if invalid)
await loadManifest(localPath);
// Process dependencies (clone and validate)
await processDependencies(localPath);
// Insert into database
const id = databaseInstancesQueries.create({
uuid,
name: options.name,
repositoryUrl: options.repositoryUrl,
localPath,
syncStrategy: options.syncStrategy,
autoPull: options.autoPull,
personalAccessToken: options.personalAccessToken,
isPrivate
});
// Get and return the created instance
const instance = databaseInstancesQueries.getById(id);
if (!instance) {
throw new Error('Failed to retrieve created database instance');
}
// Compile cache and start watching (only if enabled)
if (instance.enabled) {
try {
await compile(localPath, id);
await startWatch(localPath, id);
} catch (error) {
// Log error but don't fail the link operation
await logger.error('Failed to compile PCD cache after linking', {
source: 'PCDManager',
meta: { error: String(error), databaseId: id }
});
}
}
await notify(NotificationTypes.PCD_LINKED)
.title('Database Linked')
.message(`Database "${options.name}" has been linked successfully`)
.meta({ databaseId: id, databaseName: options.name, repositoryUrl: options.repositoryUrl })
.send();
return instance;
} catch (error) {
// Cleanup on failure - remove cloned directory
try {
await Deno.remove(localPath, { recursive: true });
} catch {
// Ignore cleanup errors
}
throw error;
}
}
/**
* Unlink a PCD repository
*/
async unlink(id: number): Promise<void> {
const instance = databaseInstancesQueries.getById(id);
if (!instance) {
throw new Error(`Database instance ${id} not found`);
}
// Store name and URL for notification
const { name, repository_url } = instance;
// Invalidate cache first
invalidate(id);
// Delete from database
databaseInstancesQueries.delete(id);
// Then cleanup filesystem
try {
await Deno.remove(instance.local_path, { recursive: true });
} catch (error) {
// Log but don't throw - database entry is already deleted
console.error(`Failed to remove PCD directory ${instance.local_path}:`, error);
}
await notify(NotificationTypes.PCD_UNLINKED)
.title('Database Unlinked')
.message(`Database "${name}" has been removed`)
.meta({ databaseId: id, databaseName: name, repositoryUrl: repository_url })
.send();
}
/**
* Sync a PCD repository (pull updates)
*/
async sync(id: number): Promise<SyncResult> {
const instance = databaseInstancesQueries.getById(id);
if (!instance) {
throw new Error(`Database instance ${id} not found`);
}
const git = new Git(instance.local_path);
try {
// Check for updates first
const updateInfo = await git.checkForUpdates();
if (!updateInfo.hasUpdates) {
// Already up to date
databaseInstancesQueries.updateSyncedAt(id);
return {
success: true,
commitsBehind: 0
};
}
// Pull updates
await git.pull();
// Sync dependencies (schema, etc.) if versions changed
await syncDependencies(instance.local_path);
// Update last_synced_at
databaseInstancesQueries.updateSyncedAt(id);
// Recompile cache and restart watching (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',
meta: { error: String(error), databaseId: id }
});
}
}
return {
success: true,
commitsBehind: updateInfo.commitsBehind
};
} catch (error) {
return {
success: false,
commitsBehind: 0,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Check for available updates without pulling
*/
async checkForUpdates(id: number): Promise<UpdateInfo> {
const instance = databaseInstancesQueries.getById(id);
if (!instance) {
throw new Error(`Database instance ${id} not found`);
}
const git = new Git(instance.local_path);
return await git.checkForUpdates();
}
/**
* Get parsed manifest for a PCD
*/
async getManifest(id: number): Promise<Manifest> {
const instance = databaseInstancesQueries.getById(id);
if (!instance) {
throw new Error(`Database instance ${id} not found`);
}
return await loadManifest(instance.local_path);
}
/**
* Switch branch for a PCD
*/
async switchBranch(id: number, branch: string): Promise<void> {
const instance = databaseInstancesQueries.getById(id);
if (!instance) {
throw new Error(`Database instance ${id} not found`);
}
const git = new Git(instance.local_path);
await git.checkout(branch);
await git.pull();
databaseInstancesQueries.updateSyncedAt(id);
}
/**
* Get git status for a PCD
*/
async getStatus(id: number): Promise<GitStatus> {
const instance = databaseInstancesQueries.getById(id);
if (!instance) {
throw new Error(`Database instance ${id} not found`);
}
const git = new Git(instance.local_path);
return await git.status();
}
/**
* Get all PCDs
*/
getAll(): DatabaseInstance[] {
return databaseInstancesQueries.getAll();
}
/**
* Get PCD by ID
*/
getById(id: number): DatabaseInstance | undefined {
return databaseInstancesQueries.getById(id);
}
/**
* Get PCDs that need auto-sync
*/
getDueForSync(): DatabaseInstance[] {
return databaseInstancesQueries.getDueForSync();
}
/**
* Initialize PCD caches for all enabled databases
* Should be called on application startup
*/
async initialize(): Promise<void> {
const startTime = performance.now();
await logger.debug('Initialize caches', { source: 'PCDManager' });
const instances = databaseInstancesQueries.getAll();
const enabledInstances = instances.filter((instance) => instance.enabled);
// Collect results for aggregate logging
const results: Array<{
name: string;
schema: number;
base: number;
tweaks: number;
user: number;
error?: string;
}> = [];
// Compile and watch 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,
schema: stats.schema,
base: stats.base,
tweaks: stats.tweaks,
user: stats.user
});
} catch (error) {
results.push({
name: instance.name,
schema: 0,
base: 0,
tweaks: 0,
user: 0,
error: String(error)
});
await logger.error(`Cache failed "${instance.name}"`, {
source: 'PCDManager',
meta: { error: String(error), databaseId: instance.id }
});
}
}
const timing = Math.round(performance.now() - startTime);
const successful = results.filter((r) => !r.error);
await logger.info('Caches ready', {
source: 'PCDManager',
meta: {
databases: successful.map((r) => ({
name: r.name,
schema: r.schema,
base: r.base,
tweaks: r.tweaks,
user: r.user
})),
timing: `${timing}ms`
}
});
}
/**
* Get the cache for a database instance
*/
getCache(id: number) {
return getCache(id);
}
}
// Export singleton instance
export const pcdManager = new PCDManager();