aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/db/repositories.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/db/repositories.test.ts')
-rw-r--r--src/client/db/repositories.test.ts697
1 files changed, 696 insertions, 1 deletions
diff --git a/src/client/db/repositories.test.ts b/src/client/db/repositories.test.ts
index 0121541..2fca210 100644
--- a/src/client/db/repositories.test.ts
+++ b/src/client/db/repositories.test.ts
@@ -7,6 +7,10 @@ import { CardState, db, Rating } from "./index";
import {
localCardRepository,
localDeckRepository,
+ localNoteFieldTypeRepository,
+ localNoteFieldValueRepository,
+ localNoteRepository,
+ localNoteTypeRepository,
localReviewLogRepository,
} from "./repositories";
@@ -133,7 +137,7 @@ describe("localDeckRepository", () => {
expect(updated?.name).toBe("Updated Name");
expect(updated?.description).toBe("New description");
expect(updated?._synced).toBe(false);
- expect(updated?.updatedAt.getTime()).toBeGreaterThan(
+ expect(updated?.updatedAt.getTime()).toBeGreaterThanOrEqual(
deck.updatedAt.getTime(),
);
});
@@ -579,3 +583,694 @@ describe("localReviewLogRepository", () => {
});
});
});
+
+describe("localNoteTypeRepository", () => {
+ beforeEach(async () => {
+ await db.noteTypes.clear();
+ await db.noteFieldTypes.clear();
+ await db.notes.clear();
+ await db.noteFieldValues.clear();
+ });
+
+ afterEach(async () => {
+ await db.noteTypes.clear();
+ await db.noteFieldTypes.clear();
+ await db.notes.clear();
+ await db.noteFieldValues.clear();
+ });
+
+ describe("create", () => {
+ it("should create a note type with generated id and timestamps", async () => {
+ const noteType = await localNoteTypeRepository.create({
+ userId: "user-1",
+ name: "Basic",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: false,
+ });
+
+ expect(noteType.id).toBeDefined();
+ expect(noteType.userId).toBe("user-1");
+ expect(noteType.name).toBe("Basic");
+ expect(noteType.frontTemplate).toBe("{{Front}}");
+ expect(noteType.backTemplate).toBe("{{Back}}");
+ expect(noteType.isReversible).toBe(false);
+ expect(noteType.createdAt).toBeInstanceOf(Date);
+ expect(noteType.updatedAt).toBeInstanceOf(Date);
+ expect(noteType.deletedAt).toBeNull();
+ expect(noteType.syncVersion).toBe(0);
+ expect(noteType._synced).toBe(false);
+ });
+
+ it("should persist the note type to the database", async () => {
+ const created = await localNoteTypeRepository.create({
+ userId: "user-1",
+ name: "Basic",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: false,
+ });
+
+ const found = await db.noteTypes.get(created.id);
+ expect(found).toEqual(created);
+ });
+ });
+
+ describe("findById", () => {
+ it("should return the note type if found", async () => {
+ const created = await localNoteTypeRepository.create({
+ userId: "user-1",
+ name: "Basic",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: false,
+ });
+
+ const found = await localNoteTypeRepository.findById(created.id);
+ expect(found).toEqual(created);
+ });
+
+ it("should return undefined if not found", async () => {
+ const found = await localNoteTypeRepository.findById("non-existent");
+ expect(found).toBeUndefined();
+ });
+ });
+
+ describe("findByUserId", () => {
+ it("should return all note types for a user", async () => {
+ await localNoteTypeRepository.create({
+ userId: "user-1",
+ name: "Basic",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: false,
+ });
+ await localNoteTypeRepository.create({
+ userId: "user-1",
+ name: "Basic (reversed)",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: true,
+ });
+ await localNoteTypeRepository.create({
+ userId: "user-2",
+ name: "Other User Type",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: false,
+ });
+
+ const noteTypes = await localNoteTypeRepository.findByUserId("user-1");
+ expect(noteTypes).toHaveLength(2);
+ expect(noteTypes.map((nt) => nt.name).sort()).toEqual([
+ "Basic",
+ "Basic (reversed)",
+ ]);
+ });
+
+ it("should exclude soft-deleted note types", async () => {
+ const noteType = await localNoteTypeRepository.create({
+ userId: "user-1",
+ name: "Deleted Type",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: false,
+ });
+ await localNoteTypeRepository.delete(noteType.id);
+
+ const noteTypes = await localNoteTypeRepository.findByUserId("user-1");
+ expect(noteTypes).toHaveLength(0);
+ });
+ });
+
+ describe("update", () => {
+ it("should update note type fields", async () => {
+ const noteType = await localNoteTypeRepository.create({
+ userId: "user-1",
+ name: "Original Name",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: false,
+ });
+
+ const updated = await localNoteTypeRepository.update(noteType.id, {
+ name: "Updated Name",
+ frontTemplate: "Q: {{Front}}",
+ isReversible: true,
+ });
+
+ expect(updated?.name).toBe("Updated Name");
+ expect(updated?.frontTemplate).toBe("Q: {{Front}}");
+ expect(updated?.isReversible).toBe(true);
+ expect(updated?._synced).toBe(false);
+ expect(updated?.updatedAt.getTime()).toBeGreaterThanOrEqual(
+ noteType.updatedAt.getTime(),
+ );
+ });
+
+ it("should return undefined for non-existent note type", async () => {
+ const updated = await localNoteTypeRepository.update("non-existent", {
+ name: "New Name",
+ });
+ expect(updated).toBeUndefined();
+ });
+ });
+
+ describe("delete", () => {
+ it("should soft delete a note type", async () => {
+ const noteType = await localNoteTypeRepository.create({
+ userId: "user-1",
+ name: "Test Type",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: false,
+ });
+
+ const result = await localNoteTypeRepository.delete(noteType.id);
+ expect(result).toBe(true);
+
+ const found = await localNoteTypeRepository.findById(noteType.id);
+ expect(found?.deletedAt).not.toBeNull();
+ expect(found?._synced).toBe(false);
+ });
+
+ it("should return false for non-existent note type", async () => {
+ const result = await localNoteTypeRepository.delete("non-existent");
+ expect(result).toBe(false);
+ });
+ });
+
+ describe("findUnsynced", () => {
+ it("should return unsynced note types", async () => {
+ const noteType1 = await localNoteTypeRepository.create({
+ userId: "user-1",
+ name: "Unsynced",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: false,
+ });
+ const noteType2 = await localNoteTypeRepository.create({
+ userId: "user-1",
+ name: "Synced",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: false,
+ });
+ await localNoteTypeRepository.markSynced(noteType2.id, 1);
+
+ const unsynced = await localNoteTypeRepository.findUnsynced();
+ expect(unsynced).toHaveLength(1);
+ expect(unsynced[0]?.id).toBe(noteType1.id);
+ });
+ });
+
+ describe("markSynced", () => {
+ it("should mark a note type as synced with version", async () => {
+ const noteType = await localNoteTypeRepository.create({
+ userId: "user-1",
+ name: "Test",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: false,
+ });
+
+ await localNoteTypeRepository.markSynced(noteType.id, 5);
+
+ const found = await localNoteTypeRepository.findById(noteType.id);
+ expect(found?._synced).toBe(true);
+ expect(found?.syncVersion).toBe(5);
+ });
+ });
+});
+
+describe("localNoteFieldTypeRepository", () => {
+ let noteTypeId: string;
+
+ beforeEach(async () => {
+ await db.noteTypes.clear();
+ await db.noteFieldTypes.clear();
+ await db.notes.clear();
+ await db.noteFieldValues.clear();
+
+ const noteType = await localNoteTypeRepository.create({
+ userId: "user-1",
+ name: "Basic",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: false,
+ });
+ noteTypeId = noteType.id;
+ });
+
+ afterEach(async () => {
+ await db.noteTypes.clear();
+ await db.noteFieldTypes.clear();
+ await db.notes.clear();
+ await db.noteFieldValues.clear();
+ });
+
+ describe("create", () => {
+ it("should create a field type with generated id and timestamps", async () => {
+ const fieldType = await localNoteFieldTypeRepository.create({
+ noteTypeId,
+ name: "Front",
+ order: 0,
+ });
+
+ expect(fieldType.id).toBeDefined();
+ expect(fieldType.noteTypeId).toBe(noteTypeId);
+ expect(fieldType.name).toBe("Front");
+ expect(fieldType.order).toBe(0);
+ expect(fieldType.fieldType).toBe("text");
+ expect(fieldType.createdAt).toBeInstanceOf(Date);
+ expect(fieldType.updatedAt).toBeInstanceOf(Date);
+ expect(fieldType.deletedAt).toBeNull();
+ expect(fieldType.syncVersion).toBe(0);
+ expect(fieldType._synced).toBe(false);
+ });
+ });
+
+ describe("findByNoteTypeId", () => {
+ it("should return all field types for a note type sorted by order", async () => {
+ await localNoteFieldTypeRepository.create({
+ noteTypeId,
+ name: "Back",
+ order: 1,
+ });
+ await localNoteFieldTypeRepository.create({
+ noteTypeId,
+ name: "Front",
+ order: 0,
+ });
+
+ const fieldTypes =
+ await localNoteFieldTypeRepository.findByNoteTypeId(noteTypeId);
+ expect(fieldTypes).toHaveLength(2);
+ expect(fieldTypes[0]?.name).toBe("Front");
+ expect(fieldTypes[1]?.name).toBe("Back");
+ });
+
+ it("should exclude soft-deleted field types", async () => {
+ const fieldType = await localNoteFieldTypeRepository.create({
+ noteTypeId,
+ name: "Deleted",
+ order: 0,
+ });
+ await localNoteFieldTypeRepository.delete(fieldType.id);
+
+ const fieldTypes =
+ await localNoteFieldTypeRepository.findByNoteTypeId(noteTypeId);
+ expect(fieldTypes).toHaveLength(0);
+ });
+ });
+
+ describe("update", () => {
+ it("should update field type fields", async () => {
+ const fieldType = await localNoteFieldTypeRepository.create({
+ noteTypeId,
+ name: "Original",
+ order: 0,
+ });
+
+ const updated = await localNoteFieldTypeRepository.update(fieldType.id, {
+ name: "Updated",
+ order: 1,
+ });
+
+ expect(updated?.name).toBe("Updated");
+ expect(updated?.order).toBe(1);
+ expect(updated?._synced).toBe(false);
+ });
+ });
+
+ describe("findUnsynced", () => {
+ it("should return unsynced field types", async () => {
+ const fieldType1 = await localNoteFieldTypeRepository.create({
+ noteTypeId,
+ name: "Unsynced",
+ order: 0,
+ });
+ const fieldType2 = await localNoteFieldTypeRepository.create({
+ noteTypeId,
+ name: "Synced",
+ order: 1,
+ });
+ await localNoteFieldTypeRepository.markSynced(fieldType2.id, 1);
+
+ const unsynced = await localNoteFieldTypeRepository.findUnsynced();
+ expect(unsynced).toHaveLength(1);
+ expect(unsynced[0]?.id).toBe(fieldType1.id);
+ });
+ });
+});
+
+describe("localNoteRepository", () => {
+ let deckId: string;
+ let noteTypeId: string;
+
+ beforeEach(async () => {
+ await db.decks.clear();
+ await db.cards.clear();
+ await db.noteTypes.clear();
+ await db.noteFieldTypes.clear();
+ await db.notes.clear();
+ await db.noteFieldValues.clear();
+
+ const deck = await localDeckRepository.create({
+ userId: "user-1",
+ name: "Test Deck",
+ description: null,
+ newCardsPerDay: 20,
+ });
+ deckId = deck.id;
+
+ const noteType = await localNoteTypeRepository.create({
+ userId: "user-1",
+ name: "Basic",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: false,
+ });
+ noteTypeId = noteType.id;
+ });
+
+ afterEach(async () => {
+ await db.decks.clear();
+ await db.cards.clear();
+ await db.noteTypes.clear();
+ await db.noteFieldTypes.clear();
+ await db.notes.clear();
+ await db.noteFieldValues.clear();
+ });
+
+ describe("create", () => {
+ it("should create a note with generated id and timestamps", async () => {
+ const note = await localNoteRepository.create({
+ deckId,
+ noteTypeId,
+ });
+
+ expect(note.id).toBeDefined();
+ expect(note.deckId).toBe(deckId);
+ expect(note.noteTypeId).toBe(noteTypeId);
+ expect(note.createdAt).toBeInstanceOf(Date);
+ expect(note.updatedAt).toBeInstanceOf(Date);
+ expect(note.deletedAt).toBeNull();
+ expect(note.syncVersion).toBe(0);
+ expect(note._synced).toBe(false);
+ });
+ });
+
+ describe("findByDeckId", () => {
+ it("should return all notes for a deck", async () => {
+ await localNoteRepository.create({ deckId, noteTypeId });
+ await localNoteRepository.create({ deckId, noteTypeId });
+
+ const notes = await localNoteRepository.findByDeckId(deckId);
+ expect(notes).toHaveLength(2);
+ });
+
+ it("should exclude soft-deleted notes", async () => {
+ const note = await localNoteRepository.create({ deckId, noteTypeId });
+ await localNoteRepository.delete(note.id);
+
+ const notes = await localNoteRepository.findByDeckId(deckId);
+ expect(notes).toHaveLength(0);
+ });
+ });
+
+ describe("findByNoteTypeId", () => {
+ it("should return all notes for a note type", async () => {
+ await localNoteRepository.create({ deckId, noteTypeId });
+
+ const notes = await localNoteRepository.findByNoteTypeId(noteTypeId);
+ expect(notes).toHaveLength(1);
+ });
+ });
+
+ describe("update", () => {
+ it("should update note metadata", async () => {
+ const note = await localNoteRepository.create({ deckId, noteTypeId });
+
+ const updated = await localNoteRepository.update(note.id);
+
+ expect(updated?._synced).toBe(false);
+ expect(updated?.updatedAt.getTime()).toBeGreaterThanOrEqual(
+ note.updatedAt.getTime(),
+ );
+ });
+
+ it("should return undefined for non-existent note", async () => {
+ const updated = await localNoteRepository.update("non-existent");
+ expect(updated).toBeUndefined();
+ });
+ });
+
+ describe("delete", () => {
+ it("should soft delete a note", async () => {
+ const note = await localNoteRepository.create({ deckId, noteTypeId });
+
+ const result = await localNoteRepository.delete(note.id);
+ expect(result).toBe(true);
+
+ const found = await localNoteRepository.findById(note.id);
+ expect(found?.deletedAt).not.toBeNull();
+ });
+
+ it("should cascade soft delete to related cards", async () => {
+ const note = await localNoteRepository.create({ deckId, noteTypeId });
+
+ // Create cards associated with this note
+ const card1 = await localCardRepository.create({
+ deckId,
+ front: "Q1",
+ back: "A1",
+ noteId: note.id,
+ isReversed: false,
+ });
+ const card2 = await localCardRepository.create({
+ deckId,
+ front: "Q2",
+ back: "A2",
+ noteId: note.id,
+ isReversed: true,
+ });
+
+ await localNoteRepository.delete(note.id);
+
+ const foundCard1 = await localCardRepository.findById(card1.id);
+ const foundCard2 = await localCardRepository.findById(card2.id);
+ expect(foundCard1?.deletedAt).not.toBeNull();
+ expect(foundCard2?.deletedAt).not.toBeNull();
+ });
+
+ it("should return false for non-existent note", async () => {
+ const result = await localNoteRepository.delete("non-existent");
+ expect(result).toBe(false);
+ });
+ });
+
+ describe("findUnsynced", () => {
+ it("should return unsynced notes", async () => {
+ const note1 = await localNoteRepository.create({ deckId, noteTypeId });
+ const note2 = await localNoteRepository.create({ deckId, noteTypeId });
+ await localNoteRepository.markSynced(note2.id, 1);
+
+ const unsynced = await localNoteRepository.findUnsynced();
+ expect(unsynced).toHaveLength(1);
+ expect(unsynced[0]?.id).toBe(note1.id);
+ });
+ });
+});
+
+describe("localNoteFieldValueRepository", () => {
+ let noteId: string;
+ let noteFieldTypeId: string;
+ let noteTypeId: string;
+
+ beforeEach(async () => {
+ await db.decks.clear();
+ await db.cards.clear();
+ await db.noteTypes.clear();
+ await db.noteFieldTypes.clear();
+ await db.notes.clear();
+ await db.noteFieldValues.clear();
+
+ const deck = await localDeckRepository.create({
+ userId: "user-1",
+ name: "Test Deck",
+ description: null,
+ newCardsPerDay: 20,
+ });
+
+ const noteType = await localNoteTypeRepository.create({
+ userId: "user-1",
+ name: "Basic",
+ frontTemplate: "{{Front}}",
+ backTemplate: "{{Back}}",
+ isReversible: false,
+ });
+ noteTypeId = noteType.id;
+
+ const fieldType = await localNoteFieldTypeRepository.create({
+ noteTypeId: noteType.id,
+ name: "Front",
+ order: 0,
+ });
+ noteFieldTypeId = fieldType.id;
+
+ const note = await localNoteRepository.create({
+ deckId: deck.id,
+ noteTypeId: noteType.id,
+ });
+ noteId = note.id;
+ });
+
+ afterEach(async () => {
+ await db.decks.clear();
+ await db.cards.clear();
+ await db.noteTypes.clear();
+ await db.noteFieldTypes.clear();
+ await db.notes.clear();
+ await db.noteFieldValues.clear();
+ });
+
+ describe("create", () => {
+ it("should create a field value with generated id and timestamps", async () => {
+ const fieldValue = await localNoteFieldValueRepository.create({
+ noteId,
+ noteFieldTypeId,
+ value: "What is the capital of Japan?",
+ });
+
+ expect(fieldValue.id).toBeDefined();
+ expect(fieldValue.noteId).toBe(noteId);
+ expect(fieldValue.noteFieldTypeId).toBe(noteFieldTypeId);
+ expect(fieldValue.value).toBe("What is the capital of Japan?");
+ expect(fieldValue.createdAt).toBeInstanceOf(Date);
+ expect(fieldValue.updatedAt).toBeInstanceOf(Date);
+ expect(fieldValue.syncVersion).toBe(0);
+ expect(fieldValue._synced).toBe(false);
+ });
+ });
+
+ describe("findByNoteId", () => {
+ it("should return all field values for a note", async () => {
+ await localNoteFieldValueRepository.create({
+ noteId,
+ noteFieldTypeId,
+ value: "Front value",
+ });
+
+ const backFieldType = await localNoteFieldTypeRepository.create({
+ noteTypeId,
+ name: "Back",
+ order: 1,
+ });
+ await localNoteFieldValueRepository.create({
+ noteId,
+ noteFieldTypeId: backFieldType.id,
+ value: "Back value",
+ });
+
+ const fieldValues =
+ await localNoteFieldValueRepository.findByNoteId(noteId);
+ expect(fieldValues).toHaveLength(2);
+ });
+ });
+
+ describe("findByNoteIdAndFieldTypeId", () => {
+ it("should return the field value for a specific note and field type", async () => {
+ await localNoteFieldValueRepository.create({
+ noteId,
+ noteFieldTypeId,
+ value: "Test value",
+ });
+
+ const found =
+ await localNoteFieldValueRepository.findByNoteIdAndFieldTypeId(
+ noteId,
+ noteFieldTypeId,
+ );
+ expect(found?.value).toBe("Test value");
+ });
+
+ it("should return undefined if not found", async () => {
+ const found =
+ await localNoteFieldValueRepository.findByNoteIdAndFieldTypeId(
+ noteId,
+ "non-existent",
+ );
+ expect(found).toBeUndefined();
+ });
+ });
+
+ describe("update", () => {
+ it("should update field value", async () => {
+ const fieldValue = await localNoteFieldValueRepository.create({
+ noteId,
+ noteFieldTypeId,
+ value: "Original",
+ });
+
+ const updated = await localNoteFieldValueRepository.update(
+ fieldValue.id,
+ {
+ value: "Updated",
+ },
+ );
+
+ expect(updated?.value).toBe("Updated");
+ expect(updated?._synced).toBe(false);
+ expect(updated?.updatedAt.getTime()).toBeGreaterThanOrEqual(
+ fieldValue.updatedAt.getTime(),
+ );
+ });
+
+ it("should return undefined for non-existent field value", async () => {
+ const updated = await localNoteFieldValueRepository.update(
+ "non-existent",
+ {
+ value: "Updated",
+ },
+ );
+ expect(updated).toBeUndefined();
+ });
+ });
+
+ describe("findUnsynced", () => {
+ it("should return unsynced field values", async () => {
+ const fieldValue1 = await localNoteFieldValueRepository.create({
+ noteId,
+ noteFieldTypeId,
+ value: "Unsynced",
+ });
+ const fieldValue2 = await localNoteFieldValueRepository.create({
+ noteId,
+ noteFieldTypeId: noteFieldTypeId,
+ value: "Synced",
+ });
+ await localNoteFieldValueRepository.markSynced(fieldValue2.id, 1);
+
+ const unsynced = await localNoteFieldValueRepository.findUnsynced();
+ expect(unsynced).toHaveLength(1);
+ expect(unsynced[0]?.id).toBe(fieldValue1.id);
+ });
+ });
+
+ describe("markSynced", () => {
+ it("should mark a field value as synced with version", async () => {
+ const fieldValue = await localNoteFieldValueRepository.create({
+ noteId,
+ noteFieldTypeId,
+ value: "Test",
+ });
+
+ await localNoteFieldValueRepository.markSynced(fieldValue.id, 5);
+
+ const found = await localNoteFieldValueRepository.findById(fieldValue.id);
+ expect(found?._synced).toBe(true);
+ expect(found?.syncVersion).toBe(5);
+ });
+ });
+});