sourcebot/packages/backend/src/configManager.ts

126 lines
4.8 KiB
TypeScript
Raw Normal View History

import { Prisma, PrismaClient } from "@sourcebot/db";
import { createLogger } from "@sourcebot/shared";
import { ConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
import { loadConfig } from "@sourcebot/shared";
import chokidar, { FSWatcher } from 'chokidar';
import { ConnectionManager } from "./connectionManager.js";
import { SINGLE_TENANT_ORG_ID } from "./constants.js";
import { syncSearchContexts } from "./ee/syncSearchContexts.js";
const logger = createLogger('config-manager');
export class ConfigManager {
private watcher: FSWatcher;
constructor(
private db: PrismaClient,
private connectionManager: ConnectionManager,
configPath: string,
) {
this.watcher = chokidar.watch(configPath, {
ignoreInitial: true, // Don't fire events for existing files
awaitWriteFinish: {
stabilityThreshold: 100, // File size stable for 100ms
pollInterval: 100 // Check every 100ms
},
atomic: true // Handle atomic writes (temp file + rename)
});
this.watcher.on('change', async () => {
logger.info(`Config file ${configPath} changed. Syncing config.`);
try {
await this.syncConfig(configPath);
} catch (error) {
logger.error(`Failed to sync config: ${error}`);
}
});
this.syncConfig(configPath);
}
private syncConfig = async (configPath: string) => {
const config = await loadConfig(configPath);
await this.syncConnections(config.connections);
await syncSearchContexts({
contexts: config.contexts,
orgId: SINGLE_TENANT_ORG_ID,
db: this.db,
});
}
private syncConnections = async (connections?: { [key: string]: ConnectionConfig }) => {
if (connections) {
for (const [key, newConnectionConfig] of Object.entries(connections)) {
const existingConnection = await this.db.connection.findUnique({
where: {
name_orgId: {
name: key,
orgId: SINGLE_TENANT_ORG_ID,
}
}
});
const existingConnectionConfig = existingConnection ? existingConnection.config as unknown as ConnectionConfig : undefined;
const connectionNeedsSyncing =
!existingConnection ||
(JSON.stringify(existingConnectionConfig) !== JSON.stringify(newConnectionConfig));
// Either update the existing connection or create a new one.
const connection = existingConnection ?
await this.db.connection.update({
where: {
id: existingConnection.id,
},
data: {
config: newConnectionConfig as unknown as Prisma.InputJsonValue,
isDeclarative: true,
}
}) :
await this.db.connection.create({
data: {
name: key,
config: newConnectionConfig as unknown as Prisma.InputJsonValue,
connectionType: newConnectionConfig.type,
isDeclarative: true,
org: {
connect: {
id: SINGLE_TENANT_ORG_ID,
}
}
}
});
if (connectionNeedsSyncing) {
const [jobId] = await this.connectionManager.createJobs([connection]);
logger.info(`Change detected for connection '${key}' (id: ${connection.id}). Created sync job ${jobId}.`);
}
}
}
// Delete any connections that are no longer in the config.
const deletedConnections = await this.db.connection.findMany({
where: {
isDeclarative: true,
name: {
notIn: Object.keys(connections ?? {}),
},
orgId: SINGLE_TENANT_ORG_ID,
}
});
for (const connection of deletedConnections) {
logger.info(`Deleting connection with name '${connection.name}'. Connection ID: ${connection.id}`);
await this.db.connection.delete({
where: {
id: connection.id,
}
})
}
}
public dispose = async () => {
await this.watcher.close();
}
}