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 { 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
422export 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 upsertMissingIds = 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.
17326const isIgnorableUpsertError = ( error : { code ?: string } | null ) : boolean =>
18327 ! error || error . code === "23505" ;
0 commit comments