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