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,309 @@ 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 missing = relations . filter ( ( r ) => ! existing . has ( r . relationId ) ) ;
100+ const asCrossApp : CrossAppRelation [ ] = missing
101+ . map ( ( r ) => {
102+ const sourceSpaceUid = spaceUids [ isImportedFrom ( r . sourceUid ) || 0 ] ;
103+ const destinationSpaceUid =
104+ spaceUids [ isImportedFrom ( r . destinationUid ) || 0 ] ;
105+ const sourceId =
106+ sourceSpaceUid === undefined
107+ ? r . sourceUid
108+ : spaceUriAndLocalIdToRid ( sourceSpaceUid , r . sourceUid ) ;
109+ const destinationId =
110+ destinationSpaceUid === undefined
111+ ? r . destinationUid
112+ : spaceUriAndLocalIdToRid ( destinationSpaceUid , r . destinationUid ) ;
113+ const relData = window . roamAlphaAPI . pull (
114+ "[:create/time :edit/time {:create/user [:user/uid]}]" ,
115+ `[:block/uid "${ r . relationId } "]` ,
116+ ) as unknown as {
117+ ":create/time" : number ;
118+ ":edit/time" : number ;
119+ ":create/user" : { ":user/uid" : string } ;
120+ } ;
121+ if ( relData == undefined ) return null ;
122+ const userUid = relData [ ":create/user" ] [ ":user/uid" ] ;
123+
124+ return {
125+ localId : r . relationId ,
126+ relationType : r . hasSchema ,
127+ source : sourceId ,
128+ destination : destinationId ,
129+ authorId : userUid ,
130+ createdAt : new Date ( relData [ ":create/time" ] ) ,
131+ modifiedAt : new Date ( relData [ ":edit/time" ] ) ,
132+ } ;
133+ } )
134+ . filter ( ( s ) => s !== null ) ;
135+ if ( asCrossApp . length == 0 ) return ;
136+ const asDbData = asCrossApp . map ( ( r ) => crossAppRelationToDbConcept ( r ) ) ;
137+ const response = await client . rpc ( "upsert_concepts" , {
138+ v_space_id : spaceId ,
139+ data : asDbData ,
140+ } ) ;
141+ if ( response . error ) throw response . error ;
142+ asCrossApp . forEach ( ( r ) => {
143+ existing . add ( r . localId ) ;
144+ } ) ;
145+ } ;
146+
147+ const ensureRelationSchemasSynced = async ( {
148+ client,
149+ spaceId,
150+ relationSchemas,
151+ existing,
152+ nodeSchemaById,
153+ } : {
154+ client : DGSupabaseClient ;
155+ spaceId : number ;
156+ relationSchemas : DiscourseRelation [ ] ;
157+ existing : Set < string > ;
158+ nodeSchemaById : Record < string , CrossAppNodeSchema > ;
159+ } ) : Promise < void > => {
160+ const missing = relationSchemas . filter ( ( r ) => ! existing . has ( r . id ) ) ;
161+ const asCrossApp : CrossAppRelationTripleSchema [ ] = missing
162+ . map ( ( r ) => {
163+ const relData = window . roamAlphaAPI . pull (
164+ "[:create/time :edit/time {:create/user [:user/uid]}]" ,
165+ `[:block/uid "${ r . id } "]` ,
166+ ) as unknown as {
167+ ":create/time" : number ;
168+ ":edit/time" : number ;
169+ ":create/user" : { ":user/uid" : string } ;
170+ } ;
171+ if ( ! relData ) return null ;
172+ const userUid = relData [ ":create/user" ] [ ":user/uid" ] ;
173+
174+ return {
175+ localId : r . id ,
176+ sourceType : r . source ,
177+ destinationType : r . destination ,
178+ label : r . label ,
179+ complement : r . complement ,
180+ authorId : userUid ,
181+ createdAt : new Date ( relData [ ":create/time" ] ) ,
182+ modifiedAt : new Date ( relData [ ":edit/time" ] ) ,
183+ } ;
184+ } )
185+ . filter ( ( x ) => x !== null ) ;
186+ if ( asCrossApp . length == 0 ) return ;
187+ const asDbData = asCrossApp
188+ . map ( ( r ) => {
189+ const sourceSchema = nodeSchemaById [ r . sourceType ] ;
190+ const destinationSchema = nodeSchemaById [ r . destinationType ] ;
191+ if ( ! sourceSchema || ! destinationSchema ) return undefined ;
192+ return crossAppRelationTripleSchemaToDbConcept ( {
193+ node : r ,
194+ sourceNodeSchema : sourceSchema ,
195+ destinationNodeSchema : destinationSchema ,
196+ } ) ;
197+ } )
198+ . filter ( ( d ) => d !== undefined ) ;
199+ const response = await client . rpc ( "upsert_concepts" , {
200+ v_space_id : spaceId ,
201+ data : asDbData ,
202+ } ) ;
203+ if ( response . error ) throw response . error ;
204+ asDbData . map ( ( d ) => {
205+ if ( d . source_local_id ) existing . add ( d . source_local_id ) ;
206+ } ) ;
207+ } ;
208+
209+ const getCrossAppNodeSchemaById = ( ) : Record < string , CrossAppNodeSchema > => {
210+ const allNodeSchemas = getDiscourseNodes ( ) ;
211+ const result : CrossAppNodeSchema [ ] = allNodeSchemas
212+ . 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+ if ( ! relData ) return null ;
222+ const userUid = relData [ ":create/user" ] [ ":user/uid" ] ;
223+ return {
224+ localId : s . type ,
225+ label : s . text ,
226+ authorId : userUid ,
227+ createdAt : new Date ( relData [ ":create/time" ] ) ,
228+ } ;
229+ } )
230+ . filter ( ( s ) => s !== null ) ;
231+ return Object . fromEntries ( result . map ( ( r ) => [ r . localId , r ] ) ) ;
232+ } ;
233+
234+ export const publishCorrespondingRelations = async ( {
235+ client,
236+ spaceId,
237+ groupIds,
238+ forNodeIds,
239+ } : {
240+ client : DGSupabaseClient ;
241+ spaceId : number ;
242+ groupIds : string [ ] ;
243+ forNodeIds ?: Set < string > ;
244+ } ) : Promise < string [ ] | null > => {
245+ const allRelationsSchemas = getDiscourseRelations ( ) ;
246+ const allRelationSchemasById = Object . fromEntries (
247+ allRelationsSchemas . map ( ( s ) => [ s . id , s ] ) ,
248+ ) ;
249+ const nodeSchemaById = getCrossAppNodeSchemaById ( ) ;
250+ // Should we even handle non-reified relations?
251+ // I need a way to know if a relation is imported.
252+ const allRelations = await getReifiedRelations ( ) ;
253+ const failedGroupIds : string [ ] = [ ] ;
254+ const sourceIdOfNodes : Record < string , number > = { } ;
255+ const syncedRelationIds = new Set (
256+ await getSyncedRelationLocalIds ( client , spaceId ) ,
257+ ) ;
258+ const isImportedFrom = ( nodeLocalId : string ) : number => {
259+ let cached = sourceIdOfNodes [ nodeLocalId ] ;
260+ if ( cached === undefined ) {
261+ cached = sourceIdOfNodes [ nodeLocalId ] =
262+ getSpaceIdOf ( nodeLocalId ) || spaceId ;
263+ }
264+ return cached === spaceId ? 0 : cached ;
265+ } ;
266+ const relations =
267+ forNodeIds !== undefined
268+ ? allRelations . filter (
269+ ( r ) =>
270+ r . importedFromRid === undefined &&
271+ ( forNodeIds . has ( r . sourceUid ) || forNodeIds . has ( r . destinationUid ) ) ,
272+ )
273+ : allRelations . filter ( ( r ) => r . importedFromRid === undefined ) ;
274+ for ( const groupId of groupIds ) {
275+ const groupSpaces = await getGroupSpaceIdAndUrls ( client , groupId ) ;
276+ const publishedIds = new Set (
277+ await getAllPublishedIds ( client , spaceId , groupId ) ,
278+ ) ;
279+ const relsBetweenPublishedAll = relations . filter (
280+ ( r ) =>
281+ ( publishedIds . has ( r . sourceUid ) ||
282+ ( isImportedFrom ( r . sourceUid ) || 0 ) in groupSpaces ) &&
283+ ( publishedIds . has ( r . destinationUid ) ||
284+ ( isImportedFrom ( r . destinationUid ) || 0 ) in groupSpaces ) ,
285+ ) ;
286+ const relationSchemaIds = new Set (
287+ relsBetweenPublishedAll
288+ . map ( ( r ) => r . hasSchema )
289+ // filter out deleted schemas
290+ . filter ( ( id ) => id in allRelationSchemasById ) ,
291+ ) ;
292+ const relationSchemaTriples = allRelationsSchemas . filter ( ( r ) =>
293+ relationSchemaIds . has ( r . id ) ,
294+ ) ;
295+ const relsBetweenPublished = relsBetweenPublishedAll . filter ( ( r ) =>
296+ relationSchemaIds . has ( r . hasSchema ) ,
297+ ) ;
298+ await ensureRelationSchemasSynced ( {
299+ client,
300+ spaceId,
301+ relationSchemas : relationSchemaTriples ,
302+ existing : syncedRelationIds ,
303+ nodeSchemaById,
304+ } ) ;
305+ await ensureRelationsSynced ( {
306+ client,
307+ spaceId,
308+ relations : relsBetweenPublished ,
309+ existing : syncedRelationIds ,
310+ isImportedFrom,
311+ spaceUids : groupSpaces ,
312+ } ) ;
313+ const missingIds = [
314+ ...relsBetweenPublished
315+ . map ( ( r ) => r . relationId )
316+ . filter ( ( id ) => ! publishedIds . has ( id ) ) ,
317+ ...relationSchemaTriples
318+ . map ( ( r ) => r . id )
319+ . filter ( ( id ) => ! publishedIds . has ( id ) ) ,
320+ ] ;
321+ const grantRes = await client . from ( "ResourceAccess" ) . upsert (
322+ missingIds . map ( ( sourceLocalId ) => ( {
323+ account_uid : groupId ,
324+ source_local_id : sourceLocalId ,
325+ space_id : spaceId ,
326+ } ) ) ,
327+ { ignoreDuplicates : true } ,
328+ ) ;
329+ if ( ! isIgnorableUpsertError ( grantRes . error ) ) {
330+ failedGroupIds . push ( groupId ) ;
331+ }
332+ }
333+ return failedGroupIds . length ? failedGroupIds : null ;
334+ } ;
335+
16336// 23505 = unique_violation: the grant already exists, which counts as success.
17337const isIgnorableUpsertError = ( error : { code ?: string } | null ) : boolean =>
18338 ! error || error . code === "23505" ;
@@ -121,6 +441,12 @@ export const publishNodesToGroups = async ({
121441
122442 result . okGroupIds . push ( groupId ) ;
123443 }
444+ await publishCorrespondingRelations ( {
445+ client,
446+ spaceId,
447+ groupIds : targetGroupIds ,
448+ forNodeIds : new Set ( syncedNodeUids ) ,
449+ } ) ;
124450
125451 result . publishedNodeUids = result . okGroupIds . length > 0 ? syncedNodeUids : [ ] ;
126452 return result ;
0 commit comments