2025-10-29 04:31:28 +00:00
|
|
|
import { Prisma, PrismaClient } from "@sourcebot/db";
|
2025-11-05 05:22:31 +00:00
|
|
|
import { createLogger } from "@sourcebot/shared";
|
2025-10-29 04:31:28 +00:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|