Skip to content

Commit efaa9f9

Browse files
committed
sync all nodes on load
1 parent 3f40d5c commit efaa9f9

File tree

5 files changed

+719
-7
lines changed

5 files changed

+719
-7
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
2+
import { TFile } from "obsidian";
3+
import { DiscourseNode } from "~/types";
4+
import { SupabaseContext } from "./supabaseContext";
5+
import { LocalConceptDataInput } from "@repo/database/inputTypes";
6+
import { ObsidianDiscourseNodeData } from "./syncDgNodesToSupabase";
7+
8+
/**
9+
* Get extra data (author, timestamps) from file metadata
10+
*/
11+
const getNodeExtraData = (
12+
file: TFile,
13+
accountLocalId: string,
14+
): {
15+
author_local_id: string;
16+
created: string;
17+
last_modified: string;
18+
} => {
19+
return {
20+
author_local_id: accountLocalId,
21+
created: new Date(file.stat.ctime).toISOString(),
22+
last_modified: new Date(file.stat.mtime).toISOString(),
23+
};
24+
};
25+
26+
export const discourseNodeSchemaToLocalConcept = ({
27+
context,
28+
node,
29+
accountLocalId,
30+
}: {
31+
context: SupabaseContext;
32+
node: DiscourseNode;
33+
accountLocalId: string;
34+
}): LocalConceptDataInput => {
35+
const now = new Date().toISOString();
36+
return {
37+
space_id: context.spaceId,
38+
name: node.name,
39+
represented_by_local_id: node.id,
40+
is_schema: true,
41+
author_local_id: accountLocalId,
42+
created: now,
43+
last_modified: now,
44+
};
45+
};
46+
47+
/**
48+
* Convert discourse node instance (file) to LocalConceptDataInput
49+
*/
50+
export const discourseNodeBlockToLocalConcept = ({
51+
context,
52+
nodeData,
53+
accountLocalId,
54+
}: {
55+
context: SupabaseContext;
56+
nodeData: ObsidianDiscourseNodeData;
57+
accountLocalId: string;
58+
}): LocalConceptDataInput => {
59+
const extraData = getNodeExtraData(nodeData.file, accountLocalId);
60+
const concept = {
61+
space_id: context.spaceId,
62+
name: nodeData.file.basename,
63+
represented_by_local_id: nodeData.nodeInstanceId,
64+
schema_represented_by_local_id: nodeData.nodeTypeId,
65+
is_schema: false,
66+
...extraData,
67+
};
68+
console.log(
69+
`[discourseNodeBlockToLocalConcept] Converting concept: represented_by_local_id=${nodeData.nodeInstanceId}, name="${nodeData.file.basename}"`,
70+
);
71+
return concept;
72+
};
73+
74+
export const relatedConcepts = (concept: LocalConceptDataInput): string[] => {
75+
const relations = Object.values(
76+
concept.local_reference_content || {},
77+
).flat() as string[];
78+
if (concept.schema_represented_by_local_id) {
79+
relations.push(concept.schema_represented_by_local_id);
80+
}
81+
// remove duplicates
82+
return [...new Set(relations)];
83+
};
84+
85+
/**
86+
* Recursively order concepts by dependency
87+
*/
88+
const orderConceptsRec = (
89+
ordered: LocalConceptDataInput[],
90+
concept: LocalConceptDataInput,
91+
remainder: { [key: string]: LocalConceptDataInput },
92+
): Set<string> => {
93+
const relatedConceptIds = relatedConcepts(concept);
94+
let missing: Set<string> = new Set();
95+
while (relatedConceptIds.length > 0) {
96+
const relatedConceptId = relatedConceptIds.shift()!;
97+
const relatedConcept = remainder[relatedConceptId];
98+
if (relatedConcept === undefined) {
99+
missing.add(relatedConceptId);
100+
} else {
101+
missing = new Set([
102+
...missing,
103+
...orderConceptsRec(ordered, relatedConcept, remainder),
104+
]);
105+
delete remainder[relatedConceptId];
106+
}
107+
}
108+
ordered.push(concept);
109+
delete remainder[concept.represented_by_local_id!];
110+
return missing;
111+
};
112+
113+
export const orderConceptsByDependency = (
114+
concepts: LocalConceptDataInput[],
115+
): { ordered: LocalConceptDataInput[]; missing: string[] } => {
116+
if (concepts.length === 0) return { ordered: concepts, missing: [] };
117+
const conceptById: { [key: string]: LocalConceptDataInput } =
118+
Object.fromEntries(
119+
concepts
120+
.filter((c) => c.represented_by_local_id)
121+
.map((c) => [c.represented_by_local_id!, c]),
122+
);
123+
const ordered: LocalConceptDataInput[] = [];
124+
let missing: Set<string> = new Set();
125+
while (Object.keys(conceptById).length > 0) {
126+
const first = Object.values(conceptById)[0];
127+
if (!first) break;
128+
missing = new Set([
129+
...missing,
130+
...orderConceptsRec(ordered, first, conceptById),
131+
]);
132+
}
133+
return { ordered, missing: Array.from(missing) };
134+
};

apps/obsidian/src/utils/registerCommands.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { BulkIdentifyDiscourseNodesModal } from "~/components/BulkIdentifyDiscou
66
import { createDiscourseNode } from "./createNode";
77
import { VIEW_TYPE_MARKDOWN, VIEW_TYPE_TLDRAW_DG_PREVIEW } from "~/constants";
88
import { createCanvas } from "~/components/canvas/utils/tldraw";
9+
import { createOrUpdateDiscourseEmbedding } from "./syncDgNodesToSupabase";
10+
import { Notice } from "obsidian";
911

1012
export const registerCommands = (plugin: DiscourseGraphPlugin) => {
1113
plugin.addCommand({
@@ -130,4 +132,27 @@ export const registerCommands = (plugin: DiscourseGraphPlugin) => {
130132
icon: "layout-dashboard", // Using Lucide icon as per style guide
131133
callback: () => createCanvas(plugin),
132134
});
135+
136+
plugin.addCommand({
137+
id: "sync-discourse-nodes-to-supabase",
138+
name: "Sync Discourse Nodes to Supabase",
139+
checkCallback: (checking: boolean) => {
140+
if (!plugin.settings.syncModeEnabled) {
141+
return false;
142+
}
143+
if (!checking) {
144+
void createOrUpdateDiscourseEmbedding(plugin)
145+
.then(() => {
146+
new Notice("Discourse nodes synced successfully", 3000);
147+
})
148+
.catch((error) => {
149+
const errorMessage =
150+
error instanceof Error ? error.message : String(error);
151+
new Notice(`Sync failed: ${errorMessage}`, 5000);
152+
console.error("Manual sync failed:", error);
153+
});
154+
}
155+
return true;
156+
},
157+
});
133158
};

0 commit comments

Comments
 (0)