Skip to content

Commit 7e9a33d

Browse files
committed
eng-1865 sync and publish relations related to published nodes, WIP
1 parent 83cd593 commit 7e9a33d

2 files changed

Lines changed: 345 additions & 0 deletions

File tree

apps/roam/src/utils/createReifiedBlock.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,36 @@ export const countReifiedRelations = async (): Promise<number> => {
102102
return (r[0] || [0])[0] as number;
103103
};
104104

105+
export type ReifiedRelationData = {
106+
sourceUid: string;
107+
destinationUid: string;
108+
hasSchema: string;
109+
importedFromRid?: string;
110+
};
111+
112+
export type ReifiedRelationDataWithRelId = ReifiedRelationData & {
113+
relationId: string;
114+
};
115+
116+
export const getReifiedRelations = async (): Promise<
117+
ReifiedRelationDataWithRelId[]
118+
> => {
119+
const pageUid = getExistingRelationPageUid();
120+
if (pageUid === undefined) return [];
121+
const r = await window.roamAlphaAPI.data.async.q(
122+
`[:find ?ruid ?rdata :where
123+
[?p :block/uid "${pageUid}"]
124+
[?p :block/children ?c]
125+
[?c :block/uid ?ruid]
126+
[?c :block/props ?pr]
127+
[(get ?pr :${DISCOURSE_GRAPH_PROP_NAME}) ?rdata] ]`,
128+
);
129+
return r.map((x) => ({
130+
relationId: x[0] as string,
131+
...(x[1] as ReifiedRelationData),
132+
}));
133+
};
134+
105135
export const createReifiedRelation = async ({
106136
sourceUid,
107137
relationBlockUid,

apps/roam/src/utils/publishNodesToGroups.ts

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
import type { DGSupabaseClient } from "@repo/database/lib/client";
22
import { getAvailableGroupIds } from "@repo/database/lib/groups";
3+
import getDiscourseRelations from "./getDiscourseRelations";
4+
import getDiscourseNodes from "./getDiscourseNodes";
5+
import { SupabaseClient } from "@supabase/supabase-js";
6+
import {
7+
getReifiedRelations,
8+
type ReifiedRelationDataWithRelId,
9+
} from "./createReifiedBlock";
10+
import type {
11+
CrossAppNodeSchema,
12+
CrossAppRelation,
13+
CrossAppRelationTripleSchema,
14+
} from "@repo/database/crossAppContracts";
15+
import type { DiscourseRelation } from "./getDiscourseRelations";
16+
import {
17+
crossAppRelationToDbConcept,
18+
crossAppRelationTripleSchemaToDbConcept,
19+
} from "@repo/database/lib/crossAppConverters";
20+
import { spaceUriAndLocalIdToRid } from "@repo/database/lib/rid";
321

422
export type PublishNode = {
523
uid: string;
@@ -13,6 +31,297 @@ type PublishNodesResult = {
1331
failedGroupIds: string[];
1432
};
1533

34+
const getAllPublishedIds = async (
35+
client: DGSupabaseClient,
36+
spaceId: number,
37+
groupId: string,
38+
): Promise<string[]> => {
39+
const response = await client
40+
.from("ResourceAccess")
41+
.select("source_local_id")
42+
.eq("space_id", spaceId)
43+
.eq("account_uid", groupId);
44+
if (response.error) throw response.error;
45+
return response.data.map((r) => r.source_local_id);
46+
};
47+
48+
const getGroupSpaceIdAndUrls = async (
49+
client: DGSupabaseClient,
50+
groupId: string,
51+
): Promise<Record<number, string>> => {
52+
const response = await client
53+
.from("SpaceAccess")
54+
.select("space_id")
55+
.eq("account_uid", groupId);
56+
if (response.error) throw response.error;
57+
const spaceIds = response.data.map((r) => r.space_id);
58+
const response2 = await client
59+
.from("Space")
60+
.select("id, url")
61+
.in("id", spaceIds);
62+
if (response2.error) throw response2.error;
63+
return Object.fromEntries(response2.data.map(({ id, url }) => [id, url]));
64+
};
65+
66+
// Use readImportedSourceIdentity when it's integrated.
67+
const getSpaceIdOf = (nodeId: string): number | undefined => undefined;
68+
69+
const getSyncedRelationLocalIds = async (
70+
client: SupabaseClient,
71+
spaceId: number,
72+
): Promise<string[]> => {
73+
const response = await client
74+
.from("my_concepts")
75+
.select("source_local_id")
76+
.eq("space_id", spaceId)
77+
.eq("arity", 2); // intentionally include both schemas and instances
78+
if (response.error) throw response.error;
79+
return response.data.map((r) => r.source_local_id);
80+
};
81+
82+
const ensureRelationsSynced = async ({
83+
client,
84+
spaceId,
85+
relations,
86+
existing,
87+
isImportedFrom,
88+
spaceUids,
89+
}: {
90+
client: SupabaseClient;
91+
spaceId: number;
92+
relations: ReifiedRelationDataWithRelId[];
93+
existing: Set<string>;
94+
isImportedFrom: (id: string) => number | undefined;
95+
spaceUids: Record<number, string>;
96+
}): Promise<void> => {
97+
const currentSpaceUri = spaceUids[spaceId];
98+
const missing = relations.filter((r) => !existing.has(r.relationId));
99+
const asCrossApp: CrossAppRelation[] = missing.map((r) => {
100+
const sourceSpaceUid = spaceUids[isImportedFrom(r.sourceUid) || 0];
101+
const destinationSpaceUid =
102+
spaceUids[isImportedFrom(r.destinationUid) || 0];
103+
const sourceId =
104+
sourceSpaceUid === undefined
105+
? r.sourceUid
106+
: spaceUriAndLocalIdToRid(sourceSpaceUid, r.sourceUid);
107+
const destinationId =
108+
destinationSpaceUid === undefined
109+
? r.destinationUid
110+
: spaceUriAndLocalIdToRid(destinationSpaceUid, r.destinationUid);
111+
const relData = window.roamAlphaAPI.pull(
112+
"[:create/time :edit/time {:create/user [:user/uid]}]",
113+
`[:block/uid "${r.relationId}"]`,
114+
) as unknown as {
115+
":create/time": number;
116+
":edit/time": number;
117+
":create/user": { ":user/uid": string };
118+
};
119+
const userUid = relData[":create/user"][":user/uid"];
120+
121+
return {
122+
localId: r.relationId,
123+
relationType: { localId: r.hasSchema },
124+
source: { localId: sourceId },
125+
destination: { localId: destinationId },
126+
author: { localId: userUid },
127+
createdAt: new Date(relData[":create/time"]),
128+
modifiedAt: new Date(relData[":edit/time"]),
129+
};
130+
});
131+
const asDbData = asCrossApp.map((r) =>
132+
crossAppRelationToDbConcept(r, currentSpaceUri),
133+
);
134+
const response = await client.rpc("upsert_concepts", {
135+
v_space_id: spaceId,
136+
data: asDbData,
137+
});
138+
if (response.error) throw response.error;
139+
relations.map((r) => {
140+
existing.add(r.relationId);
141+
});
142+
};
143+
144+
const ensureRelationSchemasSynced = async ({
145+
client,
146+
spaceId,
147+
relationSchemas,
148+
existing,
149+
nodeSchemaById,
150+
}: {
151+
client: SupabaseClient;
152+
spaceId: number;
153+
relationSchemas: DiscourseRelation[];
154+
existing: Set<string>;
155+
nodeSchemaById: Record<string, CrossAppNodeSchema>;
156+
}): Promise<void> => {
157+
const missing = relationSchemas.filter((r) => !existing.has(r.id));
158+
const asCrossApp: CrossAppRelationTripleSchema[] = missing.map((r) => {
159+
const relData = window.roamAlphaAPI.pull(
160+
"[:create/time :edit/time {:create/user [:user/uid]}]",
161+
`[:block/uid "${r.id}"]`,
162+
) as unknown as {
163+
":create/time": number;
164+
":edit/time": number;
165+
":create/user": { ":user/uid": string };
166+
};
167+
const userUid = relData[":create/user"][":user/uid"];
168+
169+
return {
170+
localId: r.id,
171+
sourceType: { localId: r.source },
172+
destinationType: { localId: r.destination },
173+
label: r.label,
174+
complement: r.complement,
175+
author: { localId: userUid },
176+
createdAt: new Date(relData[":create/time"]),
177+
modifiedAt: new Date(relData[":edit/time"]),
178+
};
179+
});
180+
const asDbData = asCrossApp
181+
.map((r) => {
182+
const sourceNodeSchema =
183+
"localId" in r.sourceType
184+
? nodeSchemaById[r.sourceType.localId]
185+
: undefined;
186+
const destinationNodeSchema =
187+
"localId" in r.destinationType
188+
? nodeSchemaById[r.destinationType.localId]
189+
: undefined;
190+
return sourceNodeSchema !== undefined &&
191+
destinationNodeSchema !== undefined
192+
? crossAppRelationTripleSchemaToDbConcept({
193+
node: r,
194+
sourceNodeSchema,
195+
destinationNodeSchema,
196+
})
197+
: undefined;
198+
})
199+
.filter((d) => d !== undefined);
200+
const response = await client.rpc("upsert_concepts", {
201+
v_space_id: spaceId,
202+
data: asDbData,
203+
});
204+
if (response.error) throw response.error;
205+
asDbData.map((d) => {
206+
if (d.source_local_id) existing.add(d.source_local_id);
207+
});
208+
};
209+
210+
const getCrossAppNodeSchemaById = (): Record<string, CrossAppNodeSchema> => {
211+
const allNodeSchemas = getDiscourseNodes();
212+
const result: CrossAppNodeSchema[] = allNodeSchemas.map((s) => {
213+
const relData = window.roamAlphaAPI.pull(
214+
"[:create/time :edit/time {:create/user [:user/uid]}]",
215+
`[:block/uid "${s.type}"]`,
216+
) as unknown as {
217+
":create/time": number;
218+
":edit/time": number;
219+
":create/user": { ":user/uid": string };
220+
};
221+
const userUid = relData[":create/user"][":user/uid"];
222+
return {
223+
localId: s.type,
224+
label: s.text,
225+
author: { localId: userUid },
226+
createdAt: new Date(relData[":create/time"]),
227+
};
228+
});
229+
return Object.fromEntries(result.map((r) => [r.localId, r]));
230+
};
231+
232+
export const publishCorrespondingRelations = async ({
233+
client,
234+
spaceId,
235+
groupIds,
236+
forNodeIds,
237+
}: {
238+
client: DGSupabaseClient;
239+
spaceId: number;
240+
groupIds: string[];
241+
forNodeIds?: Set<string>;
242+
}): Promise<string[] | null> => {
243+
const allRelationsSchemas = getDiscourseRelations();
244+
const nodeSchemaById = getCrossAppNodeSchemaById();
245+
// Should we even handle non-reified relations?
246+
// I need a way to know if a relation is imported.
247+
const allRelations = await getReifiedRelations();
248+
const failedGroupIds: string[] = [];
249+
const sourceIdOfNodes: Record<string, number> = {};
250+
const syncedRelationIds = new Set(
251+
await getSyncedRelationLocalIds(client, spaceId),
252+
);
253+
const isImportedFrom = (nodeLocalId: string): number => {
254+
let cached = sourceIdOfNodes[nodeLocalId];
255+
if (cached === undefined) {
256+
cached = sourceIdOfNodes[nodeLocalId] =
257+
getSpaceIdOf(nodeLocalId) || spaceId;
258+
}
259+
return cached === spaceId ? 0 : cached;
260+
};
261+
const relations =
262+
forNodeIds !== undefined
263+
? allRelations.filter(
264+
(r) =>
265+
r.importedFromRid === undefined &&
266+
(forNodeIds.has(r.sourceUid) || forNodeIds.has(r.destinationUid)),
267+
)
268+
: allRelations.filter((r) => r.importedFromRid === undefined);
269+
for (const groupId of groupIds) {
270+
const groupSpaces = await getGroupSpaceIdAndUrls(client, groupId);
271+
const publishedIds = new Set(
272+
await getAllPublishedIds(client, spaceId, groupId),
273+
);
274+
const relsBetweenPublished = relations.filter(
275+
(r) =>
276+
(publishedIds.has(r.sourceUid) ||
277+
(isImportedFrom(r.sourceUid) || 0) in groupSpaces) &&
278+
(publishedIds.has(r.destinationUid) ||
279+
(isImportedFrom(r.destinationUid) || 0) in groupSpaces),
280+
);
281+
await ensureRelationsSynced({
282+
client,
283+
spaceId,
284+
relations: relsBetweenPublished,
285+
existing: syncedRelationIds,
286+
isImportedFrom,
287+
spaceUids: groupSpaces,
288+
});
289+
const relationSchemaIds = new Set(
290+
relsBetweenPublished.map((r) => r.hasSchema),
291+
);
292+
const relationSchemaTriples = allRelationsSchemas.filter((r) =>
293+
relationSchemaIds.has(r.id),
294+
);
295+
await ensureRelationSchemasSynced({
296+
client,
297+
spaceId,
298+
relationSchemas: relationSchemaTriples,
299+
existing: syncedRelationIds,
300+
nodeSchemaById,
301+
});
302+
const missingIds = {
303+
...relsBetweenPublished
304+
.map((r) => r.relationId)
305+
.filter((id) => !publishedIds.has(id)),
306+
...relationSchemaTriples
307+
.map((r) => r.id)
308+
.filter((id) => !publishedIds.has(id)),
309+
};
310+
const grantRes = await client.from("ResourceAccess").upsert(
311+
missingIds.map((sourceLocalId) => ({
312+
account_uid: groupId,
313+
source_local_id: sourceLocalId,
314+
space_id: spaceId,
315+
})),
316+
{ ignoreDuplicates: true },
317+
);
318+
if (!isIgnorableUpsertError(grantRes.error)) {
319+
failedGroupIds.push(groupId);
320+
}
321+
}
322+
return failedGroupIds.length ? failedGroupIds : null;
323+
};
324+
16325
// 23505 = unique_violation: the grant already exists, which counts as success.
17326
const isIgnorableUpsertError = (error: { code?: string } | null): boolean =>
18327
!error || error.code === "23505";
@@ -119,6 +428,12 @@ export const publishNodesToGroups = async ({
119428
continue;
120429
}
121430

431+
await publishCorrespondingRelations({
432+
client,
433+
spaceId,
434+
groupIds: targetGroupIds,
435+
forNodeIds: new Set(syncedNodeUids),
436+
});
122437
result.okGroupIds.push(groupId);
123438
}
124439

0 commit comments

Comments
 (0)