Skip to content

Commit f95ede6

Browse files
trangdoan982claudegraphite-app[bot]
authored
ENG-1629: Fix relations.json scattered across multiple locations (#947)
* ENG-1629: Fix relations.json scattered across multiple locations Make vault root the canonical location for relations.json (always "relations.json" at root, never under nodesFolderPath). Add a one-time merge on plugin load to consolidate any scattered copies left behind from previous nodesFolderPath changes. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Fix early return when single relations.json exists at non-root path Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * process root last * Update apps/obsidian/src/utils/relationsStore.ts Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
1 parent 667b243 commit f95ede6

2 files changed

Lines changed: 63 additions & 15 deletions

File tree

apps/obsidian/src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ import { InlineNodeTypePicker } from "~/components/InlineNodeTypePicker";
3333
import { initializeSupabaseSync } from "~/utils/syncDgNodesToSupabase";
3434
import { FileChangeListener } from "~/utils/fileChangeListener";
3535
import generateUid from "~/utils/generateUid";
36-
import { migrateFrontmatterRelationsToRelationsJson } from "~/utils/relationsStore";
36+
import {
37+
migrateFrontmatterRelationsToRelationsJson,
38+
mergeAllRelationsJsonToRoot,
39+
} from "~/utils/relationsStore";
3740

3841
export default class DiscourseGraphPlugin extends Plugin {
3942
settings: Settings = { ...DEFAULT_SETTINGS };
@@ -46,6 +49,10 @@ export default class DiscourseGraphPlugin extends Plugin {
4649
async onload() {
4750
await this.loadSettings();
4851

52+
await mergeAllRelationsJsonToRoot(this).catch((error) => {
53+
console.error("Failed to merge relations.json files:", error);
54+
});
55+
4956
await migrateFrontmatterRelationsToRelationsJson(this).catch((error) => {
5057
console.error("Failed to migrate frontmatter relations:", error);
5158
});

apps/obsidian/src/utils/relationsStore.ts

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { uuidv7 } from "uuidv7";
33
import type { DGSupabaseClient } from "@repo/database/lib/client";
44
import type DiscourseGraphPlugin from "~/index";
55
import { ensureNodeInstanceId } from "~/utils/nodeInstanceId";
6-
import { checkAndCreateFolder } from "~/utils/file";
76
import { getVaultId, getLocalSpaceUri } from "./supabaseContext";
87
import type { RelationInstance } from "~/types";
98
import { QueryEngine, getImportedNodesRaw } from "~/services/QueryEngine";
@@ -14,13 +13,9 @@ import { getSpaceIdsBySpaceUris } from "./spaceFromRid";
1413
const RELATIONS_FILE_NAME = "relations.json";
1514
const RELATIONS_FILE_VERSION = 1;
1615

17-
/** Vault-relative path for relations.json, under the same folder as nodes (nodesFolderPath). */
18-
export const getRelationsFilePath = (plugin: DiscourseGraphPlugin): string => {
19-
const folderPath = plugin.settings.nodesFolderPath.trim();
20-
return folderPath
21-
? normalizePath(`${folderPath}/${RELATIONS_FILE_NAME}`)
22-
: normalizePath(RELATIONS_FILE_NAME);
23-
};
16+
/** Vault-relative path for relations.json — always at vault root. */
17+
export const getRelationsFilePath = (): string =>
18+
normalizePath(RELATIONS_FILE_NAME);
2419

2520
export type RelationsFile = {
2621
version: number;
@@ -37,7 +32,7 @@ const defaultRelationsFile = (): RelationsFile => ({
3732
export const loadRelations = async (
3833
plugin: DiscourseGraphPlugin,
3934
): Promise<RelationsFile> => {
40-
const path = getRelationsFilePath(plugin);
35+
const path = getRelationsFilePath();
4136
const file = plugin.app.vault.getAbstractFileByPath(path);
4237
if (!file || !(file instanceof TFile)) {
4338
return defaultRelationsFile();
@@ -67,11 +62,7 @@ export const saveRelations = async (
6762
plugin: DiscourseGraphPlugin,
6863
data: RelationsFile,
6964
): Promise<void> => {
70-
const folderPath = plugin.settings.nodesFolderPath.trim();
71-
if (folderPath) {
72-
await checkAndCreateFolder(folderPath, plugin.app.vault);
73-
}
74-
const path = getRelationsFilePath(plugin);
65+
const path = getRelationsFilePath();
7566
const toWrite: RelationsFile = {
7667
...data,
7768
lastModified: Date.now(),
@@ -85,6 +76,56 @@ export const saveRelations = async (
8576
}
8677
};
8778

79+
/**
80+
* On plugin load, finds all relations.json files in the vault and merges them
81+
* into the canonical location at vault root, then deletes the non-root copies.
82+
* Handles the case where a user changed nodesFolderPath, leaving old files behind.
83+
*/
84+
export const mergeAllRelationsJsonToRoot = async (
85+
plugin: DiscourseGraphPlugin,
86+
): Promise<void> => {
87+
const allFiles = plugin.app.vault.getFiles();
88+
const relationsFiles = allFiles.filter((f) => f.name === RELATIONS_FILE_NAME);
89+
const rootPath = normalizePath(RELATIONS_FILE_NAME);
90+
const nonRootFiles = relationsFiles.filter((f) => f.path !== rootPath);
91+
92+
if (nonRootFiles.length === 0) return;
93+
94+
// Process non-root files first so root values win on duplicate IDs.
95+
const sortedFiles = [
96+
...nonRootFiles,
97+
...relationsFiles.filter((f) => f.path === rootPath),
98+
];
99+
const merged = defaultRelationsFile();
100+
const validatedNonRootFiles: TFile[] = [];
101+
for (const file of sortedFiles) {
102+
try {
103+
const content = await plugin.app.vault.read(file);
104+
const data = JSON.parse(content) as RelationsFile;
105+
if (
106+
typeof data.version !== "number" ||
107+
typeof data.relations !== "object" ||
108+
data.relations === null
109+
)
110+
continue;
111+
Object.assign(merged.relations, data.relations);
112+
merged.lastModified = Math.max(
113+
merged.lastModified,
114+
data.lastModified ?? 0,
115+
);
116+
if (file.path !== rootPath) validatedNonRootFiles.push(file);
117+
} catch {
118+
// skip unreadable or unparseable files
119+
}
120+
}
121+
122+
await saveRelations(plugin, merged);
123+
124+
for (const file of validatedNonRootFiles) {
125+
await plugin.app.vault.delete(file);
126+
}
127+
};
128+
88129
export type AddRelationParams = {
89130
type: string;
90131
source: string;

0 commit comments

Comments
 (0)