Skip to content

Commit 25d8e3a

Browse files
authored
[ENG-1176] Sync DG node from Obsidian to database (#665)
* current progress * redo the auth so that only one user acc is created * sync all nodes on load * current progress * revert changes * clean up sync nodes * enable cors * address PR comments
1 parent 4dadfc5 commit 25d8e3a

5 files changed

Lines changed: 818 additions & 7 deletions

File tree

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

apps/obsidian/src/utils/registerCommands.ts

Lines changed: 26 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,28 @@ 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+
new Notice("Sync mode is not enabled", 3000);
142+
return false;
143+
}
144+
if (!checking) {
145+
void createOrUpdateDiscourseEmbedding(plugin)
146+
.then(() => {
147+
new Notice("Discourse nodes synced successfully", 3000);
148+
})
149+
.catch((error) => {
150+
const errorMessage =
151+
error instanceof Error ? error.message : String(error);
152+
new Notice(`Sync failed: ${errorMessage}`, 5000);
153+
console.error("Manual sync failed:", error);
154+
});
155+
}
156+
return true;
157+
},
158+
});
133159
};

0 commit comments

Comments
 (0)