feat: adds logic to sync git FS/DB for project ruleset

This commit is contained in:
Fares Osman
2026-05-21 01:14:26 -04:00
parent a105c23701
commit df72119259
2 changed files with 77 additions and 9 deletions

View File

@@ -8,15 +8,15 @@ export function getByParentId(projectId: string) {
}
export async function upsert(projectId: string, patch: Partial<ProjectLintRuleset> = {}) {
const spec = await db.findOne<ProjectLintRuleset>(type, {
const existing = await db.findOne<ProjectLintRuleset>(type, {
parentId: projectId,
});
if (!spec) {
if (!existing) {
return db.docCreate<ProjectLintRuleset>(type, { ...patch, parentId: projectId });
}
return spec;
return db.docUpdate(existing, patch);
}
export function remove(projectId: string) {

View File

@@ -51,6 +51,7 @@ import { database as db } from '../../common/database';
import { InsomniaFileTypeValues } from '../../common/import-v5-parser';
import { getInsomniaV5DataExport, tryImportV5Data } from '../../common/insomnia-v5';
import { SyncQueue } from './sync-queue';
import YAML from 'yaml';
const POLL_INTERVAL_MS = 10_000;
const DEBOUNCE_MS = 300;
@@ -223,6 +224,7 @@ class RepoFileWatcher {
}
this.queue.enqueue(() => this.flushProjectWorkspacesToDisk());
this.queue.enqueue(() => this.flushProjectLintRulesetToDisk());
await this.queue.waitUntilDone();
}
@@ -359,6 +361,7 @@ class RepoFileWatcher {
this.flushDebounce = setTimeout(() => {
this.flushDebounce = null;
this.queue.enqueue(() => this.flushProjectWorkspacesToDisk());
this.queue.enqueue(() => this.flushProjectLintRulesetToDisk());
}, DEBOUNCE_MS);
});
}
@@ -431,6 +434,39 @@ class RepoFileWatcher {
}
}
private async flushProjectLintRulesetToDisk(): Promise<void> {
if (this.stopped) {
return;
}
const absPath = path.normalize(path.join(this.repoDir, '.spectral.yaml'));
const ruleset = await services.projectLintRuleset.getByParentId(this.projectId);
try {
if (!ruleset) {
// Ruleset removed from the DB — remove the file if we were tracking it.
if (this.lastWrittenHash.has(absPath) || this.lastSyncMtime.has(absPath)) {
await fs.promises.rm(absPath, { force: true });
this.lastWrittenHash.delete(absPath);
this.lastSyncMtime.delete(absPath);
}
return;
}
const hash = contentHash(ruleset.rulesetContent);
if (this.lastWrittenHash.get(absPath) === hash) {
return;
}
await fs.promises.writeFile(absPath, ruleset.rulesetContent, 'utf8');
this.lastWrittenHash.set(absPath, hash);
const stat = await fs.promises.stat(absPath);
this.lastSyncMtime.set(absPath, stat.mtimeMs);
} catch (err) {
console.warn('[repo-file-watcher] Could not flush project lint ruleset to disk:', err);
}
}
// ---------------------------------------------------------------------------
// FS → DB direction (inbound)
// ---------------------------------------------------------------------------
@@ -505,6 +541,27 @@ class RepoFileWatcher {
this.debounceTimers.set(absPath, timer);
}
private isSpectralRulesetPath(normalisedPath: string): boolean {
return (
path.basename(normalisedPath) === '.spectral.yaml' &&
path.normalize(path.dirname(normalisedPath)) === path.normalize(this.repoDir)
);
}
private isSpectralRulesetFile(normalisedPath: string, content: string): boolean {
if (!this.isSpectralRulesetPath(normalisedPath)) {
return false;
}
try {
const parsedContent = YAML.parse(content);
return (
!!parsedContent && typeof parsedContent === 'object' && ('extends' in parsedContent || 'rules' in parsedContent)
);
} catch {
return false;
}
}
/**
* Read a YAML file from disk and import its documents into the DB.
*
@@ -528,6 +585,12 @@ class RepoFileWatcher {
this.lastWrittenHash.set(normalised, result.hash);
this.lastSyncMtime.set(normalised, result.mtimeMs);
if (this.isSpectralRulesetFile(normalised, result.content)) {
await services.projectLintRuleset.upsert(this.projectId, { rulesetContent: result.content });
this.notifyRenderer();
return;
}
const docs = this.parseAndValidate(absPath, normalised, result.content);
if (!docs) {
return;
@@ -685,6 +748,16 @@ class RepoFileWatcher {
return;
}
// The lint ruleset file was deleted — remove the ProjectLintRuleset record.
if (this.isSpectralRulesetPath(normalised)) {
await services.projectLintRuleset.remove(this.projectId);
this.lastSyncMtime.delete(normalised);
this.lastWrittenHash.delete(normalised);
this.clearProblem(normalised);
this.notifyRenderer();
return;
}
const relPath = this.toPosixRelPath(normalised);
// Find the workspace whose gitFilePath matches this deleted file
@@ -726,12 +799,7 @@ class RepoFileWatcher {
return;
}
console.warn(
'[repo-file-watcher] Failed to remove workspace file from disk:',
workspaceId,
normalisedPath,
err,
);
console.warn('[repo-file-watcher] Failed to remove workspace file from disk:', workspaceId, normalisedPath, err);
}
}