11/* eslint-disable @typescript-eslint/naming-convention */
2+ import { TFile } from "obsidian" ;
23import type { DGSupabaseClient } from "@repo/database/lib/client" ;
34import type DiscourseGraphPlugin from "~/index" ;
45import { getLoggedInClient , getSupabaseContext } from "./supabaseContext" ;
56import type { DiscourseNode , ImportableNode } from "~/types" ;
67import generateUid from "~/utils/generateUid" ;
8+ import { QueryEngine } from "~/services/QueryEngine" ;
79
810export const getAvailableGroups = async (
911 client : DGSupabaseClient ,
@@ -225,6 +227,7 @@ export const fetchNodeMetadata = async ({
225227 } ;
226228} ;
227229
230+
228231const sanitizeFileName = ( fileName : string ) : string => {
229232 // Remove invalid characters for file names
230233 return fileName
@@ -413,9 +416,9 @@ const processFileContent = async ({
413416 sourceSpaceId : number ;
414417 rawContent : string ;
415418 filePath : string ;
416- } ) : Promise < void > => {
419+ } ) : Promise < { file : TFile ; error ?: string } > => {
417420 const { frontmatter } = parseFrontmatter ( rawContent ) ;
418- const sourceNodeTypeId = frontmatter . nodeTypeId
421+ const sourceNodeTypeId = frontmatter . nodeTypeId ;
419422
420423 let mappedNodeTypeId : string | undefined ;
421424 if ( sourceNodeTypeId && typeof sourceNodeTypeId === "string" ) {
@@ -427,14 +430,20 @@ const processFileContent = async ({
427430 } ) ;
428431 }
429432
430- const file = await plugin . app . vault . create ( filePath , rawContent ) ;
431-
433+ let file : TFile | null = plugin . app . vault . getFileByPath ( filePath ) ;
434+ if ( ! file ) {
435+ file = await plugin . app . vault . create ( filePath , rawContent ) ;
436+ } else {
437+ await plugin . app . vault . modify ( file , rawContent ) ;
438+ }
432439 await plugin . app . fileManager . processFrontMatter ( file , ( fm ) => {
433440 if ( mappedNodeTypeId !== undefined ) {
434441 ( fm as Record < string , unknown > ) . nodeTypeId = mappedNodeTypeId ;
435442 }
436443 ( fm as Record < string , unknown > ) . importedFromSpaceId = sourceSpaceId ;
437444 } ) ;
445+
446+ return { file } ;
438447} ;
439448
440449export const importSelectedNodes = async ( {
@@ -454,6 +463,8 @@ export const importSelectedNodes = async ({
454463 throw new Error ( "Cannot get Supabase context" ) ;
455464 }
456465
466+ const queryEngine = new QueryEngine ( plugin . app ) ;
467+
457468 let successCount = 0 ;
458469 let failedCount = 0 ;
459470
@@ -480,6 +491,12 @@ export const importSelectedNodes = async ({
480491 // Process each node in this space
481492 for ( const node of nodes ) {
482493 try {
494+ // Check if file already exists by nodeInstanceId + importedFromSpaceId
495+ const existingFile = queryEngine . findExistingImportedFile (
496+ node . nodeInstanceId ,
497+ node . spaceId ,
498+ ) ;
499+
483500 // Fetch the file name (direct variant) and content (full variant)
484501 const fileName = await fetchNodeContent ( {
485502 client,
@@ -504,34 +521,62 @@ export const importSelectedNodes = async ({
504521 } ) ;
505522
506523 if ( content === null ) {
507- console . warn (
508- `No full variant found for node ${ node . nodeInstanceId } ` ,
509- ) ;
524+ console . warn ( `No full variant found for node ${ node . nodeInstanceId } ` ) ;
510525 failedCount ++ ;
511526 continue ;
512527 }
513528
514529 // Sanitize file name
515530 const sanitizedFileName = sanitizeFileName ( fileName ) ;
516- const filePath = `${ importFolderPath } /${ sanitizedFileName } .md` ;
517-
518- // Check if file already exists and handle duplicates
519- let finalFilePath = filePath ;
520- let counter = 1 ;
521- while ( await plugin . app . vault . adapter . exists ( finalFilePath ) ) {
522- finalFilePath = `${ importFolderPath } /${ sanitizedFileName } (${ counter } ).md` ;
523- counter ++ ;
531+ let finalFilePath : string ;
532+
533+ if ( existingFile ) {
534+ // Update existing file - use its current path
535+ finalFilePath = existingFile . path ;
536+ } else {
537+ // Create new file in the import folder
538+ finalFilePath = `${ importFolderPath } /${ sanitizedFileName } .md` ;
539+
540+ // Check if file path already exists (edge case: same title but different nodeInstanceId)
541+ let counter = 1 ;
542+ while ( await plugin . app . vault . adapter . exists ( finalFilePath ) ) {
543+ finalFilePath = `${ importFolderPath } /${ sanitizedFileName } (${ counter } ).md` ;
544+ counter ++ ;
545+ }
524546 }
525547
526548 // Process the file content (maps nodeTypeId, handles frontmatter)
527- // This creates the file and uses Obsidian's processFrontMatter API
528- await processFileContent ( {
549+ // This updates existing file or creates new one
550+ const result = await processFileContent ( {
529551 plugin,
530552 client,
531553 sourceSpaceId : node . spaceId ,
532554 rawContent : content ,
533555 filePath : finalFilePath ,
534556 } ) ;
557+
558+ if ( result . error ) {
559+ console . error (
560+ `Error processing file content for node ${ node . nodeInstanceId } :` ,
561+ result . error ,
562+ ) ;
563+ failedCount ++ ;
564+ continue ;
565+ }
566+
567+ // If title changed and file exists, rename it to match the new title
568+ const processedFile = result . file ;
569+ if ( existingFile && processedFile . basename !== sanitizedFileName ) {
570+ const newPath = `${ importFolderPath } /${ sanitizedFileName } .md` ;
571+ let targetPath = newPath ;
572+ let counter = 1 ;
573+ while ( await plugin . app . vault . adapter . exists ( targetPath ) ) {
574+ targetPath = `${ importFolderPath } /${ sanitizedFileName } (${ counter } ).md` ;
575+ counter ++ ;
576+ }
577+ await plugin . app . fileManager . renameFile ( processedFile , targetPath ) ;
578+ }
579+
535580 successCount ++ ;
536581 } catch ( error ) {
537582 console . error (
@@ -545,3 +590,80 @@ export const importSelectedNodes = async ({
545590
546591 return { success : successCount , failed : failedCount } ;
547592} ;
593+
594+ /**
595+ * Refresh a single imported file by fetching the latest content from the database
596+ * Reuses the same logic as importSelectedNodes by treating it as a single-node import
597+ */
598+ export const refreshImportedFile = async ( {
599+ plugin,
600+ file,
601+ client,
602+ } : {
603+ plugin : DiscourseGraphPlugin ;
604+ file : TFile ;
605+ client ?: DGSupabaseClient ;
606+ } ) : Promise < { success : boolean ; error ?: string } > => {
607+ const supabaseClient = client || await getLoggedInClient ( plugin ) ;
608+ if ( ! supabaseClient ) {
609+ throw new Error ( "Cannot get Supabase client" ) ;
610+ }
611+ const cache = plugin . app . metadataCache . getFileCache ( file ) ;
612+ const frontmatter = cache ?. frontmatter ;
613+ const spaceName = await getSpaceName ( supabaseClient , frontmatter ?. importedFromSpaceId as number ) ;
614+ const result = await importSelectedNodes ( { plugin, selectedNodes : [ {
615+ nodeInstanceId : frontmatter ?. nodeInstanceId as string ,
616+ title : file . basename ,
617+ spaceId : frontmatter ?. importedFromSpaceId as number ,
618+ spaceName : spaceName ,
619+ groupId : frontmatter ?. publishedToGroups ?. [ 0 ] as string ,
620+ selected : false ,
621+ } ] } ) ;
622+ return { success : result . success > 0 , error : result . failed > 0 ? "Failed to refresh imported file" : undefined } ;
623+ } ;
624+
625+ /**
626+ * Refresh all imported files in the vault
627+ */
628+ export const refreshAllImportedFiles = async (
629+ plugin : DiscourseGraphPlugin ,
630+ ) : Promise < { success : number ; failed : number ; errors : Array < { file : string ; error : string } > } > => {
631+ const allFiles = plugin . app . vault . getMarkdownFiles ( ) ;
632+ const importedFiles : TFile [ ] = [ ] ;
633+ const client = await getLoggedInClient ( plugin ) ;
634+ if ( ! client ) {
635+ throw new Error ( "Cannot get Supabase client" ) ;
636+ }
637+ // Find all imported files
638+ for ( const file of allFiles ) {
639+ const cache = plugin . app . metadataCache . getFileCache ( file ) ;
640+ const frontmatter = cache ?. frontmatter ;
641+ if ( frontmatter ?. importedFromSpaceId && frontmatter ?. nodeInstanceId ) {
642+ importedFiles . push ( file ) ;
643+ }
644+ }
645+
646+ if ( importedFiles . length === 0 ) {
647+ return { success : 0 , failed : 0 , errors : [ ] } ;
648+ }
649+
650+ let successCount = 0 ;
651+ let failedCount = 0 ;
652+ const errors : Array < { file : string ; error : string } > = [ ] ;
653+
654+ // Refresh each file
655+ for ( const file of importedFiles ) {
656+ const result = await refreshImportedFile ( { plugin, file, client } ) ;
657+ if ( result . success ) {
658+ successCount ++ ;
659+ } else {
660+ failedCount ++ ;
661+ errors . push ( {
662+ file : file . path ,
663+ error : result . error || "Unknown error" ,
664+ } ) ;
665+ }
666+ }
667+
668+ return { success : successCount , failed : failedCount , errors } ;
669+ } ;
0 commit comments