11/* eslint-disable @typescript-eslint/naming-convention */
22import { Notice , TFile } from "obsidian" ;
3- import { DGSupabaseClient } from "@repo/database/lib/client" ;
4- import { Json } from "@repo/database/dbTypes" ;
3+ import type { DGSupabaseClient } from "@repo/database/lib/client" ;
4+ import type { Json } from "@repo/database/dbTypes" ;
55import {
66 getSupabaseContext ,
77 getLoggedInClient ,
8- SupabaseContext ,
8+ type SupabaseContext ,
99} from "./supabaseContext" ;
1010import { default as DiscourseGraphPlugin } from "~/index" ;
1111import { upsertNodesToSupabaseAsContentWithEmbeddings } from "./upsertNodesAsContentWithEmbeddings" ;
1212import {
1313 orderConceptsByDependency ,
1414 discourseNodeInstanceToLocalConcept ,
15+ discourseNodeSchemaToLocalConcept ,
1516} from "./conceptConversion" ;
16- import { LocalConceptDataInput } from "@repo/database/inputTypes" ;
17+ import type { LocalConceptDataInput } from "@repo/database/inputTypes" ;
1718
18- const DEFAULT_TIME = new Date ( "1970-01-01" ) ;
19+ const DEFAULT_TIME = "1970-01-01" ;
1920export type ChangeType = "title" | "content" ;
2021
2122export type ObsidianDiscourseNodeData = {
@@ -101,7 +102,10 @@ const deleteNodesFromSupabase = async (
101102 if ( conceptDeleteError ) {
102103 result . success = false ;
103104 result . errors . concept = conceptDeleteError ;
104- console . error ( "Failed to delete concepts from Supabase:" , conceptDeleteError ) ;
105+ console . error (
106+ "Failed to delete concepts from Supabase:" ,
107+ conceptDeleteError ,
108+ ) ;
105109 }
106110
107111 const { error : contentDeleteError } = await supabaseClient
@@ -159,7 +163,8 @@ const ensureNodeInstanceId = async (
159163
160164 return nodeInstanceId ;
161165} ;
162- const getLastSyncTime = async (
166+
167+ const getLastContentSyncTime = async (
163168 supabaseClient : DGSupabaseClient ,
164169 spaceId : number ,
165170) : Promise < Date > => {
@@ -170,7 +175,22 @@ const getLastSyncTime = async (
170175 . order ( "last_modified" , { ascending : false } )
171176 . limit ( 1 )
172177 . maybeSingle ( ) ;
173- return new Date ( data ?. last_modified || DEFAULT_TIME ) ;
178+ return new Date ( ( data ?. last_modified || DEFAULT_TIME ) + "Z" ) ;
179+ } ;
180+
181+ const getLastSchemaSyncTime = async (
182+ supabaseClient : DGSupabaseClient ,
183+ spaceId : number ,
184+ ) : Promise < Date > => {
185+ const { data } = await supabaseClient
186+ . from ( "Concept" )
187+ . select ( "last_modified" )
188+ . eq ( "space_id" , spaceId )
189+ . eq ( "is_schema" , true )
190+ . order ( "last_modified" , { ascending : false } )
191+ . limit ( 1 )
192+ . maybeSingle ( ) ;
193+ return new Date ( ( data ?. last_modified || DEFAULT_TIME ) + "Z" ) ;
174194} ;
175195
176196type DiscourseNodeInVault = {
@@ -195,7 +215,6 @@ const mergeChangeTypes = (
195215 return Array . from ( merged ) ;
196216} ;
197217
198-
199218/**
200219 * Step 1: Collect all discourse nodes from the vault
201220 * Filters markdown files that have nodeTypeId in frontmatter
@@ -362,7 +381,10 @@ const buildChangedNodesFromNodes = async ({
362381 nodeInstanceIds ,
363382 ) ;
364383
365- const lastSyncTime = await getLastSyncTime ( supabaseClient , context . spaceId ) ;
384+ const lastSyncTime = await getLastContentSyncTime (
385+ supabaseClient ,
386+ context . spaceId ,
387+ ) ;
366388 const changedNodes : ObsidianDiscourseNodeData [ ] = [ ] ;
367389
368390 for ( const node of nodes ) {
@@ -466,12 +488,20 @@ export const createOrUpdateDiscourseEmbedding = async (
466488 throw new Error ( "accountLocalId not found in plugin settings" ) ;
467489 }
468490
469- await syncChangedNodesToSupabase ( {
470- changedNodes : allNodeInstances ,
491+ await upsertNodesToSupabaseAsContentWithEmbeddings ( {
492+ obsidianNodes : allNodeInstances ,
493+ supabaseClient,
494+ context,
495+ accountLocalId,
471496 plugin,
497+ } ) ;
498+
499+ await convertDgToSupabaseConcepts ( {
500+ nodesSince : allNodeInstances ,
472501 supabaseClient,
473502 context,
474503 accountLocalId,
504+ plugin,
475505 } ) ;
476506
477507 console . debug ( "Sync completed successfully" ) ;
@@ -486,43 +516,59 @@ const convertDgToSupabaseConcepts = async ({
486516 supabaseClient,
487517 context,
488518 accountLocalId,
519+ plugin,
489520} : {
490521 nodesSince : ObsidianDiscourseNodeData [ ] ;
491522 supabaseClient : DGSupabaseClient ;
492523 context : SupabaseContext ;
493524 accountLocalId : string ;
525+ plugin : DiscourseGraphPlugin ;
494526} ) : Promise < void > => {
495- // TODO: handling schema (node types and relations) will be handled in the future by ENG-1181
496- // Schema upsert will need allNodeTypes parameter when enabled
527+ const lastSchemaSync = (
528+ await getLastSchemaSyncTime ( supabaseClient , context . spaceId )
529+ ) . getTime ( ) ;
530+ const newNodeTypes = ( plugin . settings . nodeTypes ?? [ ] ) . filter (
531+ ( n ) => n . modified > lastSchemaSync ,
532+ ) ;
497533
498- const nodeInstanceToLocalConcepts = nodesSince . map ( ( node ) => {
499- return discourseNodeInstanceToLocalConcept ( {
534+ const nodesTypesToLocalConcepts = newNodeTypes . map ( ( nodeType ) =>
535+ discourseNodeSchemaToLocalConcept ( {
536+ context,
537+ node : nodeType ,
538+ accountLocalId,
539+ } ) ,
540+ ) ;
541+
542+ const nodeInstanceToLocalConcepts = nodesSince . map ( ( node ) =>
543+ discourseNodeInstanceToLocalConcept ( {
500544 context,
501545 nodeData : node ,
502546 accountLocalId,
503- } ) ;
504- } ) ;
547+ } ) ,
548+ ) ;
505549
506550 const conceptsToUpsert : LocalConceptDataInput [ ] = [
507- // ...nodesTypesToLocalConcepts,
551+ ...nodesTypesToLocalConcepts ,
508552 ...nodeInstanceToLocalConcepts ,
509553 ] ;
510554
511- const { ordered } = orderConceptsByDependency ( conceptsToUpsert ) ;
555+ if ( conceptsToUpsert . length > 0 ) {
556+ const { ordered } = orderConceptsByDependency ( conceptsToUpsert ) ;
512557
513- const { error } = await supabaseClient . rpc ( "upsert_concepts" , {
514- data : ordered as Json ,
515- v_space_id : context . spaceId ,
516- } ) ;
558+ const { error } = await supabaseClient . rpc ( "upsert_concepts" , {
559+ data : ordered as Json ,
560+ v_space_id : context . spaceId ,
561+ } ) ;
517562
518- if ( error ) {
519- const errorMessage =
520- error instanceof Error
521- ? error . message
522- : typeof error === "string"
523- ? error
524- : JSON . stringify ( error , null , 2 ) ;
525- throw new Error ( `upsert_concepts failed: ${ errorMessage } ` ) ;
563+ if ( error ) {
564+ const errorMessage =
565+ error instanceof Error
566+ ? error . message
567+ : typeof error === "string"
568+ ? error
569+ : JSON . stringify ( error , null , 2 ) ;
570+ throw new Error ( `upsert_concepts failed: ${ errorMessage } ` ) ;
571+ }
526572 }
527573} ;
528574
@@ -543,33 +589,29 @@ const syncChangedNodesToSupabase = async ({
543589 context : SupabaseContext ;
544590 accountLocalId : string ;
545591} ) : Promise < void > => {
546- if ( changedNodes . length === 0 ) {
547- console . debug ( "No nodes to sync" ) ;
548- return ;
592+ if ( changedNodes . length > 0 ) {
593+ await upsertNodesToSupabaseAsContentWithEmbeddings ( {
594+ obsidianNodes : changedNodes ,
595+ supabaseClient,
596+ context,
597+ accountLocalId,
598+ plugin,
599+ } ) ;
549600 }
550601
551- await upsertNodesToSupabaseAsContentWithEmbeddings ( {
552- obsidianNodes : changedNodes ,
553- supabaseClient,
554- context,
555- accountLocalId,
556- plugin,
557- } ) ;
558-
559602 // Only upsert concepts for nodes with title changes or new files
560603 // (concepts store the title, so content-only changes don't affect them)
561604 const nodesNeedingConceptUpsert = changedNodes . filter ( ( node ) =>
562605 node . changeTypes . includes ( "title" ) ,
563606 ) ;
564607
565- if ( nodesNeedingConceptUpsert . length > 0 ) {
566- await convertDgToSupabaseConcepts ( {
567- nodesSince : nodesNeedingConceptUpsert ,
568- supabaseClient,
569- context,
570- accountLocalId,
571- } ) ;
572- }
608+ await convertDgToSupabaseConcepts ( {
609+ nodesSince : nodesNeedingConceptUpsert ,
610+ supabaseClient,
611+ context,
612+ accountLocalId,
613+ plugin,
614+ } ) ;
573615} ;
574616
575617/**
@@ -635,10 +677,7 @@ export const syncSpecificFiles = async (
635677 const changeTypesByPath = new Map < string , ChangeType [ ] > ( ) ;
636678 for ( const filePath of filePaths ) {
637679 const existing = changeTypesByPath . get ( filePath ) ?? [ ] ;
638- changeTypesByPath . set (
639- filePath ,
640- mergeChangeTypes ( existing , [ "content" ] ) ,
641- ) ;
680+ changeTypesByPath . set ( filePath , mergeChangeTypes ( existing , [ "content" ] ) ) ;
642681 }
643682
644683 await syncDiscourseNodeChanges ( plugin , changeTypesByPath ) ;
0 commit comments