Skip to content

Commit 2379a72

Browse files
authored
Merge pull request #35 from BjornMelin/refactor/dry-dal-workflows
refactor(db): centralize db_not_migrated wrapping
2 parents 35bbe73 + a91f48d commit 2379a72

22 files changed

Lines changed: 120 additions & 349 deletions

src/lib/data/approvals.server.test.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ const state = vi.hoisted(() => ({
88
findFirst: vi.fn(),
99
findMany: vi.fn(),
1010
insertReturning: vi.fn(),
11-
isUndefinedColumnError: vi.fn(),
12-
isUndefinedTableError: vi.fn(),
1311
revalidateTag: vi.fn(),
1412
updateReturning: vi.fn(),
1513
}));
@@ -43,16 +41,9 @@ vi.mock("@/db/client", () => ({
4341
}),
4442
}));
4543

46-
vi.mock("@/lib/db/postgres-errors", () => ({
47-
isUndefinedColumnError: (err: unknown) => state.isUndefinedColumnError(err),
48-
isUndefinedTableError: (err: unknown) => state.isUndefinedTableError(err),
49-
}));
50-
5144
beforeEach(() => {
5245
vi.clearAllMocks();
5346
vi.resetModules();
54-
state.isUndefinedColumnError.mockReturnValue(false);
55-
state.isUndefinedTableError.mockReturnValue(false);
5647
});
5748

5849
describe("approvals DAL", () => {
@@ -133,9 +124,8 @@ describe("approvals DAL", () => {
133124
});
134125

135126
it("wraps undefined-table/column errors into db_not_migrated", async () => {
136-
const err = new Error("missing");
127+
const err = Object.assign(new Error("missing"), { code: "42703" });
137128
state.findFirst.mockRejectedValueOnce(err);
138-
state.isUndefinedColumnError.mockReturnValueOnce(true);
139129

140130
const { getApprovalById } = await import("@/lib/data/approvals.server");
141131
await expect(getApprovalById("approval_1")).rejects.toMatchObject({

src/lib/data/approvals.server.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ import { getDb } from "@/db/client";
77
import * as schema from "@/db/schema";
88
import { tagApprovalsIndex } from "@/lib/cache/tags";
99
import { AppError } from "@/lib/core/errors";
10-
import {
11-
isUndefinedColumnError,
12-
isUndefinedTableError,
13-
} from "@/lib/db/postgres-errors";
10+
import { maybeWrapDbNotMigrated } from "@/lib/db/postgres-errors";
1411

1512
/**
1613
* JSON-safe approval DTO.
@@ -48,18 +45,6 @@ function toApprovalDto(row: ApprovalRow): ApprovalDto {
4845
};
4946
}
5047

51-
function maybeWrapDbNotMigrated(err: unknown): unknown {
52-
if (isUndefinedTableError(err) || isUndefinedColumnError(err)) {
53-
return new AppError(
54-
"db_not_migrated",
55-
500,
56-
"Database is not migrated. Run migrations and refresh the page.",
57-
err,
58-
);
59-
}
60-
return err;
61-
}
62-
6348
/**
6449
* Fetch a single approval row by ID.
6550
*

src/lib/data/chat.server.test.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ const state = vi.hoisted(() => ({
3131
vi.fn<
3232
(projectId: string, userId: string) => Promise<{ id: string } | null>
3333
>(),
34-
isUndefinedTableError: vi.fn<(err: unknown) => boolean>(),
3534
lastChatMessagesFindManyLimit: null as number | null,
3635
lastChatThreadsFindManyLimit: null as number | null,
3736
lastUpdateSetValues: null as Record<string, unknown> | null,
@@ -208,10 +207,6 @@ vi.mock("@/lib/data/projects.server", () => ({
208207
state.getProjectByIdForUser(projectId, userId),
209208
}));
210209

211-
vi.mock("@/lib/db/postgres-errors", () => ({
212-
isUndefinedTableError: (err: unknown) => state.isUndefinedTableError(err),
213-
}));
214-
215210
vi.mock("react", () => ({
216211
cache: <TArgs extends readonly unknown[], TResult>(
217212
fn: (...args: TArgs) => TResult,
@@ -226,7 +221,6 @@ async function loadChatDal() {
226221
beforeEach(() => {
227222
state.db = createFakeDb();
228223
state.getProjectByIdForUser.mockResolvedValue({ id: "proj_1" });
229-
state.isUndefinedTableError.mockReturnValue(false);
230224

231225
state.lastChatMessagesFindManyLimit = null;
232226
state.lastChatThreadsFindManyLimit = null;
@@ -308,9 +302,8 @@ describe("chat DAL", () => {
308302
it("wraps undefined-table insert errors as db_not_migrated", async () => {
309303
const { ensureChatThreadForWorkflowRun } = await loadChatDal();
310304

311-
const err = new Error("missing table");
305+
const err = Object.assign(new Error("missing table"), { code: "42P01" });
312306
state.threadInsertError = err;
313-
state.isUndefinedTableError.mockReturnValueOnce(true);
314307

315308
await expect(
316309
ensureChatThreadForWorkflowRun({
@@ -410,9 +403,8 @@ describe("chat DAL", () => {
410403
it("wraps undefined-table insert errors for chat messages as db_not_migrated", async () => {
411404
const { appendChatMessages } = await loadChatDal();
412405

413-
const err = new Error("missing table");
406+
const err = Object.assign(new Error("missing table"), { code: "42P01" });
414407
state.messageInsertError = err;
415-
state.isUndefinedTableError.mockReturnValueOnce(true);
416408

417409
await expect(
418410
appendChatMessages({

src/lib/data/chat.server.ts

Lines changed: 26 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as schema from "@/db/schema";
99
import type { ChatThreadStatus } from "@/lib/chat/thread-status";
1010
import { AppError } from "@/lib/core/errors";
1111
import { getProjectByIdForUser } from "@/lib/data/projects.server";
12-
import { isUndefinedTableError } from "@/lib/db/postgres-errors";
12+
import { maybeWrapDbNotMigrated } from "@/lib/db/postgres-errors";
1313

1414
/** Chat thread status values shared with chat data DTOs and update helpers. */
1515
export type { ChatThreadStatus } from "@/lib/chat/thread-status";
@@ -92,24 +92,27 @@ export async function ensureChatThreadForWorkflowRun(
9292
.onConflictDoNothing({ target: schema.chatThreadsTable.workflowRunId })
9393
.returning();
9494
} catch (error) {
95-
if (isUndefinedTableError(error)) {
96-
throw new AppError(
97-
"db_not_migrated",
98-
500,
99-
"Database is not migrated. Run migrations and try again.",
100-
error,
101-
);
102-
}
103-
throw error;
95+
throw maybeWrapDbNotMigrated(
96+
error,
97+
"Database is not migrated. Run migrations and try again.",
98+
);
10499
}
105100

106101
if (row) {
107102
return toChatThreadDto(row);
108103
}
109104

110-
const existing = await db.query.chatThreadsTable.findFirst({
111-
where: eq(schema.chatThreadsTable.workflowRunId, input.workflowRunId),
112-
});
105+
let existing: ChatThreadRow | undefined;
106+
try {
107+
existing = await db.query.chatThreadsTable.findFirst({
108+
where: eq(schema.chatThreadsTable.workflowRunId, input.workflowRunId),
109+
});
110+
} catch (error) {
111+
throw maybeWrapDbNotMigrated(
112+
error,
113+
"Database is not migrated. Run migrations and try again.",
114+
);
115+
}
113116

114117
if (!existing) {
115118
throw new AppError(
@@ -138,15 +141,7 @@ export const getChatThreadByWorkflowRunId = cache(
138141
});
139142
return row ? toChatThreadDto(row) : null;
140143
} catch (error) {
141-
if (isUndefinedTableError(error)) {
142-
throw new AppError(
143-
"db_not_migrated",
144-
500,
145-
"Database is not migrated. Run migrations and refresh the page.",
146-
error,
147-
);
148-
}
149-
throw error;
144+
throw maybeWrapDbNotMigrated(error);
150145
}
151146
},
152147
);
@@ -171,15 +166,7 @@ export const getLatestChatThreadByProjectId = cache(
171166
});
172167
return row ? toChatThreadDto(row) : null;
173168
} catch (error) {
174-
if (isUndefinedTableError(error)) {
175-
throw new AppError(
176-
"db_not_migrated",
177-
500,
178-
"Database is not migrated. Run migrations and refresh the page.",
179-
error,
180-
);
181-
}
182-
throw error;
169+
throw maybeWrapDbNotMigrated(error);
183170
}
184171
},
185172
);
@@ -212,15 +199,7 @@ export const listChatThreadsByProjectId = cache(
212199
});
213200
return rows.map(toChatThreadDto);
214201
} catch (error) {
215-
if (isUndefinedTableError(error)) {
216-
throw new AppError(
217-
"db_not_migrated",
218-
500,
219-
"Database is not migrated. Run migrations and refresh the page.",
220-
error,
221-
);
222-
}
223-
throw error;
202+
throw maybeWrapDbNotMigrated(error);
224203
}
225204
},
226205
);
@@ -245,15 +224,7 @@ export const getChatThreadById = cache(
245224
await assertProjectAccess(row.projectId, userId);
246225
return toChatThreadDto(row);
247226
} catch (error) {
248-
if (isUndefinedTableError(error)) {
249-
throw new AppError(
250-
"db_not_migrated",
251-
500,
252-
"Database is not migrated. Run migrations and refresh the page.",
253-
error,
254-
);
255-
}
256-
throw error;
227+
throw maybeWrapDbNotMigrated(error);
257228
}
258229
},
259230
);
@@ -345,15 +316,7 @@ export async function appendChatMessages(
345316
],
346317
});
347318
} catch (error) {
348-
if (isUndefinedTableError(error)) {
349-
throw new AppError(
350-
"db_not_migrated",
351-
500,
352-
"Database is not migrated. Run migrations and refresh the page.",
353-
error,
354-
);
355-
}
356-
throw error;
319+
throw maybeWrapDbNotMigrated(error);
357320
}
358321
}
359322

@@ -389,15 +352,7 @@ export const listChatMessagesByThreadId = cache(
389352
});
390353
return rows.map(toChatMessageDto);
391354
} catch (error) {
392-
if (isUndefinedTableError(error)) {
393-
throw new AppError(
394-
"db_not_migrated",
395-
500,
396-
"Database is not migrated. Run migrations and refresh the page.",
397-
error,
398-
);
399-
}
400-
throw error;
355+
throw maybeWrapDbNotMigrated(error);
401356
}
402357
},
403358
);
@@ -435,14 +390,9 @@ export async function updateChatThreadByWorkflowRunId(
435390
.set(next)
436391
.where(eq(schema.chatThreadsTable.workflowRunId, workflowRunId));
437392
} catch (error) {
438-
if (isUndefinedTableError(error)) {
439-
throw new AppError(
440-
"db_not_migrated",
441-
500,
442-
"Database is not migrated. Run migrations and try again.",
443-
error,
444-
);
445-
}
446-
throw error;
393+
throw maybeWrapDbNotMigrated(
394+
error,
395+
"Database is not migrated. Run migrations and try again.",
396+
);
447397
}
448398
}

src/lib/data/deployments.server.test.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ const state = vi.hoisted(() => ({
88
findFirst: vi.fn(),
99
findMany: vi.fn(),
1010
insertReturning: vi.fn(),
11-
isUndefinedColumnError: vi.fn(),
12-
isUndefinedTableError: vi.fn(),
1311
revalidateTag: vi.fn(),
1412
updateReturning: vi.fn(),
1513
}));
@@ -43,16 +41,9 @@ vi.mock("@/db/client", () => ({
4341
}),
4442
}));
4543

46-
vi.mock("@/lib/db/postgres-errors", () => ({
47-
isUndefinedColumnError: (err: unknown) => state.isUndefinedColumnError(err),
48-
isUndefinedTableError: (err: unknown) => state.isUndefinedTableError(err),
49-
}));
50-
5144
beforeEach(() => {
5245
vi.clearAllMocks();
5346
vi.resetModules();
54-
state.isUndefinedColumnError.mockReturnValue(false);
55-
state.isUndefinedTableError.mockReturnValue(false);
5647
});
5748

5849
describe("deployments DAL", () => {
@@ -150,9 +141,8 @@ describe("deployments DAL", () => {
150141
});
151142

152143
it("wraps undefined-table/column errors into db_not_migrated", async () => {
153-
const err = new Error("missing");
144+
const err = Object.assign(new Error("missing"), { code: "42P01" });
154145
state.findFirst.mockRejectedValueOnce(err);
155-
state.isUndefinedTableError.mockReturnValueOnce(true);
156146

157147
const { getDeploymentById } = await import("@/lib/data/deployments.server");
158148
await expect(getDeploymentById("dep_1")).rejects.toMatchObject({

src/lib/data/deployments.server.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ import { getDb } from "@/db/client";
77
import * as schema from "@/db/schema";
88
import { tagDeploymentsIndex } from "@/lib/cache/tags";
99
import { AppError } from "@/lib/core/errors";
10-
import {
11-
isUndefinedColumnError,
12-
isUndefinedTableError,
13-
} from "@/lib/db/postgres-errors";
10+
import { maybeWrapDbNotMigrated } from "@/lib/db/postgres-errors";
1411

1512
/**
1613
* JSON-safe deployment DTO.
@@ -51,18 +48,6 @@ function toDeploymentDto(row: DeploymentRow): DeploymentDto {
5148
};
5249
}
5350

54-
function maybeWrapDbNotMigrated(err: unknown): unknown {
55-
if (isUndefinedTableError(err) || isUndefinedColumnError(err)) {
56-
return new AppError(
57-
"db_not_migrated",
58-
500,
59-
"Database is not migrated. Run migrations and refresh the page.",
60-
err,
61-
);
62-
}
63-
return err;
64-
}
65-
6651
/**
6752
* List deployment records for a project (newest-first).
6853
*

src/lib/data/infra-resources.server.test.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ const state = vi.hoisted(() => ({
88
findFirst: vi.fn(),
99
findMany: vi.fn(),
1010
insertReturning: vi.fn(),
11-
isUndefinedColumnError: vi.fn(),
12-
isUndefinedTableError: vi.fn(),
1311
revalidateTag: vi.fn(),
1412
updateReturning: vi.fn(),
1513
}));
@@ -43,16 +41,9 @@ vi.mock("@/db/client", () => ({
4341
}),
4442
}));
4543

46-
vi.mock("@/lib/db/postgres-errors", () => ({
47-
isUndefinedColumnError: (err: unknown) => state.isUndefinedColumnError(err),
48-
isUndefinedTableError: (err: unknown) => state.isUndefinedTableError(err),
49-
}));
50-
5144
beforeEach(() => {
5245
vi.clearAllMocks();
5346
vi.resetModules();
54-
state.isUndefinedColumnError.mockReturnValue(false);
55-
state.isUndefinedTableError.mockReturnValue(false);
5647
});
5748

5849
describe("infra resources DAL", () => {
@@ -140,9 +131,8 @@ describe("infra resources DAL", () => {
140131
});
141132

142133
it("wraps undefined-table/column errors into db_not_migrated", async () => {
143-
const err = new Error("missing");
134+
const err = Object.assign(new Error("missing"), { code: "42P01" });
144135
state.findMany.mockRejectedValueOnce(err);
145-
state.isUndefinedTableError.mockReturnValueOnce(true);
146136

147137
const { listInfraResourcesByProject } = await import(
148138
"@/lib/data/infra-resources.server"

0 commit comments

Comments
 (0)