Skip to content

Commit 32e2609

Browse files
[ENG-1369] Sync non text assets automatically (#754)
Co-authored-by: Marc-Antoine Parent <maparent@acm.org>
1 parent 6d40f58 commit 32e2609

2 files changed

Lines changed: 119 additions & 54 deletions

File tree

apps/obsidian/src/utils/publishNode.ts

Lines changed: 81 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,18 @@ export const publishNode = async ({
7272
const context = await getSupabaseContext(plugin);
7373
if (!context) throw new Error("Cannot get context");
7474
const spaceId = context.spaceId;
75-
const myGroupResponse = await client
75+
const myGroupsResponse = await client
7676
.from("group_membership")
7777
.select("group_id");
78-
if (myGroupResponse.error) throw myGroupResponse.error;
79-
const myGroup = myGroupResponse.data[0]?.group_id;
80-
if (!myGroup) throw new Error("Cannot get group");
78+
if (myGroupsResponse.error) throw myGroupsResponse.error;
79+
const myGroups = new Set(
80+
myGroupsResponse.data.map(({ group_id }) => group_id),
81+
);
82+
if (myGroups.size === 0) throw new Error("Cannot get group");
8183
const existingPublish =
8284
(frontmatter.publishedToGroups as undefined | string[]) || [];
85+
const commonGroups = existingPublish.filter((g) => myGroups.has(g));
86+
const myGroup = (commonGroups.length > 0 ? commonGroups : [...myGroups])[0]!;
8387
const idResponse = await client
8488
.from("Content")
8589
.select("last_modified")
@@ -111,63 +115,87 @@ export const publishNode = async ({
111115
...attachments.map((a) => a.stat.mtime),
112116
);
113117

114-
if (existingPublish.includes(myGroup) && lastModified <= lastModifiedDb)
115-
return; // already published
116-
const publishSpaceResponse = await client.from("SpaceAccess").upsert(
117-
{
118-
/* eslint-disable @typescript-eslint/naming-convention */
119-
account_uid: myGroup,
120-
space_id: spaceId,
121-
/* eslint-enable @typescript-eslint/naming-convention */
122-
permissions: "partial",
123-
},
124-
{ ignoreDuplicates: true },
125-
);
126-
if (publishSpaceResponse.error && publishSpaceResponse.error.code !== "23505")
127-
// 23505 is duplicate key, which counts as a success.
128-
throw publishSpaceResponse.error;
118+
const skipPublishAccess =
119+
commonGroups.length > 0 && lastModified <= lastModifiedDb;
129120

130-
const publishResponse = await client.from("ResourceAccess").upsert(
131-
{
132-
/* eslint-disable @typescript-eslint/naming-convention */
133-
account_uid: myGroup,
134-
source_local_id: nodeId,
135-
space_id: spaceId,
136-
/* eslint-enable @typescript-eslint/naming-convention */
137-
},
138-
{ ignoreDuplicates: true },
139-
);
140-
if (publishResponse.error && publishResponse.error.code !== "23505")
141-
// 23505 is duplicate key, which counts as a success.
142-
throw publishResponse.error;
121+
if (!skipPublishAccess) {
122+
const publishSpaceResponse = await client.from("SpaceAccess").upsert(
123+
{
124+
/* eslint-disable @typescript-eslint/naming-convention */
125+
account_uid: myGroup,
126+
space_id: spaceId,
127+
/* eslint-enable @typescript-eslint/naming-convention */
128+
permissions: "partial",
129+
},
130+
{ ignoreDuplicates: true },
131+
);
132+
if (
133+
publishSpaceResponse.error &&
134+
publishSpaceResponse.error.code !== "23505"
135+
)
136+
throw publishSpaceResponse.error;
137+
138+
const publishResponse = await client.from("ResourceAccess").upsert(
139+
{
140+
/* eslint-disable @typescript-eslint/naming-convention */
141+
account_uid: myGroup,
142+
source_local_id: nodeId,
143+
space_id: spaceId,
144+
/* eslint-enable @typescript-eslint/naming-convention */
145+
},
146+
{ ignoreDuplicates: true },
147+
);
148+
if (publishResponse.error && publishResponse.error.code !== "23505")
149+
throw publishResponse.error;
143150

144-
// Also publish the schema so it's accessible when importing nodes
145-
const nodeTypeId = frontmatter.nodeTypeId as string | undefined;
146-
if (nodeTypeId) {
147-
await publishSchema({
148-
client,
149-
spaceId,
150-
nodeTypeId,
151-
groupId: myGroup,
152-
});
151+
const nodeTypeId = frontmatter.nodeTypeId as string | undefined;
152+
if (nodeTypeId) {
153+
await publishSchema({
154+
client,
155+
spaceId,
156+
nodeTypeId,
157+
groupId: myGroup,
158+
});
159+
}
153160
}
154161

162+
// Always sync non-text assets when node is published to this group
155163
const existingFiles: string[] = [];
164+
const existingReferencesReq = await client
165+
.from("FileReference")
166+
.select("*")
167+
.eq("space_id", spaceId)
168+
.eq("source_local_id", nodeId);
169+
if (existingReferencesReq.error) {
170+
console.error(existingReferencesReq.error);
171+
return;
172+
}
173+
const existingReferencesByPath = Object.fromEntries(
174+
existingReferencesReq.data.map((ref) => [ref.filepath, ref]),
175+
);
176+
156177
for (const attachment of attachments) {
157178
const mimetype = mime.lookup(attachment.path) || "application/octet-stream";
158179
if (mimetype.startsWith("text/")) continue;
159180
existingFiles.push(attachment.path);
160-
const content = await plugin.app.vault.readBinary(attachment);
161-
await addFile({
162-
client,
163-
spaceId,
164-
sourceLocalId: nodeId,
165-
fname: attachment.path,
166-
mimetype,
167-
created: new Date(attachment.stat.ctime),
168-
lastModified: new Date(attachment.stat.mtime),
169-
content,
170-
});
181+
const existingRef = existingReferencesByPath[attachment.path];
182+
if (
183+
!existingRef ||
184+
new Date(existingRef.last_modified + "Z").valueOf() <
185+
attachment.stat.mtime
186+
) {
187+
const content = await plugin.app.vault.readBinary(attachment);
188+
await addFile({
189+
client,
190+
spaceId,
191+
sourceLocalId: nodeId,
192+
fname: attachment.path,
193+
mimetype,
194+
created: new Date(attachment.stat.ctime),
195+
lastModified: new Date(attachment.stat.mtime),
196+
content,
197+
});
198+
}
171199
}
172200
let cleanupCommand = client
173201
.from("FileReference")
@@ -182,7 +210,7 @@ export const publishNode = async ({
182210
// do not fail on cleanup
183211
if (cleanupResult.error) console.error(cleanupResult.error);
184212

185-
if (!existingPublish.includes(myGroup))
213+
if (commonGroups.length === 0)
186214
await plugin.app.fileManager.processFrontMatter(
187215
file,
188216
(fm: Record<string, unknown>) => {

apps/obsidian/src/utils/syncDgNodesToSupabase.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable @typescript-eslint/naming-convention */
2-
import { Notice, TFile } from "obsidian";
2+
import { FrontMatterCache, Notice, TFile } from "obsidian";
33
import { ensureNodeInstanceId } from "~/utils/nodeInstanceId";
44
import type { DGSupabaseClient } from "@repo/database/lib/client";
55
import type { Json } from "@repo/database/dbTypes";
@@ -9,6 +9,7 @@ import {
99
type SupabaseContext,
1010
} from "./supabaseContext";
1111
import { default as DiscourseGraphPlugin } from "~/index";
12+
import { publishNode } from "./publishNode";
1213
import { upsertNodesToSupabaseAsContentWithEmbeddings } from "./upsertNodesAsContentWithEmbeddings";
1314
import {
1415
orderConceptsByDependency,
@@ -491,6 +492,9 @@ export const createOrUpdateDiscourseEmbedding = async (
491492
plugin,
492493
});
493494

495+
// When synced nodes are already published, ensure non-text assets are in storage.
496+
await syncPublishedNodesAssets(plugin, allNodeInstances);
497+
494498
console.debug("Sync completed successfully");
495499
} catch (error) {
496500
console.error("createOrUpdateDiscourseEmbedding: Process failed:", error);
@@ -559,6 +563,35 @@ const convertDgToSupabaseConcepts = async ({
559563
}
560564
};
561565

566+
/**
567+
* For nodes that are already published, ensure non-text assets are pushed to
568+
* storage. Called after content sync so new embeds (e.g. images) get uploaded.
569+
*/
570+
const syncPublishedNodesAssets = async (
571+
plugin: DiscourseGraphPlugin,
572+
nodes: ObsidianDiscourseNodeData[],
573+
): Promise<void> => {
574+
const published = nodes.filter(
575+
(n) =>
576+
((n.frontmatter.publishedToGroups as string[] | undefined)?.length ?? 0) >
577+
0,
578+
);
579+
for (const node of published) {
580+
try {
581+
await publishNode({
582+
plugin,
583+
file: node.file,
584+
frontmatter: node.frontmatter as FrontMatterCache,
585+
});
586+
} catch (error) {
587+
console.error(
588+
`Failed to sync published node assets for ${node.file.path}:`,
589+
error,
590+
);
591+
}
592+
}
593+
};
594+
562595
/**
563596
* Shared function to sync changed nodes to Supabase
564597
* Handles content/embedding upsert and concept upsert
@@ -599,6 +632,10 @@ const syncChangedNodesToSupabase = async ({
599632
accountLocalId,
600633
plugin,
601634
});
635+
636+
// When file changes affect an already-published node, ensure new non-text
637+
// assets (e.g. images) are pushed to storage.
638+
await syncPublishedNodesAssets(plugin, changedNodes);
602639
};
603640

604641
/**

0 commit comments

Comments
 (0)