aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/server')
-rw-r--r--src/server/repositories/noteType.test.ts1
-rw-r--r--src/server/repositories/noteType.ts18
-rw-r--r--src/server/repositories/types.ts1
-rw-r--r--src/server/routes/noteTypes.test.ts37
-rw-r--r--src/server/routes/noteTypes.ts16
5 files changed, 67 insertions, 6 deletions
diff --git a/src/server/repositories/noteType.test.ts b/src/server/repositories/noteType.test.ts
index fdb9d5c..3fd9e7d 100644
--- a/src/server/repositories/noteType.test.ts
+++ b/src/server/repositories/noteType.test.ts
@@ -66,6 +66,7 @@ function createMockNoteTypeRepo(): NoteTypeRepository {
update: vi.fn(),
softDelete: vi.fn(),
hasNotes: vi.fn(),
+ countCards: vi.fn(),
};
}
diff --git a/src/server/repositories/noteType.ts b/src/server/repositories/noteType.ts
index 06c8834..89cf6fd 100644
--- a/src/server/repositories/noteType.ts
+++ b/src/server/repositories/noteType.ts
@@ -143,6 +143,24 @@ export const noteTypeRepository: NoteTypeRepository = {
return result.length > 0;
},
+
+ async countCards(noteTypeId: string): Promise<number> {
+ const { cards } = await import("../db/schema.js");
+
+ const result = await db
+ .select({ count: sql<number>`cast(count(*) as int)` })
+ .from(cards)
+ .innerJoin(notes, eq(cards.noteId, notes.id))
+ .where(
+ and(
+ eq(notes.noteTypeId, noteTypeId),
+ isNull(notes.deletedAt),
+ isNull(cards.deletedAt),
+ ),
+ );
+
+ return Number(result[0]?.count ?? 0);
+ },
};
export const noteFieldTypeRepository: NoteFieldTypeRepository = {
diff --git a/src/server/repositories/types.ts b/src/server/repositories/types.ts
index 7e0819a..27a12b4 100644
--- a/src/server/repositories/types.ts
+++ b/src/server/repositories/types.ts
@@ -250,6 +250,7 @@ export interface NoteTypeRepository {
): Promise<NoteType | undefined>;
softDelete(id: string, userId: string): Promise<boolean>;
hasNotes(id: string, userId: string): Promise<boolean>;
+ countCards(noteTypeId: string): Promise<number>;
}
export interface NoteFieldTypeRepository {
diff --git a/src/server/routes/noteTypes.test.ts b/src/server/routes/noteTypes.test.ts
index ccc29af..edfce59 100644
--- a/src/server/routes/noteTypes.test.ts
+++ b/src/server/routes/noteTypes.test.ts
@@ -20,6 +20,7 @@ function createMockNoteTypeRepo(): NoteTypeRepository {
update: vi.fn(),
softDelete: vi.fn(),
hasNotes: vi.fn(),
+ countCards: vi.fn(),
};
}
@@ -835,13 +836,14 @@ describe("DELETE /api/note-types/:id/fields/:fieldId", () => {
);
});
- it("returns 409 when field has values", async () => {
+ it("returns 409 with card count when field has values", async () => {
const noteTypeId = "a0000000-0000-4000-8000-000000000001";
const fieldId = "b0000000-0000-4000-8000-000000000002";
const mockNoteType = createMockNoteType({ id: noteTypeId });
vi.mocked(mockNoteTypeRepo.findById).mockResolvedValue(mockNoteType);
vi.mocked(mockNoteFieldTypeRepo.hasNoteFieldValues).mockResolvedValue(true);
+ vi.mocked(mockNoteTypeRepo.countCards).mockResolvedValue(5);
const res = await app.request(
`/api/note-types/${noteTypeId}/fields/${fieldId}`,
@@ -852,8 +854,39 @@ describe("DELETE /api/note-types/:id/fields/:fieldId", () => {
);
expect(res.status).toBe(409);
- const body = (await res.json()) as NoteTypeResponse;
+ const body = (await res.json()) as NoteTypeResponse & {
+ cardCount?: number;
+ };
expect(body.error?.code).toBe("FIELD_HAS_VALUES");
+ expect(body.cardCount).toBe(5);
+ expect(mockNoteTypeRepo.countCards).toHaveBeenCalledWith(noteTypeId);
+ });
+
+ it("deletes field with values when force=true", async () => {
+ const noteTypeId = "a0000000-0000-4000-8000-000000000001";
+ const fieldId = "b0000000-0000-4000-8000-000000000002";
+ const mockNoteType = createMockNoteType({ id: noteTypeId });
+
+ vi.mocked(mockNoteTypeRepo.findById).mockResolvedValue(mockNoteType);
+ vi.mocked(mockNoteFieldTypeRepo.hasNoteFieldValues).mockResolvedValue(true);
+ vi.mocked(mockNoteFieldTypeRepo.softDelete).mockResolvedValue(true);
+
+ const res = await app.request(
+ `/api/note-types/${noteTypeId}/fields/${fieldId}?force=true`,
+ {
+ method: "DELETE",
+ headers: { Authorization: `Bearer ${authToken}` },
+ },
+ );
+
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as NoteTypeResponse;
+ expect(body.success).toBe(true);
+ expect(mockNoteFieldTypeRepo.softDelete).toHaveBeenCalledWith(
+ fieldId,
+ noteTypeId,
+ );
+ expect(mockNoteTypeRepo.countCards).not.toHaveBeenCalled();
});
it("returns 404 when note type not found", async () => {
diff --git a/src/server/routes/noteTypes.ts b/src/server/routes/noteTypes.ts
index 7ab5aa0..ce051d4 100644
--- a/src/server/routes/noteTypes.ts
+++ b/src/server/routes/noteTypes.ts
@@ -188,6 +188,7 @@ export function createNoteTypesRouter(deps: NoteTypeDependencies) {
async (c) => {
const user = getAuthUser(c);
const { id, fieldId } = c.req.valid("param");
+ const force = c.req.query("force") === "true";
// Verify note type exists and belongs to user
const noteType = await noteTypeRepo.findById(id, user.id);
@@ -197,10 +198,17 @@ export function createNoteTypesRouter(deps: NoteTypeDependencies) {
// Check if there are note field values referencing this field
const hasValues = await noteFieldTypeRepo.hasNoteFieldValues(fieldId);
- if (hasValues) {
- throw Errors.conflict(
- "Cannot delete field with existing values",
- "FIELD_HAS_VALUES",
+ if (hasValues && !force) {
+ const cardCount = await noteTypeRepo.countCards(id);
+ return c.json(
+ {
+ error: {
+ message: "Cannot delete field with existing values",
+ code: "FIELD_HAS_VALUES",
+ },
+ cardCount,
+ },
+ 409,
);
}