Files
insomnia/packages/insomnia-data/node-src/database/repair-database.ts
Bingbing bf61f6ba45 fix: stop restamping ApiSpec.modified on startup and git round-trips (#10625)
## Background

In a Git-synced project, restarting Insomnia without any change marked every design document as modified in the Git staging list. The diff of each affected YAML contained exactly one line: the embedded ApiSpec `meta.modified` timestamp.

The v5 design-doc YAML does not serialize `ApiSpec.fileName` (the v5 schema has no such field), while v5 import parses a missing `fileName` as an empty string. Any git round-trip therefore reset `fileName` to empty in the database, and the startup repair (`_applyApiSpecName`) rewrote it via `docUpdate`, which unconditionally restamped `modified: Date.now()` for every design document in one burst. RepoFileWatcher then re-exported the database to disk and git status reported all files as modified, repeating on every startup until committed.

## Changes

- Import: fall back `ApiSpec.fileName` to the file top-level name when the spec uses the `{ contents }` shape, so a round-trip never stores an empty fileName.
- Repair: skip the fileName backfill when it would write the existing value; a no-op write only restamps `modified`.
- Regression tests for both behaviors.

Fixes INS-4058
2026-10-20 02:58:17 +00:00

135 lines
4.0 KiB
TypeScript

import type { CookieJar, Environment, GitRepository, Workspace } from 'insomnia-data';
import { database, models } from 'insomnia-data';
import * as apiSpecServices from '../services/api-spec';
/**
* Run various database repair scripts
*/
export async function repairDatabase() {
console.log('[fix] Running database repairs');
for (const workspace of await database.find<Workspace>(models.workspace.type)) {
await _repairBaseEnvironments(workspace);
await _fixMultipleCookieJars(workspace);
await _applyApiSpecName(workspace);
}
for (const gitRepository of await database.find<GitRepository>(models.gitRepository.type)) {
await _fixOldGitURIs(gitRepository);
}
}
/**
* This function ensures that apiSpec exists for each workspace
* If the filename on the apiSpec is not set or is the default initialized name
* It will apply the workspace name to it
*/
async function _applyApiSpecName(workspace: Workspace) {
const apiSpec = await apiSpecServices.getByParentId(workspace._id);
const existsAndFilenameIsDefaultOrMissing =
apiSpec && (!apiSpec.fileName || apiSpec.fileName === models.apiSpec.init().fileName);
// A backfill that would write the existing value changes nothing but would
// still restamp `modified`, which git sync serializes into YAML and shows up
// as a modified file. Skip no-op writes.
if (existsAndFilenameIsDefaultOrMissing && apiSpec.fileName !== workspace.name) {
await apiSpecServices.update(apiSpec, { fileName: workspace.name });
}
}
/**
* This function repairs workspaces that have multiple base environments. Since a workspace
* can only have one, this function walks over all base environments, merges the data, and
* moves all children as well.
*/
async function _repairBaseEnvironments(workspace: Workspace) {
const baseEnvironments = await database.find<Environment>(models.environment.type, {
parentId: workspace._id,
});
// Nothing to do here
if (baseEnvironments.length <= 1) {
return;
}
const chosenBase = baseEnvironments[0];
for (const baseEnvironment of baseEnvironments) {
if (baseEnvironment._id === chosenBase._id) {
continue;
}
chosenBase.data = Object.assign(baseEnvironment.data, chosenBase.data);
const subEnvironments = await database.find<Environment>(models.environment.type, {
parentId: baseEnvironment._id,
});
for (const subEnvironment of subEnvironments) {
await database.docUpdate(subEnvironment, {
parentId: chosenBase._id,
});
}
// Remove unnecessary base env
await database.remove(baseEnvironment);
}
// Update remaining base env
await database.update(chosenBase);
console.log(`[fix] Merged ${baseEnvironments.length} base environments under ${workspace.name}`);
}
/**
* This function repairs workspaces that have multiple cookie jars. Since a workspace
* can only have one, this function walks over all jars and merges them and their cookies
* together.
*/
async function _fixMultipleCookieJars(workspace: Workspace) {
const cookieJars = await database.find<CookieJar>(models.cookieJar.type, {
parentId: workspace._id,
});
// Nothing to do here
if (cookieJars.length <= 1) {
return;
}
const chosenJar = cookieJars[0];
for (const cookieJar of cookieJars) {
if (cookieJar._id === chosenJar._id) {
continue;
}
for (const cookie of cookieJar.cookies) {
if (chosenJar.cookies.find(c => c.id === cookie.id)) {
continue;
}
chosenJar.cookies.push(cookie);
}
// Remove unnecessary jar
await database.remove(cookieJar);
}
// Update remaining jar
await database.update(chosenJar);
console.log(`[fix] Merged ${cookieJars.length} cookie jars under ${workspace.name}`);
}
// Append .git to old git URIs to mimic previous isomorphic-git behavior
async function _fixOldGitURIs(doc: GitRepository) {
if (!doc.uriNeedsMigration) {
return;
}
if (!doc.uri.endsWith('.git')) {
doc.uri += '.git';
}
doc.uriNeedsMigration = false;
await database.update(doc);
console.log(`[fix] Fixed git URI for ${doc._id}`);
}