fix(api): resolve CI typecheck failures in GRO-485 fix

Fix type errors that caused CI Lint & Typecheck job to fail:
- setup.ts: replace unavailable isNull import with sql template tag
  (isNull not exported from @groombook/db; sql IS exported)
- setup.ts: add non-null assertion on newStaff after insert.returning()
- setup.test.ts: add sql mock template tag to @groombook/db mock
- setup.test.ts: fix evaluateCond to handle sql template tag type
- setup.test.ts: add type assertions for body.staff in OOBE regression tests
- setup.test.ts: fix dbStaffRows type casts in mock insert function

All 18 tests pass, full typecheck clean.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Flea Flicker
2026-04-05 20:05:53 +00:00
parent fa18c41677
commit 8348f1c152
2 changed files with 24 additions and 15 deletions
+21 -12
View File
@@ -88,7 +88,7 @@ vi.mock("@groombook/db", () => {
const rows = getRowsForTable(table); const rows = getRowsForTable(table);
const base = { const base = {
where: (cond?: unknown) => { where: (cond?: unknown) => {
const filtered = cond ? rows.filter((r) => evaluateCond(cond, r)) : rows; const filtered = cond ? rows.filter((r) => evaluateCond(cond, r as Record<string, unknown>)) : rows;
return { return {
limit: () => filtered, limit: () => filtered,
for: () => ({ for: () => ({
@@ -126,9 +126,9 @@ vi.mock("@groombook/db", () => {
} else if (vals.email) { } else if (vals.email) {
// staff insert // staff insert
insertedStaff.push(vals); insertedStaff.push(vals);
dbStaffRows.push(row as MockStaff); dbStaffRows.push(row as unknown as MockStaff);
} else if (vals.businessName) { } else if (vals.businessName) {
dbBusinessSettingsRows.push(row as { id: string; businessName: string }); dbBusinessSettingsRows.push(row as unknown as { id: string; businessName: string });
} }
return { returning: () => [row] }; return { returning: () => [row] };
}, },
@@ -159,7 +159,7 @@ vi.mock("@groombook/db", () => {
: table === businessSettings : table === businessSettings
? dbBusinessSettingsRows ? dbBusinessSettingsRows
: []; : [];
const filtered = cond ? rows.filter((r) => evaluateCond(cond, r)) : rows; const filtered = cond ? rows.filter((r) => evaluateCond(cond, r as Record<string, unknown>)) : rows;
return { return {
limit: () => filtered, limit: () => filtered,
for: () => ({ for: () => ({
@@ -214,9 +214,9 @@ vi.mock("@groombook/db", () => {
dbAuthConfigRows.push({ id: row.id as string, enabled: vals.enabled as boolean }); dbAuthConfigRows.push({ id: row.id as string, enabled: vals.enabled as boolean });
} else if (vals.email) { } else if (vals.email) {
insertedStaff.push(vals); insertedStaff.push(vals);
dbStaffRows.push(row as MockStaff); dbStaffRows.push(row as unknown as MockStaff);
} else if (vals.businessName) { } else if (vals.businessName) {
dbBusinessSettingsRows.push(row as { id: string; businessName: string }); dbBusinessSettingsRows.push(row as unknown as { id: string; businessName: string });
} }
return { returning: () => [row] }; return { returning: () => [row] };
}, },
@@ -229,6 +229,11 @@ vi.mock("@groombook/db", () => {
eq: (col: unknown, val: unknown) => ({ __type: "eq", col, val }), eq: (col: unknown, val: unknown) => ({ __type: "eq", col, val }),
and: (...conds: unknown[]) => ({ __type: "and", conds }), and: (...conds: unknown[]) => ({ __type: "and", conds }),
isNull: (col: unknown) => ({ __type: "isNull", col }), isNull: (col: unknown) => ({ __type: "isNull", col }),
sql: (strings: TemplateStringsArray, ...values: unknown[]) => {
// Mock sql template tag — raw SQL can't be evaluated in mock, always passes
void strings; void values;
return { __type: "sql" };
},
encryptSecret: (val: string) => { encryptSecret: (val: string) => {
encryptCalls.push(val); encryptCalls.push(val);
return `encrypted:${val}`; return `encrypted:${val}`;
@@ -253,6 +258,10 @@ function evaluateCond(cond: unknown, row: Record<string, unknown>): boolean {
const colName = colObj.column as string; const colName = colObj.column as string;
return row[colName] === null || row[colName] === undefined; return row[colName] === null || row[colName] === undefined;
} }
if (c.__type === "sql") {
// Raw SQL can't be evaluated in mock — pass through
return true;
}
return true; return true;
} }
@@ -597,9 +606,9 @@ describe("POST /setup — OOBE regression (GRO-485)", () => {
expect(status).toBe(201); expect(status).toBe(201);
expect(body.ok).toBe(true); expect(body.ok).toBe(true);
expect(body.staff).toBeDefined(); expect(body.staff).toBeDefined();
expect(body.staff.isSuperUser).toBe(true); expect((body.staff as MockStaff).isSuperUser).toBe(true);
expect(body.staff.email).toBe("alice@example.com"); expect((body.staff as any).email).toBe("alice@example.com");
expect(body.staff.role).toBe("manager"); expect((body.staff as MockStaff).role).toBe("manager");
// New staff record was created // New staff record was created
expect(insertedStaff.length).toBe(1); expect(insertedStaff.length).toBe(1);
expect(insertedStaff[0]!.email).toBe("alice@example.com"); expect(insertedStaff[0]!.email).toBe("alice@example.com");
@@ -619,13 +628,13 @@ describe("POST /setup — OOBE regression (GRO-485)", () => {
expect(status).toBe(201); expect(status).toBe(201);
expect(body.ok).toBe(true); expect(body.ok).toBe(true);
expect(body.staff.isSuperUser).toBe(true); expect((body.staff as MockStaff).isSuperUser).toBe(true);
// No new staff was created (insertedStaff should be empty since staff was pre-existing) // No new staff was created (insertedStaff should be empty since staff was pre-existing)
}); });
it("auto-links staff by email if record exists with matching email but no userId", async () => { it("auto-links staff by email if record exists with matching email but no userId", async () => {
// Staff record exists with matching email but no userId (legacy record) // Staff record exists with matching email but no userId (legacy record)
dbStaffRows = [{ id: "staff-legacy", role: "manager", isSuperUser: false, email: "alice@example.com", userId: null }]; dbStaffRows = [{ id: "staff-legacy", role: "manager", isSuperUser: false, email: "alice@example.com", userId: null } as unknown as MockStaff];
dbBusinessSettingsRows = []; dbBusinessSettingsRows = [];
const jwtPayload = { sub: "user-123", email: "alice@example.com", name: "Alice" }; const jwtPayload = { sub: "user-123", email: "alice@example.com", name: "Alice" };
@@ -636,7 +645,7 @@ describe("POST /setup — OOBE regression (GRO-485)", () => {
expect(status).toBe(201); expect(status).toBe(201);
expect(body.ok).toBe(true); expect(body.ok).toBe(true);
expect(body.staff.isSuperUser).toBe(true); expect((body.staff as MockStaff).isSuperUser).toBe(true);
}); });
it("returns 400 if JWT has no email claim and no staff record exists", async () => { it("returns 400 if JWT has no email claim and no staff record exists", async () => {
+3 -3
View File
@@ -1,7 +1,7 @@
import { Hono } from "hono"; import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { z } from "zod/v3"; import { z } from "zod/v3";
import { and, eq, getDb, isNull, staff, businessSettings, authProviderConfig, encryptSecret } from "@groombook/db"; import { and, eq, getDb, sql, staff, businessSettings, authProviderConfig, encryptSecret } from "@groombook/db";
import type { AppEnv } from "../middleware/rbac.js"; import type { AppEnv } from "../middleware/rbac.js";
export const setupRouter = new Hono<AppEnv>(); export const setupRouter = new Hono<AppEnv>();
@@ -102,7 +102,7 @@ setupRouter.post("/", zValidator("json", setupSchema), async (c) => {
const [byEmail] = await tx const [byEmail] = await tx
.select() .select()
.from(staff) .from(staff)
.where(and(eq(staff.email, jwt.email), isNull(staff.userId))); .where(and(eq(staff.email, jwt.email), sql`${staff.userId} IS NULL`));
if (byEmail) { if (byEmail) {
await tx await tx
.update(staff) .update(staff)
@@ -127,7 +127,7 @@ setupRouter.post("/", zValidator("json", setupSchema), async (c) => {
isSuperUser: false, // will be set below isSuperUser: false, // will be set below
}) })
.returning(); .returning();
resolvedStaff = newStaff; resolvedStaff = newStaff!;
} }
// Mark as super user // Mark as super user