aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--src/server/repositories/index.ts1
-rw-r--r--src/server/repositories/note.test.ts417
-rw-r--r--src/server/repositories/note.ts325
-rw-r--r--src/server/repositories/types.ts52
4 files changed, 795 insertions, 0 deletions
diff --git a/src/server/repositories/index.ts b/src/server/repositories/index.ts
index 267430f..3256a49 100644
--- a/src/server/repositories/index.ts
+++ b/src/server/repositories/index.ts
@@ -1,5 +1,6 @@
export { cardRepository } from "./card.js";
export { deckRepository } from "./deck.js";
+export { noteRepository } from "./note.js";
export {
noteFieldTypeRepository,
noteTypeRepository,
diff --git a/src/server/repositories/note.test.ts b/src/server/repositories/note.test.ts
new file mode 100644
index 0000000..fc8b553
--- /dev/null
+++ b/src/server/repositories/note.test.ts
@@ -0,0 +1,417 @@
+import { describe, expect, it, vi } from "vitest";
+import type {
+ Card,
+ CreateNoteResult,
+ Note,
+ NoteFieldValue,
+ NoteRepository,
+ NoteWithFieldValues,
+} from "./types.js";
+
+function createMockNote(overrides: Partial<Note> = {}): Note {
+ return {
+ id: "note-uuid-123",
+ deckId: "deck-uuid-123",
+ noteTypeId: "note-type-uuid-123",
+ createdAt: new Date("2024-01-01"),
+ updatedAt: new Date("2024-01-01"),
+ deletedAt: null,
+ syncVersion: 0,
+ ...overrides,
+ };
+}
+
+function createMockNoteFieldValue(
+ overrides: Partial<NoteFieldValue> = {},
+): NoteFieldValue {
+ return {
+ id: "field-value-uuid-123",
+ noteId: "note-uuid-123",
+ noteFieldTypeId: "field-type-uuid-123",
+ value: "Test value",
+ createdAt: new Date("2024-01-01"),
+ updatedAt: new Date("2024-01-01"),
+ syncVersion: 0,
+ ...overrides,
+ };
+}
+
+function createMockCard(overrides: Partial<Card> = {}): Card {
+ return {
+ id: "card-uuid-123",
+ deckId: "deck-uuid-123",
+ front: "Front text",
+ back: "Back text",
+ state: 0,
+ due: new Date("2024-01-01"),
+ stability: 0,
+ difficulty: 0,
+ elapsedDays: 0,
+ scheduledDays: 0,
+ reps: 0,
+ lapses: 0,
+ lastReview: null,
+ createdAt: new Date("2024-01-01"),
+ updatedAt: new Date("2024-01-01"),
+ deletedAt: null,
+ syncVersion: 0,
+ ...overrides,
+ };
+}
+
+function createMockNoteWithFieldValues(
+ overrides: Partial<NoteWithFieldValues> = {},
+): NoteWithFieldValues {
+ const note = createMockNote(overrides);
+ return {
+ ...note,
+ fieldValues: overrides.fieldValues ?? [
+ createMockNoteFieldValue({
+ noteFieldTypeId: "field-front",
+ value: "Question",
+ }),
+ createMockNoteFieldValue({
+ id: "field-value-uuid-456",
+ noteFieldTypeId: "field-back",
+ value: "Answer",
+ }),
+ ],
+ };
+}
+
+function createMockCreateNoteResult(
+ overrides: Partial<CreateNoteResult> = {},
+): CreateNoteResult {
+ return {
+ note: createMockNote(overrides.note),
+ fieldValues: overrides.fieldValues ?? [
+ createMockNoteFieldValue({
+ noteFieldTypeId: "field-front",
+ value: "Question",
+ }),
+ createMockNoteFieldValue({
+ id: "field-value-uuid-456",
+ noteFieldTypeId: "field-back",
+ value: "Answer",
+ }),
+ ],
+ cards: overrides.cards ?? [createMockCard()],
+ };
+}
+
+function createMockNoteRepo(): NoteRepository {
+ return {
+ findByDeckId: vi.fn(),
+ findById: vi.fn(),
+ findByIdWithFieldValues: vi.fn(),
+ create: vi.fn(),
+ update: vi.fn(),
+ softDelete: vi.fn(),
+ };
+}
+
+describe("NoteRepository mock factory", () => {
+ describe("createMockNote", () => {
+ it("creates a valid Note with defaults", () => {
+ const note = createMockNote();
+
+ expect(note.id).toBe("note-uuid-123");
+ expect(note.deckId).toBe("deck-uuid-123");
+ expect(note.noteTypeId).toBe("note-type-uuid-123");
+ expect(note.deletedAt).toBeNull();
+ expect(note.syncVersion).toBe(0);
+ });
+
+ it("allows overriding properties", () => {
+ const note = createMockNote({
+ id: "custom-id",
+ noteTypeId: "custom-note-type-id",
+ });
+
+ expect(note.id).toBe("custom-id");
+ expect(note.noteTypeId).toBe("custom-note-type-id");
+ expect(note.deckId).toBe("deck-uuid-123");
+ });
+ });
+
+ describe("createMockNoteFieldValue", () => {
+ it("creates a valid NoteFieldValue with defaults", () => {
+ const fieldValue = createMockNoteFieldValue();
+
+ expect(fieldValue.id).toBe("field-value-uuid-123");
+ expect(fieldValue.noteId).toBe("note-uuid-123");
+ expect(fieldValue.noteFieldTypeId).toBe("field-type-uuid-123");
+ expect(fieldValue.value).toBe("Test value");
+ expect(fieldValue.syncVersion).toBe(0);
+ });
+
+ it("allows overriding properties", () => {
+ const fieldValue = createMockNoteFieldValue({
+ value: "Custom value",
+ noteFieldTypeId: "custom-field-type",
+ });
+
+ expect(fieldValue.value).toBe("Custom value");
+ expect(fieldValue.noteFieldTypeId).toBe("custom-field-type");
+ });
+ });
+
+ describe("createMockNoteWithFieldValues", () => {
+ it("creates Note with default field values", () => {
+ const noteWithFields = createMockNoteWithFieldValues();
+
+ expect(noteWithFields.fieldValues).toHaveLength(2);
+ expect(noteWithFields.fieldValues[0]?.value).toBe("Question");
+ expect(noteWithFields.fieldValues[1]?.value).toBe("Answer");
+ });
+
+ it("allows overriding field values", () => {
+ const customFieldValues = [
+ createMockNoteFieldValue({ noteFieldTypeId: "word", value: "日本語" }),
+ createMockNoteFieldValue({
+ noteFieldTypeId: "reading",
+ value: "にほんご",
+ }),
+ createMockNoteFieldValue({
+ noteFieldTypeId: "meaning",
+ value: "Japanese",
+ }),
+ ];
+ const noteWithFields = createMockNoteWithFieldValues({
+ fieldValues: customFieldValues,
+ });
+
+ expect(noteWithFields.fieldValues).toHaveLength(3);
+ expect(noteWithFields.fieldValues[0]?.value).toBe("日本語");
+ expect(noteWithFields.fieldValues[2]?.value).toBe("Japanese");
+ });
+ });
+
+ describe("createMockCreateNoteResult", () => {
+ it("creates a valid CreateNoteResult with defaults", () => {
+ const result = createMockCreateNoteResult();
+
+ expect(result.note.id).toBe("note-uuid-123");
+ expect(result.fieldValues).toHaveLength(2);
+ expect(result.cards).toHaveLength(1);
+ });
+
+ it("creates result with multiple cards for reversible note type", () => {
+ const result = createMockCreateNoteResult({
+ cards: [
+ createMockCard({ id: "card-1", front: "Q", back: "A" }),
+ createMockCard({ id: "card-2", front: "A", back: "Q" }),
+ ],
+ });
+
+ expect(result.cards).toHaveLength(2);
+ expect(result.cards[0]?.front).toBe("Q");
+ expect(result.cards[1]?.front).toBe("A");
+ });
+ });
+
+ describe("createMockNoteRepo", () => {
+ it("creates a repository with all required methods", () => {
+ const repo = createMockNoteRepo();
+
+ expect(repo.findByDeckId).toBeDefined();
+ expect(repo.findById).toBeDefined();
+ expect(repo.findByIdWithFieldValues).toBeDefined();
+ expect(repo.create).toBeDefined();
+ expect(repo.update).toBeDefined();
+ expect(repo.softDelete).toBeDefined();
+ });
+
+ it("methods are mockable for findByDeckId", async () => {
+ const repo = createMockNoteRepo();
+ const mockNotes = [createMockNote(), createMockNote({ id: "note-2" })];
+
+ vi.mocked(repo.findByDeckId).mockResolvedValue(mockNotes);
+
+ const results = await repo.findByDeckId("deck-123");
+ expect(results).toHaveLength(2);
+ expect(repo.findByDeckId).toHaveBeenCalledWith("deck-123");
+ });
+
+ it("methods are mockable for findById", async () => {
+ const repo = createMockNoteRepo();
+ const mockNote = createMockNote();
+
+ vi.mocked(repo.findById).mockResolvedValue(mockNote);
+
+ const found = await repo.findById("note-id", "deck-id");
+ expect(found).toEqual(mockNote);
+ expect(repo.findById).toHaveBeenCalledWith("note-id", "deck-id");
+ });
+
+ it("methods are mockable for findByIdWithFieldValues", async () => {
+ const repo = createMockNoteRepo();
+ const mockNoteWithFields = createMockNoteWithFieldValues();
+
+ vi.mocked(repo.findByIdWithFieldValues).mockResolvedValue(
+ mockNoteWithFields,
+ );
+
+ const found = await repo.findByIdWithFieldValues("note-id", "deck-id");
+ expect(found?.fieldValues).toHaveLength(2);
+ expect(repo.findByIdWithFieldValues).toHaveBeenCalledWith(
+ "note-id",
+ "deck-id",
+ );
+ });
+
+ it("methods are mockable for create", async () => {
+ const repo = createMockNoteRepo();
+ const mockResult = createMockCreateNoteResult();
+
+ vi.mocked(repo.create).mockResolvedValue(mockResult);
+
+ const result = await repo.create("deck-123", {
+ noteTypeId: "note-type-123",
+ fields: { "field-front": "Question", "field-back": "Answer" },
+ });
+ expect(result.note.id).toBe("note-uuid-123");
+ expect(result.cards).toHaveLength(1);
+ expect(repo.create).toHaveBeenCalledWith("deck-123", {
+ noteTypeId: "note-type-123",
+ fields: { "field-front": "Question", "field-back": "Answer" },
+ });
+ });
+
+ it("methods are mockable for update", async () => {
+ const repo = createMockNoteRepo();
+ const mockUpdated = createMockNoteWithFieldValues({
+ fieldValues: [
+ createMockNoteFieldValue({ value: "Updated Question" }),
+ createMockNoteFieldValue({ value: "Updated Answer" }),
+ ],
+ });
+
+ vi.mocked(repo.update).mockResolvedValue(mockUpdated);
+
+ const updated = await repo.update("note-id", "deck-id", {
+ "field-front": "Updated Question",
+ "field-back": "Updated Answer",
+ });
+ expect(updated?.fieldValues[0]?.value).toBe("Updated Question");
+ });
+
+ it("methods are mockable for softDelete", async () => {
+ const repo = createMockNoteRepo();
+
+ vi.mocked(repo.softDelete).mockResolvedValue(true);
+
+ const deleted = await repo.softDelete("note-id", "deck-id");
+ expect(deleted).toBe(true);
+ expect(repo.softDelete).toHaveBeenCalledWith("note-id", "deck-id");
+ });
+
+ it("returns undefined when note not found", async () => {
+ const repo = createMockNoteRepo();
+
+ vi.mocked(repo.findById).mockResolvedValue(undefined);
+ vi.mocked(repo.findByIdWithFieldValues).mockResolvedValue(undefined);
+ vi.mocked(repo.update).mockResolvedValue(undefined);
+
+ expect(await repo.findById("nonexistent", "deck-id")).toBeUndefined();
+ expect(
+ await repo.findByIdWithFieldValues("nonexistent", "deck-id"),
+ ).toBeUndefined();
+ expect(await repo.update("nonexistent", "deck-id", {})).toBeUndefined();
+ });
+
+ it("returns false when soft delete fails", async () => {
+ const repo = createMockNoteRepo();
+
+ vi.mocked(repo.softDelete).mockResolvedValue(false);
+
+ const deleted = await repo.softDelete("nonexistent", "deck-id");
+ expect(deleted).toBe(false);
+ });
+ });
+});
+
+describe("Note interface contracts", () => {
+ it("Note has required sync fields", () => {
+ const note = createMockNote();
+
+ expect(note).toHaveProperty("syncVersion");
+ expect(note).toHaveProperty("createdAt");
+ expect(note).toHaveProperty("updatedAt");
+ expect(note).toHaveProperty("deletedAt");
+ });
+
+ it("NoteFieldValue has required sync fields", () => {
+ const fieldValue = createMockNoteFieldValue();
+
+ expect(fieldValue).toHaveProperty("syncVersion");
+ expect(fieldValue).toHaveProperty("createdAt");
+ expect(fieldValue).toHaveProperty("updatedAt");
+ });
+
+ it("NoteWithFieldValues extends Note with fieldValues array", () => {
+ const noteWithFields = createMockNoteWithFieldValues();
+
+ expect(noteWithFields).toHaveProperty("id");
+ expect(noteWithFields).toHaveProperty("deckId");
+ expect(noteWithFields).toHaveProperty("noteTypeId");
+ expect(noteWithFields).toHaveProperty("fieldValues");
+ expect(Array.isArray(noteWithFields.fieldValues)).toBe(true);
+ });
+
+ it("CreateNoteResult contains note, fieldValues, and cards", () => {
+ const result = createMockCreateNoteResult();
+
+ expect(result).toHaveProperty("note");
+ expect(result).toHaveProperty("fieldValues");
+ expect(result).toHaveProperty("cards");
+ expect(Array.isArray(result.fieldValues)).toBe(true);
+ expect(Array.isArray(result.cards)).toBe(true);
+ });
+});
+
+describe("Note deletion behavior", () => {
+ it("soft delete cascades to cards", async () => {
+ const repo = createMockNoteRepo();
+
+ vi.mocked(repo.softDelete).mockResolvedValue(true);
+
+ const deleted = await repo.softDelete("note-id", "deck-id");
+ expect(deleted).toBe(true);
+ });
+});
+
+describe("Card generation from Note", () => {
+ it("creates one card for non-reversible note type", () => {
+ const result = createMockCreateNoteResult({
+ cards: [createMockCard({ front: "Question", back: "Answer" })],
+ });
+
+ expect(result.cards).toHaveLength(1);
+ expect(result.cards[0]?.front).toBe("Question");
+ expect(result.cards[0]?.back).toBe("Answer");
+ });
+
+ it("creates two cards for reversible note type", () => {
+ const result = createMockCreateNoteResult({
+ cards: [
+ createMockCard({
+ id: "card-normal",
+ front: "Question",
+ back: "Answer",
+ }),
+ createMockCard({
+ id: "card-reversed",
+ front: "Answer",
+ back: "Question",
+ }),
+ ],
+ });
+
+ expect(result.cards).toHaveLength(2);
+ expect(result.cards[0]?.front).toBe("Question");
+ expect(result.cards[0]?.back).toBe("Answer");
+ expect(result.cards[1]?.front).toBe("Answer");
+ expect(result.cards[1]?.back).toBe("Question");
+ });
+});
diff --git a/src/server/repositories/note.ts b/src/server/repositories/note.ts
new file mode 100644
index 0000000..52cbf9b
--- /dev/null
+++ b/src/server/repositories/note.ts
@@ -0,0 +1,325 @@
+import { and, eq, isNull, sql } from "drizzle-orm";
+import { db } from "../db/index.js";
+import {
+ CardState,
+ cards,
+ noteFieldTypes,
+ noteFieldValues,
+ notes,
+ noteTypes,
+} from "../db/schema.js";
+import type {
+ Card,
+ CreateNoteResult,
+ Note,
+ NoteFieldValue,
+ NoteRepository,
+ NoteWithFieldValues,
+} from "./types.js";
+
+export const noteRepository: NoteRepository = {
+ async findByDeckId(deckId: string): Promise<Note[]> {
+ const result = await db
+ .select()
+ .from(notes)
+ .where(and(eq(notes.deckId, deckId), isNull(notes.deletedAt)));
+ return result;
+ },
+
+ async findById(id: string, deckId: string): Promise<Note | undefined> {
+ const result = await db
+ .select()
+ .from(notes)
+ .where(
+ and(
+ eq(notes.id, id),
+ eq(notes.deckId, deckId),
+ isNull(notes.deletedAt),
+ ),
+ );
+ return result[0];
+ },
+
+ async findByIdWithFieldValues(
+ id: string,
+ deckId: string,
+ ): Promise<NoteWithFieldValues | undefined> {
+ const note = await this.findById(id, deckId);
+ if (!note) {
+ return undefined;
+ }
+
+ const fieldValuesResult = await db
+ .select()
+ .from(noteFieldValues)
+ .where(eq(noteFieldValues.noteId, id));
+
+ return {
+ ...note,
+ fieldValues: fieldValuesResult,
+ };
+ },
+
+ async create(
+ deckId: string,
+ data: {
+ noteTypeId: string;
+ fields: Record<string, string>;
+ },
+ ): Promise<CreateNoteResult> {
+ const noteType = await db
+ .select()
+ .from(noteTypes)
+ .where(
+ and(eq(noteTypes.id, data.noteTypeId), isNull(noteTypes.deletedAt)),
+ );
+
+ if (!noteType[0]) {
+ throw new Error("Note type not found");
+ }
+
+ const fieldTypes = await db
+ .select()
+ .from(noteFieldTypes)
+ .where(
+ and(
+ eq(noteFieldTypes.noteTypeId, data.noteTypeId),
+ isNull(noteFieldTypes.deletedAt),
+ ),
+ )
+ .orderBy(noteFieldTypes.order);
+
+ const [note] = await db
+ .insert(notes)
+ .values({
+ deckId,
+ noteTypeId: data.noteTypeId,
+ })
+ .returning();
+
+ if (!note) {
+ throw new Error("Failed to create note");
+ }
+
+ const fieldValuesResult: NoteFieldValue[] = [];
+ for (const fieldType of fieldTypes) {
+ const value = data.fields[fieldType.id] ?? "";
+ const [fieldValue] = await db
+ .insert(noteFieldValues)
+ .values({
+ noteId: note.id,
+ noteFieldTypeId: fieldType.id,
+ value,
+ })
+ .returning();
+ if (fieldValue) {
+ fieldValuesResult.push(fieldValue);
+ }
+ }
+
+ const createdCards: Card[] = [];
+
+ const normalCard = await createCardForNote(
+ deckId,
+ note.id,
+ noteType[0],
+ fieldValuesResult,
+ fieldTypes,
+ false,
+ );
+ createdCards.push(normalCard);
+
+ if (noteType[0].isReversible) {
+ const reversedCard = await createCardForNote(
+ deckId,
+ note.id,
+ noteType[0],
+ fieldValuesResult,
+ fieldTypes,
+ true,
+ );
+ createdCards.push(reversedCard);
+ }
+
+ return {
+ note,
+ fieldValues: fieldValuesResult,
+ cards: createdCards,
+ };
+ },
+
+ async update(
+ id: string,
+ deckId: string,
+ fields: Record<string, string>,
+ ): Promise<NoteWithFieldValues | undefined> {
+ const note = await this.findById(id, deckId);
+ if (!note) {
+ return undefined;
+ }
+
+ const [updatedNote] = await db
+ .update(notes)
+ .set({
+ updatedAt: new Date(),
+ syncVersion: sql`${notes.syncVersion} + 1`,
+ })
+ .where(and(eq(notes.id, id), eq(notes.deckId, deckId)))
+ .returning();
+
+ if (!updatedNote) {
+ return undefined;
+ }
+
+ const updatedFieldValues: NoteFieldValue[] = [];
+ for (const [fieldTypeId, value] of Object.entries(fields)) {
+ const existingFieldValue = await db
+ .select()
+ .from(noteFieldValues)
+ .where(
+ and(
+ eq(noteFieldValues.noteId, id),
+ eq(noteFieldValues.noteFieldTypeId, fieldTypeId),
+ ),
+ );
+
+ if (existingFieldValue[0]) {
+ const [updated] = await db
+ .update(noteFieldValues)
+ .set({
+ value,
+ updatedAt: new Date(),
+ syncVersion: sql`${noteFieldValues.syncVersion} + 1`,
+ })
+ .where(
+ and(
+ eq(noteFieldValues.noteId, id),
+ eq(noteFieldValues.noteFieldTypeId, fieldTypeId),
+ ),
+ )
+ .returning();
+ if (updated) {
+ updatedFieldValues.push(updated);
+ }
+ } else {
+ const [created] = await db
+ .insert(noteFieldValues)
+ .values({
+ noteId: id,
+ noteFieldTypeId: fieldTypeId,
+ value,
+ })
+ .returning();
+ if (created) {
+ updatedFieldValues.push(created);
+ }
+ }
+ }
+
+ const allFieldValues = await db
+ .select()
+ .from(noteFieldValues)
+ .where(eq(noteFieldValues.noteId, id));
+
+ return {
+ ...updatedNote,
+ fieldValues: allFieldValues,
+ };
+ },
+
+ async softDelete(id: string, deckId: string): Promise<boolean> {
+ const note = await this.findById(id, deckId);
+ if (!note) {
+ return false;
+ }
+
+ const now = new Date();
+
+ await db
+ .update(cards)
+ .set({
+ deletedAt: now,
+ updatedAt: now,
+ syncVersion: sql`${cards.syncVersion} + 1`,
+ })
+ .where(and(eq(cards.noteId, id), isNull(cards.deletedAt)));
+
+ const result = await db
+ .update(notes)
+ .set({
+ deletedAt: now,
+ updatedAt: now,
+ syncVersion: sql`${notes.syncVersion} + 1`,
+ })
+ .where(
+ and(
+ eq(notes.id, id),
+ eq(notes.deckId, deckId),
+ isNull(notes.deletedAt),
+ ),
+ )
+ .returning({ id: notes.id });
+
+ return result.length > 0;
+ },
+};
+
+async function createCardForNote(
+ deckId: string,
+ noteId: string,
+ noteType: { frontTemplate: string; backTemplate: string },
+ fieldValues: NoteFieldValue[],
+ fieldTypes: { id: string; name: string }[],
+ isReversed: boolean,
+): Promise<Card> {
+ const fieldMap = new Map<string, string>();
+ for (const fv of fieldValues) {
+ const fieldType = fieldTypes.find((ft) => ft.id === fv.noteFieldTypeId);
+ if (fieldType) {
+ fieldMap.set(fieldType.name, fv.value);
+ }
+ }
+
+ const frontTemplate = isReversed
+ ? noteType.backTemplate
+ : noteType.frontTemplate;
+ const backTemplate = isReversed
+ ? noteType.frontTemplate
+ : noteType.backTemplate;
+
+ const front = renderTemplate(frontTemplate, fieldMap);
+ const back = renderTemplate(backTemplate, fieldMap);
+
+ const [card] = await db
+ .insert(cards)
+ .values({
+ deckId,
+ noteId,
+ isReversed,
+ front,
+ back,
+ state: CardState.New,
+ due: new Date(),
+ stability: 0,
+ difficulty: 0,
+ elapsedDays: 0,
+ scheduledDays: 0,
+ reps: 0,
+ lapses: 0,
+ })
+ .returning();
+
+ if (!card) {
+ throw new Error("Failed to create card");
+ }
+
+ return card;
+}
+
+function renderTemplate(template: string, fields: Map<string, string>): string {
+ let result = template;
+ for (const [name, value] of fields) {
+ result = result.replace(new RegExp(`\\{\\{${name}\\}\\}`, "g"), value);
+ }
+ return result;
+}
diff --git a/src/server/repositories/types.ts b/src/server/repositories/types.ts
index a65a9cf..8c4c6a9 100644
--- a/src/server/repositories/types.ts
+++ b/src/server/repositories/types.ts
@@ -243,3 +243,55 @@ export interface NoteFieldTypeRepository {
reorder(noteTypeId: string, fieldIds: string[]): Promise<NoteFieldType[]>;
hasNoteFieldValues(id: string): Promise<boolean>;
}
+
+export interface Note {
+ id: string;
+ deckId: string;
+ noteTypeId: string;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: Date | null;
+ syncVersion: number;
+}
+
+export interface NoteFieldValue {
+ id: string;
+ noteId: string;
+ noteFieldTypeId: string;
+ value: string;
+ createdAt: Date;
+ updatedAt: Date;
+ syncVersion: number;
+}
+
+export interface NoteWithFieldValues extends Note {
+ fieldValues: NoteFieldValue[];
+}
+
+export interface CreateNoteResult {
+ note: Note;
+ fieldValues: NoteFieldValue[];
+ cards: Card[];
+}
+
+export interface NoteRepository {
+ findByDeckId(deckId: string): Promise<Note[]>;
+ findById(id: string, deckId: string): Promise<Note | undefined>;
+ findByIdWithFieldValues(
+ id: string,
+ deckId: string,
+ ): Promise<NoteWithFieldValues | undefined>;
+ create(
+ deckId: string,
+ data: {
+ noteTypeId: string;
+ fields: Record<string, string>;
+ },
+ ): Promise<CreateNoteResult>;
+ update(
+ id: string,
+ deckId: string,
+ fields: Record<string, string>,
+ ): Promise<NoteWithFieldValues | undefined>;
+ softDelete(id: string, deckId: string): Promise<boolean>;
+}