diff --git a/prisma/migrations/20260616163116_adding_search_content_to_notes/migration.sql b/prisma/migrations/20260616163116_adding_search_content_to_notes/migration.sql deleted file mode 100644 index 6ab5e2f..0000000 --- a/prisma/migrations/20260616163116_adding_search_content_to_notes/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- CreateIndex -CREATE INDEX "Note_userId_updatedAt_searchContent_idx" ON "Note"("userId", "updatedAt", "searchContent"); diff --git a/prisma/migrations/20260620040952_add_folders_to_notes_relation/migration.sql b/prisma/migrations/20260620040952_add_folders_to_notes_relation/migration.sql new file mode 100644 index 0000000..702cd9d --- /dev/null +++ b/prisma/migrations/20260620040952_add_folders_to_notes_relation/migration.sql @@ -0,0 +1,34 @@ +/* + Warnings: + + - Added the required column `folderId` to the `Note` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Note" ADD COLUMN "folderId" TEXT NOT NULL; + +-- CreateTable +CREATE TABLE "Folder" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "color" TEXT NOT NULL DEFAULT '#ffffff', + "isDeleted" BOOLEAN NOT NULL DEFAULT false, + "deletedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "userId" INTEGER NOT NULL, + + CONSTRAINT "Folder_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "Folder_userId_idx" ON "Folder"("userId"); + +-- CreateIndex +CREATE INDEX "Folder_updatedAt_idx" ON "Folder"("updatedAt"); + +-- AddForeignKey +ALTER TABLE "Note" ADD CONSTRAINT "Note_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Folder" ADD CONSTRAINT "Folder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3701545..09bec80 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,12 +14,14 @@ datasource db { } model User { - id Int @id @default(autoincrement()) - email String @unique + id Int @id @default(autoincrement()) + email String @unique name String? password_hash String hashed_refresh_token String? + // Relations notes Note[] + folders Folder[] } model Note { @@ -28,10 +30,29 @@ model Note { content Json searchContent String @default("") version Int @default(1) - updatedAt DateTime + folderId String? + folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull) + updatedAt DateTime @updatedAt isDeleted Boolean @default(false) userId Int user User @relation(fields: [userId], references: [id]) @@index([userId, updatedAt, searchContent]) // Critical composite for fast delta indexing } + +model Folder { + id String @id @default(uuid()) + name String + color String @default("#ffffff") + isDeleted Boolean @default(false) + deletedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + userId Int + user User @relation(fields: [userId], references: [id]) + // Relations + notes Note[] + + @@index([userId]) + @@index([updatedAt]) +} diff --git a/src/app.module.ts b/src/app.module.ts index d09383e..843c30f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,6 +7,7 @@ import { NotesModule } from './notes/notes.module'; import { AuthModule } from './auth/auth.module'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { TransformResponseInterceptor } from './interceptors/transformResponse.interceptor'; +import { FoldersModule } from './folders/folders.module'; @Module({ imports: [ @@ -14,7 +15,8 @@ import { TransformResponseInterceptor } from './interceptors/transformResponse.i envFilePath: '.env.development.local', }), NotesModule, - AuthModule + AuthModule, + FoldersModule ], controllers: [AppController], providers: [ diff --git a/src/folders/dto/base-folder.dto.ts b/src/folders/dto/base-folder.dto.ts new file mode 100644 index 0000000..f5ebd50 --- /dev/null +++ b/src/folders/dto/base-folder.dto.ts @@ -0,0 +1,34 @@ +import { Type } from 'class-transformer'; +import { + IsBoolean, + IsDate, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; + +export class BaseFolderDto { + @IsUUID() + @IsNotEmpty() + id!: string; // This will be generated on the client side + + @IsString() + @IsNotEmpty() + name!: string; + + @IsOptional() + @IsString() + color!: string; + + @IsDate() + @Type(() => Date) + updatedAt!: Date; + + @IsBoolean() + isDeleted!: boolean; + + @IsDate() + @Type(() => Date) + deletedAt: Date; +} diff --git a/src/folders/dto/create-folder.dto.ts b/src/folders/dto/create-folder.dto.ts new file mode 100644 index 0000000..40e1c8f --- /dev/null +++ b/src/folders/dto/create-folder.dto.ts @@ -0,0 +1,3 @@ +import { BaseFolderDto } from './base-folder.dto'; + +export class CreateFolderDto extends BaseFolderDto {} diff --git a/src/folders/dto/update-folder.dto.ts b/src/folders/dto/update-folder.dto.ts new file mode 100644 index 0000000..4bbf61b --- /dev/null +++ b/src/folders/dto/update-folder.dto.ts @@ -0,0 +1,3 @@ +import { BaseFolderDto } from './base-folder.dto'; + +export class UpdateFolderDto extends BaseFolderDto {} diff --git a/src/folders/entities/folder.entity.ts b/src/folders/entities/folder.entity.ts new file mode 100644 index 0000000..1a899ff --- /dev/null +++ b/src/folders/entities/folder.entity.ts @@ -0,0 +1 @@ +export class Folder {} diff --git a/src/folders/folders.controller.spec.ts b/src/folders/folders.controller.spec.ts new file mode 100644 index 0000000..b8e5067 --- /dev/null +++ b/src/folders/folders.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FoldersController } from './folders.controller'; +import { FoldersService } from './folders.service'; + +describe('FoldersController', () => { + let controller: FoldersController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [FoldersController], + providers: [FoldersService], + }).compile(); + + controller = module.get(FoldersController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/folders/folders.controller.ts b/src/folders/folders.controller.ts new file mode 100644 index 0000000..82c053d --- /dev/null +++ b/src/folders/folders.controller.ts @@ -0,0 +1,48 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + UseGuards, +} from '@nestjs/common'; +import { FoldersService } from './folders.service'; +import { CreateFolderDto } from './dto/create-folder.dto'; +import { UpdateFolderDto } from './dto/update-folder.dto'; +import { type RequestUser } from '@/auth/types/JwtPayload'; +import { GetUser } from '@/auth/decorators/get-user.decorator'; +import { JwtAuthGuard } from '@/auth/guards/jwt.auth-guard'; + +@UseGuards(JwtAuthGuard) +@Controller('folders') +export class FoldersController { + constructor(private readonly foldersService: FoldersService) {} + + @Post() + create( + @GetUser() user: RequestUser, + @Body() createFolderDto: CreateFolderDto, + ) { + return this.foldersService.create(user.id, createFolderDto); + } + + @Get(':id') + findOne(@GetUser() user: RequestUser, @Param('id') id: string) { + return this.foldersService.findOne(user.id, id); + } + + @Patch(':id') + update( + @GetUser() user: RequestUser, + @Body() updateFolderDto: UpdateFolderDto, + ) { + return this.foldersService.update(user.id, updateFolderDto); + } + + @Delete(':id') + remove(@GetUser() user: RequestUser, @Param('id') id: string) { + return this.foldersService.remove(user.id, id); + } +} diff --git a/src/folders/folders.module.ts b/src/folders/folders.module.ts new file mode 100644 index 0000000..d20b696 --- /dev/null +++ b/src/folders/folders.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { FoldersService } from './folders.service'; +import { FoldersController } from './folders.controller'; +import { Prisma } from '@/prisma/prisma.service'; + +@Module({ + controllers: [FoldersController], + providers: [FoldersService, Prisma], +}) +export class FoldersModule {} diff --git a/src/folders/folders.service.spec.ts b/src/folders/folders.service.spec.ts new file mode 100644 index 0000000..4a036ca --- /dev/null +++ b/src/folders/folders.service.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FoldersService } from './folders.service'; +import { Prisma } from '@/prisma/prisma.service'; + +describe('FoldersService', () => { + let service: FoldersService; + let prisma: Prisma; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [FoldersService], + }).compile(); + + service = module.get(FoldersService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/folders/folders.service.ts b/src/folders/folders.service.ts new file mode 100644 index 0000000..8a69b35 --- /dev/null +++ b/src/folders/folders.service.ts @@ -0,0 +1,85 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { CreateFolderDto } from './dto/create-folder.dto'; +import { UpdateFolderDto } from './dto/update-folder.dto'; +import { Prisma } from '@/prisma/prisma.service'; + +@Injectable() +export class FoldersService { + constructor(private prisma: Prisma) {} + + async create(userId: number, createFolderDto: CreateFolderDto) { + const found = await this.prisma.folder.findUnique({ + where: { + id: createFolderDto.id, + userId: userId, + isDeleted: false, + }, + select: { + id: true, + name: true, + color: true, + updatedAt: true, + isDeleted: true, + }, + }); + if (found) { + return found; + } + const created = await this.prisma.folder.create({ + data: { + ...createFolderDto, + userId: userId, + }, + }); + return created; + } + + async findOne(userId: number, folderId: string) { + const found = await this.prisma.folder.findFirst({ + where: { + id: folderId, + userId, + }, + }); + if (!found) { + throw new NotFoundException('Folder not found'); + } + return found; + } + + async update(userId: number, updateFolderDto: UpdateFolderDto) { + const found = await this.prisma.folder.findFirst({ + where: { + id: updateFolderDto.id, + userId, + }, + }); + if (!found) { + throw new NotFoundException('Folder not found'); + } + const updated = await this.prisma.folder.update({ + where: { + id: found.id, + userId, + }, + data: { + ...updateFolderDto, + userId, + }, + }); + return updated; + } + + async remove(userId: number, folderId: string) { + const folderToDelete = await this.prisma.folder.delete({ + where: { + id: folderId, + userId, + }, + }); + if (!folderToDelete) { + throw new NotFoundException('Folder Not found'); + } + return { data: null, message: 'Folder successfully deleted' }; + } +} diff --git a/src/notes/dto/base-note.dto.ts b/src/notes/dto/base-note.dto.ts index b6d65dc..9ec4478 100644 --- a/src/notes/dto/base-note.dto.ts +++ b/src/notes/dto/base-note.dto.ts @@ -6,6 +6,7 @@ import { IsUUID, IsBoolean, IsObject, + IsOptional, } from 'class-validator'; import { Prisma } from 'generated/prisma/browser'; @@ -18,6 +19,10 @@ export class BaseNoteDto { @IsNotEmpty() title!: string; + @IsOptional() + @IsString() + folderId!: string | null; + @IsObject() @IsNotEmpty() content!: Prisma.InputJsonValue; // To Map seamlessly to Prisma's native JSON typing diff --git a/src/notes/dto/sync-notes.dto.ts b/src/notes/dto/sync-notes.dto.ts index 4b6f923..7b7f7fb 100644 --- a/src/notes/dto/sync-notes.dto.ts +++ b/src/notes/dto/sync-notes.dto.ts @@ -7,6 +7,7 @@ import { ValidateNested, } from 'class-validator'; import { BaseNoteDto } from './base-note.dto'; +import { BaseFolderDto } from '@/folders/dto/base-folder.dto'; export class SyncNotesDto { @IsDate() @@ -18,6 +19,12 @@ export class SyncNotesDto { @IsOptional() cursor?: string; + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => BaseFolderDto) + folders?: BaseFolderDto[]; + @IsArray() @ValidateNested({ each: true }) @Type(() => BaseNoteDto) diff --git a/src/notes/notes.module.ts b/src/notes/notes.module.ts index c555445..125d232 100644 --- a/src/notes/notes.module.ts +++ b/src/notes/notes.module.ts @@ -8,4 +8,4 @@ import { SyncService } from './sync.service'; controllers: [NotesController], providers: [NotesService, SyncService, Prisma], }) -export class NotesModule { } +export class NotesModule {} diff --git a/src/notes/notes.service.spec.ts b/src/notes/notes.service.spec.ts index 1dab8b0..b5892f8 100644 --- a/src/notes/notes.service.spec.ts +++ b/src/notes/notes.service.spec.ts @@ -6,7 +6,6 @@ import { SyncService } from './sync.service'; describe('NotesService', () => { let service: NotesService; - let prisma: Prisma; // 1. Create a Mock Prisma Object const mockPrisma = { @@ -54,7 +53,9 @@ describe('NotesService', () => { it('should throw NotFoundException if note does not exist', async () => { mockPrisma.note.findUnique.mockResolvedValue(null); - await expect(service.findOne(123, '999')).rejects.toThrow(NotFoundException); + await expect(service.findOne(123, '999')).rejects.toThrow( + NotFoundException, + ); }); }); -}); \ No newline at end of file +}); diff --git a/src/notes/sync.service.ts b/src/notes/sync.service.ts index 923bb12..ec0073a 100644 --- a/src/notes/sync.service.ts +++ b/src/notes/sync.service.ts @@ -1,142 +1,230 @@ import { Prisma } from '../prisma/prisma.service'; import { Injectable } from '@nestjs/common'; import { SyncNotesDto } from './dto/sync-notes.dto'; -import { NoteWhereInput, NoteWhereUniqueInput } from 'generated/prisma/models'; +import { NoteWhereUniqueInput } from 'generated/prisma/models'; +import { UpstreamResult } from '@/types'; +import { Folder } from '@prisma/client'; @Injectable() export class SyncService { + PAGE_SIZE = 100; constructor(private prisma: Prisma) {} - async processSync(userId: number, incomingNotes: SyncNotesDto) { - const clientChanges = incomingNotes.notes || []; - - const lastSyncedAt = incomingNotes.lastSyncedAt - ? incomingNotes.lastSyncedAt + async processSync(userId: number, incomingPayload: SyncNotesDto) { + const lastSyncedAt = incomingPayload.lastSyncedAt + ? new Date(incomingPayload.lastSyncedAt) : new Date(0); - const serverTimeCheckpoint = new Date(); - const PAGE_SIZE = 100; + // 1. Process all incoming modifications from the client in a transaction + const syncTracking = await this.handleUpstreamReconciliation( + userId, + incomingPayload, + serverTimeCheckpoint, + ); + + //2. Fetch changes that happened on the server side since the client's last sync + const downstreamFolders = await this.fetchDownstreamFolders( + userId, + lastSyncedAt, + serverTimeCheckpoint, + syncTracking, + ); + + const { + notes: downstreamNotes, + nextCursor, + hasMore, + } = await this.fetchDownstreamNotes( + userId, + lastSyncedAt, + serverTimeCheckpoint, + incomingPayload.cursor, + syncTracking, + ); + + return { + processedFolderIds: syncTracking.processedFolderIds, + processedNoteIds: syncTracking.processedNoteIds, + folderConflicts: syncTracking.folderConflicts, + noteConflicts: syncTracking.noteConflicts, + folders: downstreamFolders, + notes: downstreamNotes, + nextCursor, + hasMore, + serverTime: serverTimeCheckpoint.toString(), + }; + } + + private async handleUpstreamReconciliation( + userId: number, + payload: SyncNotesDto, + serverTime: Date, + ): Promise { + const clientFolders = payload.folders || []; + const clientNotes = payload.notes || []; - const processedIds: string[] = []; - const conflicts: string[] = []; + const tracking: UpstreamResult = { + processedFolderIds: [], + processedNoteIds: [], + folderConflicts: [], + noteConflicts: [], + }; - // Phase 1: Upstream reconciliation Loop await this.prisma.$transaction(async (tx) => { - for (const clientNote of clientChanges) { - const serverNote = await tx.note.findUnique({ - where: { - id: clientNote.id, - }, - select: { - id: true, - userId: true, - updatedAt: true, - }, + for (const clientFolder of clientFolders) { + const serverFolder = await tx.folder.findUnique({ + where: { id: clientFolder.id }, + select: { id: true, userId: true, updatedAt: true }, }); - if (serverNote) { - if (serverNote.userId !== userId) continue; - - // Last Write Wins execution based on comparing LastUpdated timestamps - if (clientNote.updatedAt < serverNote.updatedAt) { - conflicts.push(clientNote.id); + if (serverFolder) { + if (serverFolder.userId !== userId) continue; + if (serverFolder.updatedAt < serverFolder.updatedAt) { + tracking.folderConflicts.push(serverFolder.id); continue; } - await tx.note.update({ - where: { - id: clientNote.id, - }, + await tx.folder.update({ + where: { id: clientFolder.id }, data: { - title: clientNote.title, - version: 1, - content: clientNote.content, - searchContent: clientNote.searchContent, - isDeleted: clientNote.isDeleted, - updatedAt: clientNote.updatedAt, + ...clientFolder, + color: clientFolder.color || '#ffffff', + deletedAt: clientFolder.isDeleted ? serverTime : null, + updatedAt: clientFolder.updatedAt, }, }); } else { - if (clientNote.isDeleted) { - processedIds.push(clientNote.id); + if (clientFolder.isDeleted) { + tracking.processedFolderIds.push(clientFolder.id); continue; } - await tx.note.create({ + await tx.folder.create({ data: { - id: clientNote.id, + ...clientFolder, userId: userId, - title: clientNote.title, - version: 1, - content: clientNote.content, - searchContent: clientNote.searchContent, - isDeleted: clientNote.isDeleted, - updatedAt: clientNote.updatedAt, }, }); - processedIds.push(clientNote.id); } + tracking.processedFolderIds.push(clientFolder.id); + } + + for (const clientNote of clientNotes) { + if (clientNote.folderId) { + const folderExists = await tx.folder.findFirst({ + where: { id: clientNote.folderId, userId: userId }, + }); + if (!folderExists) clientNote.folderId = null; + } + const serverNote = await tx.note.findUnique({ + where: { id: clientNote.id }, + select: { id: true, userId: true, updatedAt: true }, + }); + if (serverNote) { + if (serverNote.userId !== userId) continue; + if (serverNote.updatedAt < serverNote.updatedAt) { + tracking.noteConflicts.push(clientNote.id); + continue; + } + } + if (!serverNote && clientNote.isDeleted) { + tracking.processedNoteIds.push(clientNote.id); + continue; + } + + await tx.note.upsert({ + where: { id: clientNote.id }, + update: { + ...clientNote, + }, + create: { + ...clientNote, + userId: userId, + }, + }); + tracking.processedNoteIds.push(clientNote.id); } }); + return tracking; + } - // Phase 2: Cursor based downstream fetching - const baseWhereClause: NoteWhereInput = { - userId: userId, - updatedAt: { - gt: lastSyncedAt, - lte: serverTimeCheckpoint, - }, - id: { - notIn: processedIds.concat(conflicts), + private async fetchDownstreamFolders( + userId: number, + lastSyncedAt: Date, + serverTime: Date, + tracking: UpstreamResult, + ): Promise { + const downstreamFolders: Folder[] = await this.prisma.folder.findMany({ + where: { + userId: userId, + updatedAt: { gt: lastSyncedAt, lte: serverTime }, + id: { + notIn: tracking.processedFolderIds.concat(tracking.folderConflicts), + }, }, - }; + }); + const serverWonFolderConflicts: Folder[] = + await this.prisma.folder.findMany({ + where: { id: { in: tracking.folderConflicts } }, + }); + + return [...downstreamFolders, ...serverWonFolderConflicts]; + } - // Decode incoming pagination token if provided by client + private async fetchDownstreamNotes( + userId: number, + lastSyncedAt: Date, + serverTime: Date, + rawCursor: string | undefined, + tracking: UpstreamResult, + ) { let cursorCondition: NoteWhereUniqueInput | undefined = undefined; - if (incomingNotes.cursor) { + if (rawCursor) { try { - const decodedJson = JSON.parse( - Buffer.from(incomingNotes.cursor, 'base64').toString('utf-8'), + const decodedJSON = JSON.parse( + Buffer.from(rawCursor, 'base64').toString('utf-8'), ) as { id: string; updatedAt: string }; - cursorCondition = { id: decodedJson.id }; + cursorCondition = { id: decodedJSON.id }; } catch { cursorCondition = undefined; } } - const downstreamChanges = await this.prisma.note.findMany({ - where: baseWhereClause, - take: PAGE_SIZE + 1, + const downstreamNotes = await this.prisma.note.findMany({ + where: { + userId: userId, + updatedAt: { + gt: lastSyncedAt, + lte: serverTime, + }, + id: { notIn: tracking.processedNoteIds.concat(tracking.noteConflicts) }, + }, + take: this.PAGE_SIZE + 1, cursor: cursorCondition, skip: cursorCondition ? 1 : 0, orderBy: [{ updatedAt: 'asc' }, { id: 'asc' }], }); - const serverWonConflicts = await this.prisma.note.findMany({ - where: { id: { in: conflicts } }, + const serverWonNoteConflicts = await this.prisma.note.findMany({ + where: { + id: { + in: tracking.noteConflicts, + }, + }, }); + const combinedNotes = [...downstreamNotes, ...serverWonNoteConflicts]; + const hasMore = combinedNotes.length > this.PAGE_SIZE; + const targetedNotes = combinedNotes.slice(0, this.PAGE_SIZE); - const combinedChanges = [...downstreamChanges, ...serverWonConflicts]; - const hasMore = combinedChanges.length > PAGE_SIZE; - - // Truncate the current array snapshot back to the explicit PAGE_SIZE limit - const targetedChanges = combinedChanges.slice(0, PAGE_SIZE); let nextCursor: string | null = null; - if (hasMore && targetedChanges.length > 0) { - const lastNoteInBatch = targetedChanges[targetedChanges.length - 1]; + if (hasMore && targetedNotes.length > 0) { + const lastNoteInBatch = targetedNotes[targetedNotes.length - 1]; const cursorPayload = { id: lastNoteInBatch.id, updatedAt: lastNoteInBatch.updatedAt.toISOString(), }; - nextCursor = Buffer.from(JSON.stringify(cursorPayload)).toString( 'base64', ); } - return { - processedIds, - changes: targetedChanges, - nextCursor, - hasMore, - serverTime: serverTimeCheckpoint.toISOString(), - }; + return { notes: targetedNotes, nextCursor, hasMore }; } } diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts index 98f7a24..3916ce5 100644 --- a/src/prisma/prisma.service.ts +++ b/src/prisma/prisma.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { PrismaClient } from "../../generated/prisma/client"; +import { PrismaClient } from '../../generated/prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; -import "dotenv/config" +import 'dotenv/config'; import { env } from 'prisma/config'; @Injectable() @@ -10,7 +10,7 @@ export class Prisma extends PrismaClient { constructor() { const connectionString = env('DATABASE_URL'); const adapter = new PrismaPg({ - connectionString + connectionString, }); super({ adapter }); } diff --git a/src/types/index.ts b/src/types/index.ts index 299ca2c..356dae8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,3 @@ import { SearchNoteResult } from './search-note-result'; - -export type { SearchNoteResult }; +import { UpstreamResult } from './upstream-result'; +export type { SearchNoteResult, UpstreamResult }; diff --git a/src/types/upstream-result.ts b/src/types/upstream-result.ts new file mode 100644 index 0000000..e3f003a --- /dev/null +++ b/src/types/upstream-result.ts @@ -0,0 +1,6 @@ +export type UpstreamResult = { + processedFolderIds: string[]; + processedNoteIds: string[]; + folderConflicts: string[]; + noteConflicts: string[]; +};