diff --git a/.ast-grep/rule-tests/__snapshots__/no-countall-number-snapshot.yml b/.ast-grep/rule-tests/__snapshots__/no-countall-number-snapshot.yml new file mode 100644 index 0000000..61633c4 --- /dev/null +++ b/.ast-grep/rule-tests/__snapshots__/no-countall-number-snapshot.yml @@ -0,0 +1,16 @@ +id: no-countall-number +snapshots: + countAll(): + fixed: countAll() + labels: + - source: countAll() + style: primary + start: 0 + end: 18 + eb.fn.countAll().as("count"): + fixed: eb.fn.countAll().as("count") + labels: + - source: eb.fn.countAll() + style: primary + start: 0 + end: 24 diff --git a/.ast-grep/rule-tests/__snapshots__/no-string-function-snapshot.yml b/.ast-grep/rule-tests/__snapshots__/no-string-function-snapshot.yml new file mode 100644 index 0000000..e9713eb --- /dev/null +++ b/.ast-grep/rule-tests/__snapshots__/no-string-function-snapshot.yml @@ -0,0 +1,20 @@ +id: no-string-function +snapshots: + String(123): + labels: + - source: String(123) + style: primary + start: 0 + end: 11 + String(Date.now()): + labels: + - source: String(Date.now()) + style: primary + start: 0 + end: 18 + String(value): + labels: + - source: String(value) + style: primary + start: 0 + end: 13 diff --git a/.ast-grep/rule-tests/__snapshots__/zod-namespace-import-snapshot.yml b/.ast-grep/rule-tests/__snapshots__/zod-namespace-import-snapshot.yml index 07904da..01ec3d2 100644 --- a/.ast-grep/rule-tests/__snapshots__/zod-namespace-import-snapshot.yml +++ b/.ast-grep/rule-tests/__snapshots__/zod-namespace-import-snapshot.yml @@ -3,7 +3,7 @@ snapshots: ? | import { z } from "zod"; : fixed: | - import * as z from "zod" + import * as z from "zod"; labels: - source: import { z } from "zod"; style: primary @@ -12,7 +12,7 @@ snapshots: ? | import { z, ZodError } from "zod"; : fixed: | - import * as z from "zod" + import * as z from "zod"; labels: - source: import { z, ZodError } from "zod"; style: primary diff --git a/.ast-grep/rule-tests/no-countall-number-test.yml b/.ast-grep/rule-tests/no-countall-number-test.yml new file mode 100644 index 0000000..605ea5e --- /dev/null +++ b/.ast-grep/rule-tests/no-countall-number-test.yml @@ -0,0 +1,9 @@ +id: no-countall-number +valid: + # Plain countAll() is fine + - eb.fn.countAll().as("count") + # Other type arguments are fine + - eb.fn.countAll().as("count") +invalid: + # countAll() should be flagged + - eb.fn.countAll().as("count") diff --git a/.ast-grep/rule-tests/no-string-function-test.yml b/.ast-grep/rule-tests/no-string-function-test.yml new file mode 100644 index 0000000..814c8dd --- /dev/null +++ b/.ast-grep/rule-tests/no-string-function-test.yml @@ -0,0 +1,13 @@ +id: no-string-function +valid: + # toString() is fine + - value.toString() + - (123).toString() + - date.toLocaleString() + # Other functions named String are fine + - myString(value) +invalid: + # String() function should be flagged + - String(value) + - String(123) + - String(Date.now()) diff --git a/.ast-grep/rules/no-countall-number.yml b/.ast-grep/rules/no-countall-number.yml index 3835ee8..da99e94 100644 --- a/.ast-grep/rules/no-countall-number.yml +++ b/.ast-grep/rules/no-countall-number.yml @@ -4,5 +4,5 @@ severity: error message: "Don't use countAll() - use countAll() instead. PostgreSQL COUNT returns bigint (string), so the type annotation is misleading." note: "Use Number() to convert the result if you need a number type." rule: - pattern: countAll() -fix: countAll() + pattern: $OBJ.countAll() +fix: $OBJ.countAll() diff --git a/.ast-grep/rules/no-string-function.yml b/.ast-grep/rules/no-string-function.yml new file mode 100644 index 0000000..10044df --- /dev/null +++ b/.ast-grep/rules/no-string-function.yml @@ -0,0 +1,7 @@ +id: no-string-function +language: typescript +severity: error +message: "Don't use String() - use .toString() or .toLocaleString() instead." +note: "String() can have unexpected behavior. Use .toString() for general conversion or .toLocaleString() for locale-aware formatting." +rule: + pattern: String($VAL) diff --git a/apps/api-server/src/__tests__/e2e/admin.test.ts b/apps/api-server/src/__tests__/e2e/admin.test.ts index 6221c2e..bb0245b 100644 --- a/apps/api-server/src/__tests__/e2e/admin.test.ts +++ b/apps/api-server/src/__tests__/e2e/admin.test.ts @@ -36,6 +36,7 @@ import { initTestDb, TEST_RP, truncateAllTables, + uniqueTestId, withTestTransaction, } from "@reviq/test-helpers"; import { router } from "../../router.js"; @@ -84,7 +85,7 @@ async function createSession( db: Kysely, userId: number, ): Promise<{ token: string; sessionId: number }> { - const token = `test-session-${String(Date.now())}${String(Math.random())}`; + const token = `test-session-${uniqueTestId()}`; const tokenHashValue = await hashToken(token); const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS); @@ -115,9 +116,7 @@ async function createOrg( logoUrl?: string; }, ): Promise<{ id: number; slug: string }> { - const slug = - options?.slug ?? - `org-${String(Date.now())}-${String(Math.random()).slice(2, 8)}`; + const slug = options?.slug ?? `org-${uniqueTestId()}`; const result = await db .insertInto("orgs") @@ -183,7 +182,7 @@ async function createLoginRequest( expiresAt?: Date; }, ): Promise<{ id: number; token: string }> { - const token = `login-${String(Date.now())}${String(Math.random())}`; + const token = `login-${uniqueTestId()}`; const expiresAt = options?.expiresAt ?? new Date(Date.now() + LOGIN_REQUEST_EXPIRY_MS); @@ -212,7 +211,7 @@ async function createOrgInvite( email: string, invitedBy: number, ): Promise<{ id: number }> { - const token = `invite-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const token = `invite-${uniqueTestId()}`; const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days const result = await db @@ -461,7 +460,7 @@ describeE2E("admin", () => { test("creates passwordless user", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -492,7 +491,7 @@ describeE2E("admin", () => { test("creates user with name", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -519,7 +518,7 @@ describeE2E("admin", () => { test("creates user and adds to organization as member", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -554,7 +553,7 @@ describeE2E("admin", () => { test("creates user and adds to organization with custom role", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -588,7 +587,7 @@ describeE2E("admin", () => { test("normalizes email to lowercase", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -615,7 +614,7 @@ describeE2E("admin", () => { test("throws CONFLICT for duplicate email", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -637,7 +636,7 @@ describeE2E("admin", () => { test("throws NOT_FOUND for non-existent org", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -1060,7 +1059,7 @@ describeE2E("admin", () => { test("creates organization with owner", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -1109,7 +1108,7 @@ describeE2E("admin", () => { test("normalizes owner email to lowercase", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -1135,7 +1134,7 @@ describeE2E("admin", () => { test("throws NOT_FOUND for non-existent owner", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -1160,7 +1159,7 @@ describeE2E("admin", () => { test("throws CONFLICT for duplicate slug", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -1284,7 +1283,7 @@ describeE2E("admin", () => { await createOrg(db, { slug: "test-org", displayName: "Old", - logoUrl: null, + logoUrl: undefined, }); const { token: sessionToken } = await createSession(db, admin.id); @@ -1379,7 +1378,7 @@ describeE2E("admin", () => { test("deletes organization and related records", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -1444,7 +1443,7 @@ describeE2E("admin", () => { test("throws NOT_FOUND for non-existent organization", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -1564,7 +1563,7 @@ describeE2E("admin", () => { test("adds site to organization", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -1596,7 +1595,7 @@ describeE2E("admin", () => { test("throws NOT_FOUND for non-existent organization", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -1617,7 +1616,7 @@ describeE2E("admin", () => { test("throws CONFLICT for duplicate domain", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -1640,7 +1639,7 @@ describeE2E("admin", () => { test("throws CONFLICT for domain in another organization", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -1793,7 +1792,7 @@ describeE2E("admin", () => { // Verify login request was completed const request = await db .selectFrom("login_requests") - .where("id", "=", String(loginRequest.id)) + .where("id", "=", loginRequest.id.toString()) .select(["completed_at"]) .executeTakeFirstOrThrow(); @@ -1825,7 +1824,7 @@ describeE2E("admin", () => { const request = await db .selectFrom("login_requests") - .where("id", "=", String(loginRequest.id)) + .where("id", "=", loginRequest.id.toString()) .select(["completed_at"]) .executeTakeFirstOrThrow(); diff --git a/apps/api-server/src/__tests__/e2e/auth.test.ts b/apps/api-server/src/__tests__/e2e/auth.test.ts index 99088f2..5654241 100644 --- a/apps/api-server/src/__tests__/e2e/auth.test.ts +++ b/apps/api-server/src/__tests__/e2e/auth.test.ts @@ -47,6 +47,7 @@ import { getSharedDb, initTestDb, TEST_RP, + uniqueTestId, withTestTransaction, } from "@reviq/test-helpers"; import { VirtualAuthenticator } from "@reviq/virtual-authenticator"; @@ -146,7 +147,7 @@ async function createSession( userId: number, options?: { deviceId?: bigint }, ): Promise<{ token: string; sessionId: number }> { - const token = `test-session-${String(Date.now())}${String(Math.random())}`; + const token = `test-session-${uniqueTestId()}`; const tokenHashValue = await hashToken(token); const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS); @@ -154,7 +155,7 @@ async function createSession( .insertInto("sessions") .values({ user_id: userId, - device_id: options?.deviceId ? String(options.deviceId) : null, + device_id: options?.deviceId ? options.deviceId.toString() : null, token_hash: tokenHashValue, trusted_mode: false, expires_at: expiresAt, @@ -178,7 +179,7 @@ async function createLoginRequest( expiresAt?: Date; }, ): Promise<{ token: string; id: number }> { - const token = `login_test-${String(Date.now())}${String(Math.random())}`; + const token = `login_test-${uniqueTestId()}`; const expiresAt = options?.expiresAt ?? new Date(Date.now() + LOGIN_REQUEST_EXPIRY_MS); @@ -228,7 +229,7 @@ async function createEmailVerification( userId: number, options?: { expiresAt?: Date }, ): Promise { - const token = `verify-${String(Date.now())}${String(Math.random())}`; + const token = `verify-${uniqueTestId()}`; const expiresAt = options?.expiresAt ?? new Date(Date.now() + 24 * 60 * 60 * 1000); @@ -252,7 +253,7 @@ async function createPasswordReset( userId: number, options?: { expiresAt?: Date; usedAt?: Date | null }, ): Promise { - const token = `reset-${String(Date.now())}${String(Math.random())}`; + const token = `reset-${uniqueTestId()}`; const expiresAt = options?.expiresAt ?? new Date(Date.now() + 60 * 60 * 1000); await db @@ -457,7 +458,7 @@ describeE2E("auth", () => { const challenges = await db .selectFrom("webauthn_challenges") .selectAll() - .where("id", "=", String(challengeId)) + .where("id", "=", challengeId.toString()) .execute(); expect(challenges.length).toBe(0); }); @@ -483,7 +484,7 @@ describeE2E("auth", () => { await db .updateTable("webauthn_challenges") .set({ created_at: new Date(Date.now() - 20 * 60 * 1000) }) // 20 minutes ago - .where("id", "=", String(challengeId)) + .where("id", "=", challengeId.toString()) .execute(); // Step 4: Try to signup with expired challenge @@ -540,7 +541,7 @@ describeE2E("auth", () => { const challenges = await db .selectFrom("webauthn_challenges") .selectAll() - .where("id", "=", String(challengeId)) + .where("id", "=", challengeId.toString()) .execute(); expect(challenges.length).toBe(0); }); @@ -1072,7 +1073,7 @@ describeE2E("auth", () => { const loginRequest = await db .selectFrom("login_requests") .selectAll() - .where("id", "=", String(loginRequestId)) + .where("id", "=", loginRequestId.toString()) .executeTakeFirst(); expect(loginRequest).toBeUndefined(); @@ -1152,7 +1153,7 @@ describeE2E("auth", () => { }); // Create login request without device fingerprint - const token = `login_test-${String(Date.now())}`; + const token = `login_test-${uniqueTestId()}`; await db .insertInto("login_requests") .values({ @@ -1644,7 +1645,7 @@ describeE2E("auth", () => { const session = await db .selectFrom("sessions") .select(["revoked_at"]) - .where("id", "=", String(sessionId)) + .where("id", "=", sessionId.toString()) .executeTakeFirst(); expect(session?.revoked_at).not.toBeNull(); @@ -1981,7 +1982,7 @@ describeE2E("auth", () => { // Clean up registration session await db .deleteFrom("sessions") - .where("id", "=", String(regSessionId)) + .where("id", "=", regSessionId.toString()) .execute(); // Step 1: Create login request diff --git a/apps/api-server/src/__tests__/e2e/me.test.ts b/apps/api-server/src/__tests__/e2e/me.test.ts index a1116a5..61ecb62 100644 --- a/apps/api-server/src/__tests__/e2e/me.test.ts +++ b/apps/api-server/src/__tests__/e2e/me.test.ts @@ -29,6 +29,7 @@ import { getSharedDb, initTestDb, TEST_RP, + uniqueTestId, withTestTransaction, } from "@reviq/test-helpers"; import { router } from "../../router.js"; @@ -92,7 +93,7 @@ async function createSession( userId: number, options?: { ipAddress?: string; userAgent?: string }, ): Promise<{ token: string; sessionId: number }> { - const token = `test-session-${String(Date.now())}${String(Math.random())}`; + const token = `test-session-${uniqueTestId()}`; const tokenHashValue = await hashToken(token); const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS); @@ -125,9 +126,7 @@ async function createDevice( userAgent?: string; }, ): Promise<{ fingerprint: string; deviceId: number }> { - const fingerprint = - options?.fingerprint ?? - `test-fp-${String(Date.now())}${String(Math.random())}`; + const fingerprint = options?.fingerprint ?? `test-fp-${uniqueTestId()}`; const result = await db .insertInto("user_devices") @@ -153,7 +152,7 @@ async function createApiToken( db: Kysely, userId: number, ): Promise<{ token: string; name: string }> { - const token = `test-api-token-${String(Date.now())}${String(Math.random())}`; + const token = `test-api-token-${uniqueTestId()}`; const tokenHashValue = await hashToken(token); const expiresAt = new Date(Date.now() + API_TOKEN_EXPIRY_MS); @@ -224,7 +223,7 @@ describeE2E("me", () => { const user = await createTestUser(db, { email: "expired@example.com" }); // Create an expired session - const token = `expired-session-${String(Date.now())}`; + const token = `expired-session-${uniqueTestId()}`; const tokenHashValue = await hashToken(token); await db .insertInto("sessions") @@ -249,7 +248,7 @@ describeE2E("me", () => { const user = await createTestUser(db, { email: "revoked@example.com" }); // Create a revoked session - const token = `revoked-session-${String(Date.now())}`; + const token = `revoked-session-${uniqueTestId()}`; const tokenHashValue = await hashToken(token); await db .insertInto("sessions") @@ -925,7 +924,7 @@ describeE2E("me", () => { country: "US", trusted_mode: true, }) - .where("id", "=", String(sessionId)) + .where("id", "=", sessionId.toString()) .execute(); const context = createAPIContext(db, { sessionToken }); @@ -968,7 +967,7 @@ describeE2E("me", () => { const session = await db .selectFrom("sessions") .select(["revoked_at"]) - .where("id", "=", String(sessionId2)) + .where("id", "=", sessionId2.toString()) .executeTakeFirstOrThrow(); expect(session.revoked_at).not.toBeNull(); @@ -1021,7 +1020,7 @@ describeE2E("me", () => { await db .updateTable("sessions") .set({ revoked_at: new Date() }) - .where("id", "=", String(sessionId2)) + .where("id", "=", sessionId2.toString()) .execute(); const context = createAPIContext(db, { sessionToken: sessionToken1 }); @@ -1080,7 +1079,7 @@ describeE2E("me", () => { const currentSession = await db .selectFrom("sessions") .select(["revoked_at"]) - .where("id", "=", String(id1)) + .where("id", "=", id1.toString()) .executeTakeFirstOrThrow(); expect(currentSession.revoked_at).toBeNull(); @@ -1088,7 +1087,7 @@ describeE2E("me", () => { const otherSessions = await db .selectFrom("sessions") .select(["id", "revoked_at"]) - .where("id", "in", [String(id2), String(id3)]) + .where("id", "in", [id2.toString(), id3.toString()]) .execute(); for (const session of otherSessions) { @@ -1116,7 +1115,7 @@ describeE2E("me", () => { const session = await db .selectFrom("sessions") .select(["revoked_at"]) - .where("id", "=", String(sessionId)) + .where("id", "=", sessionId.toString()) .executeTakeFirstOrThrow(); expect(session.revoked_at).toBeNull(); }); @@ -1147,7 +1146,7 @@ describeE2E("me", () => { region: "NY", country: "US", }) - .where("id", "=", String(deviceId)) + .where("id", "=", deviceId.toString()) .execute(); const { token: sessionToken } = await createSession(db, user.id); @@ -1256,7 +1255,7 @@ describeE2E("me", () => { const device = await db .selectFrom("user_devices") .select(["is_trusted", "name"]) - .where("id", "=", String(deviceId)) + .where("id", "=", deviceId.toString()) .executeTakeFirstOrThrow(); expect(device.is_trusted).toBe(true); @@ -1401,7 +1400,7 @@ describeE2E("me", () => { const device = await db .selectFrom("user_devices") .select(["is_trusted"]) - .where("id", "=", String(deviceId)) + .where("id", "=", deviceId.toString()) .executeTakeFirstOrThrow(); expect(device.is_trusted).toBe(false); @@ -1501,7 +1500,7 @@ async function createTrustedSession( db: Kysely, userId: number, ): Promise<{ token: string; sessionId: number }> { - const token = `test-session-${String(Date.now())}${String(Math.random())}`; + const token = `test-session-${uniqueTestId()}`; const tokenHashValue = await hashToken(token); const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS); @@ -1568,7 +1567,7 @@ async function createOrgInvite( expiresAt?: Date; }, ): Promise<{ id: number }> { - const token = `invite-token-${String(Date.now())}-${Math.random().toString(36).slice(2)}`; + const token = `invite-token-${uniqueTestId()}-${Math.random().toString(36).slice(2)}`; const result = await db .insertInto("org_invites") .values({ @@ -1693,7 +1692,7 @@ describeE2E("me.apiTokens and me.invites", () => { }); expect(tokens).toHaveLength(1); - expect(tokens[0].name).toBe("User1 Token"); + expect(tokens[0]?.name).toBe("User1 Token"); }); }); }); @@ -1727,7 +1726,7 @@ describeE2E("me.apiTokens and me.invites", () => { .execute(); expect(tokens).toHaveLength(1); - expect(tokens[0].name).toBe("My New Token"); + expect(tokens[0]?.name).toBe("My New Token"); }); }); @@ -1937,10 +1936,10 @@ describeE2E("me.apiTokens and me.invites", () => { }); expect(invites).toHaveLength(1); - expect(invites[0].org.slug).toBe("invite-org"); - expect(invites[0].org.displayName).toBe("Invite Org"); - expect(invites[0].role).toBe("admin"); - expect(invites[0].invitedBy).toBe("Inviter Person"); + expect(invites[0]?.org.slug).toBe("invite-org"); + expect(invites[0]?.org.displayName).toBe("Invite Org"); + expect(invites[0]?.role).toBe("admin"); + expect(invites[0]?.invitedBy).toBe("Inviter Person"); }); }); @@ -2086,7 +2085,7 @@ describeE2E("me.apiTokens and me.invites", () => { describe("me.invites.accept", () => { test("accepts invite and adds user to org", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const inviter = await createTestUser(db, { email: `inviter-accept-${uniqueId}@example.com`, @@ -2188,7 +2187,7 @@ describeE2E("me.apiTokens and me.invites", () => { test("returns error if already a member", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const inviter = await createTestUser(db, { email: `inviter-already-${uniqueId}@example.com`, diff --git a/apps/api-server/src/__tests__/e2e/orgs.test.ts b/apps/api-server/src/__tests__/e2e/orgs.test.ts index a05ac81..40ea8a2 100644 --- a/apps/api-server/src/__tests__/e2e/orgs.test.ts +++ b/apps/api-server/src/__tests__/e2e/orgs.test.ts @@ -20,6 +20,7 @@ import { getSharedDb, initTestDb, TEST_RP, + uniqueTestId, withTestTransaction, } from "@reviq/test-helpers"; import { router } from "../../router.js"; @@ -68,7 +69,7 @@ async function createSession( userId: number, options?: { trustedMode?: boolean }, ): Promise<{ token: string; sessionId: number }> { - const token = `test-session-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const token = `test-session-${uniqueTestId()}`; const tokenHashValue = await hashToken(token); const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS); @@ -166,9 +167,7 @@ async function createOrgInvite( expiresAt?: Date; }, ): Promise<{ id: number; token: string }> { - const token = - options?.token ?? - `invite-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const token = options?.token ?? `invite-${uniqueTestId()}`; const expiresAt = options?.expiresAt ?? new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); @@ -319,7 +318,7 @@ describeE2E("orgs", () => { describe("orgs.create", () => { test("creates org and makes user owner", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const user = await createTestUser(db, { email: `user-${uniqueId}@example.com`, @@ -349,7 +348,7 @@ describeE2E("orgs", () => { test("rejects duplicate slug", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const user = await createTestUser(db, { email: `user-${uniqueId}@example.com`, @@ -532,7 +531,7 @@ describeE2E("orgs", () => { describe("orgs.delete", () => { test("deletes org when user is owner", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const user = await createTestUser(db, { email: `user-${uniqueId}@example.com`, @@ -581,7 +580,7 @@ describeE2E("orgs", () => { describe("orgs.leave", () => { test("allows member to leave org", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, @@ -614,7 +613,7 @@ describeE2E("orgs", () => { test("allows owner to leave when there are other owners", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner1 = await createTestUser(db, { email: `owner1-${uniqueId}@example.com`, @@ -647,7 +646,7 @@ describeE2E("orgs", () => { test("prevents only owner from leaving", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, @@ -770,7 +769,7 @@ describeE2E("orgs", () => { describe("orgs.members.updateRole", () => { test("owner can promote member to admin", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, @@ -803,7 +802,7 @@ describeE2E("orgs", () => { test("owner can promote member to owner", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, @@ -836,7 +835,7 @@ describeE2E("orgs", () => { test("owner can demote owner to admin when multiple owners exist", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner1 = await createTestUser(db, { email: `owner1-${uniqueId}@example.com`, @@ -869,7 +868,7 @@ describeE2E("orgs", () => { test("prevents demoting the only owner", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, @@ -916,7 +915,7 @@ describeE2E("orgs", () => { test("rejects when target member not found", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, @@ -942,7 +941,7 @@ describeE2E("orgs", () => { describe("orgs.members.remove", () => { test("owner can remove member", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, @@ -975,7 +974,7 @@ describeE2E("orgs", () => { test("owner can remove admin", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, @@ -1008,7 +1007,7 @@ describeE2E("orgs", () => { test("owner can remove other owner when multiple owners exist", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner1 = await createTestUser(db, { email: `owner1-${uniqueId}@example.com`, @@ -1041,7 +1040,7 @@ describeE2E("orgs", () => { test("prevents removing the only owner", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, @@ -1065,7 +1064,7 @@ describeE2E("orgs", () => { test("admin can remove member", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, @@ -1102,7 +1101,7 @@ describeE2E("orgs", () => { test("admin cannot remove owner", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, @@ -1130,7 +1129,7 @@ describeE2E("orgs", () => { test("admin cannot remove other admin", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, @@ -1162,7 +1161,7 @@ describeE2E("orgs", () => { test("member cannot remove anyone", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, @@ -1192,7 +1191,7 @@ describeE2E("orgs", () => { test("rejects when target member not found", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, @@ -1297,7 +1296,7 @@ describeE2E("orgs", () => { describe("orgs.invites.create", () => { test("admin can create member invite", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -1331,7 +1330,7 @@ describeE2E("orgs", () => { test("admin can create admin invite", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, @@ -1385,7 +1384,7 @@ describeE2E("orgs", () => { test("owner can create owner invite", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, @@ -1571,7 +1570,7 @@ describeE2E("orgs", () => { describe("orgs.invites.accept", () => { test("accepts invite and adds user to org", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, @@ -1669,9 +1668,7 @@ describeE2E("orgs", () => { test("rejects when email doesn't match", async () => { await withTestTransaction(getSharedDb(), async (db) => { const owner = await createTestUser(db, { email: "owner@example.com" }); - const _invitee = await createTestUser(db, { - email: "invitee@example.com", - }); + await createTestUser(db, { email: "invitee@example.com" }); const wrongUser = await createTestUser(db, { email: "wrong@example.com", }); @@ -1701,7 +1698,7 @@ describeE2E("orgs", () => { test("handles already a member gracefully", async () => { const db = getSharedDb(); - const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`; + const uniqueId = uniqueTestId(); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, diff --git a/apps/api-server/src/__tests__/e2e/webauthn.test.ts b/apps/api-server/src/__tests__/e2e/webauthn.test.ts index faf9e87..d007150 100644 --- a/apps/api-server/src/__tests__/e2e/webauthn.test.ts +++ b/apps/api-server/src/__tests__/e2e/webauthn.test.ts @@ -20,6 +20,7 @@ import { initTestDb, KNOWN_AAGUIDS, TEST_RP, + uniqueTestId, withTestTransaction, } from "@reviq/test-helpers"; import { VirtualAuthenticator } from "@reviq/virtual-authenticator"; @@ -60,7 +61,7 @@ async function createSession( db: Kysely, userId: number, ): Promise { - const token = `test-session-${String(Date.now())}${String(Math.random())}`; + const token = `test-session-${uniqueTestId()}`; const tokenHashValue = await hashToken(token); const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS); @@ -87,7 +88,7 @@ async function createLoginRequest( userId: number, email: string, ): Promise<{ id: number; token: string }> { - const token = `test-login-${String(Date.now())}${String(Math.random())}`; + const token = `test-login-${uniqueTestId()}`; const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes const result = await db @@ -236,7 +237,7 @@ describeE2E("webauthn", () => { const challengeRow = await db .selectFrom("webauthn_challenges") .select("id") - .where("id", "=", String(challengeId)) + .where("id", "=", challengeId.toString()) .executeTakeFirst(); expect(challengeRow).toBeDefined(); @@ -382,7 +383,7 @@ describeE2E("webauthn", () => { const challengeRow = await db .selectFrom("webauthn_challenges") .select("id") - .where("id", "=", String(challengeId)) + .where("id", "=", challengeId.toString()) .executeTakeFirst(); expect(challengeRow).toBeUndefined(); @@ -585,7 +586,7 @@ describeE2E("webauthn", () => { const challengeRow = await db .selectFrom("webauthn_challenges") .select("id") - .where("id", "=", String(authChallengeId)) + .where("id", "=", authChallengeId.toString()) .executeTakeFirst(); expect(challengeRow).toBeUndefined(); diff --git a/apps/api-server/src/constants.ts b/apps/api-server/src/constants.ts index 2ae3206..78d4ae7 100644 --- a/apps/api-server/src/constants.ts +++ b/apps/api-server/src/constants.ts @@ -22,7 +22,7 @@ export const getAllowedOrigins = (): string[] => { // Default to localhost origins for development return [ - `http://localhost:${String(DEFAULT_PORT)}`, + `http://localhost:${DEFAULT_PORT.toString()}`, "http://localhost:6827", "http://localhost:6828", ]; diff --git a/apps/api-server/src/index.ts b/apps/api-server/src/index.ts index e2a8926..ff87f83 100644 --- a/apps/api-server/src/index.ts +++ b/apps/api-server/src/index.ts @@ -45,7 +45,7 @@ Bun.serve({ if (url.pathname.startsWith("/api/v1/rpc")) { // Build context for the request const origin = - request.headers.get("origin") ?? `http://localhost:${String(port)}`; + request.headers.get("origin") ?? `http://localhost:${port.toString()}`; // Create response headers for setting cookies const resHeaders = new Headers(); diff --git a/apps/api-server/src/procedures/auth/signup.ts b/apps/api-server/src/procedures/auth/signup.ts index 19fcd1c..7e1cc67 100644 --- a/apps/api-server/src/procedures/auth/signup.ts +++ b/apps/api-server/src/procedures/auth/signup.ts @@ -108,7 +108,7 @@ export async function signupWithPasskey( const challengeRow = await db .selectFrom("webauthn_challenges") .select("options") - .where("id", "=", String(challengeId)) + .where("id", "=", challengeId.toString()) .where("created_at", ">", fifteenMinutesAgo) .executeTakeFirst(); @@ -134,7 +134,7 @@ export async function signupWithPasskey( // Delete the challenge await db .deleteFrom("webauthn_challenges") - .where("id", "=", String(challengeId)) + .where("id", "=", challengeId.toString()) .execute(); // Log error for debugging but don't expose to client @@ -149,7 +149,7 @@ export async function signupWithPasskey( // Delete the challenge await db .deleteFrom("webauthn_challenges") - .where("id", "=", String(challengeId)) + .where("id", "=", challengeId.toString()) .execute(); throw new ORPCError("BAD_REQUEST", { @@ -200,7 +200,7 @@ export async function signupWithPasskey( // Delete the challenge await trx .deleteFrom("webauthn_challenges") - .where("id", "=", String(challengeId)) + .where("id", "=", challengeId.toString()) .execute(); return { userId: newUserId }; diff --git a/apps/api-server/src/procedures/me/api-tokens.ts b/apps/api-server/src/procedures/me/api-tokens.ts index 92ba464..b771aba 100644 --- a/apps/api-server/src/procedures/me/api-tokens.ts +++ b/apps/api-server/src/procedures/me/api-tokens.ts @@ -95,7 +95,7 @@ export const deleteApiToken = os.me.apiTokens.delete .handler(async ({ input, context }) => { const result = await context.db .deleteFrom("api_tokens") - .where("id", "=", String(input.tokenId)) + .where("id", "=", input.tokenId.toString()) .where("user_id", "=", context.user.id) .executeTakeFirst(); diff --git a/apps/api-server/src/procedures/me/devices.ts b/apps/api-server/src/procedures/me/devices.ts index 8edaf9e..5e4af57 100644 --- a/apps/api-server/src/procedures/me/devices.ts +++ b/apps/api-server/src/procedures/me/devices.ts @@ -108,7 +108,7 @@ export const untrustDevice = os.me.devices.untrust const result = await context.db .updateTable("user_devices") .set({ is_trusted: false }) - .where("id", "=", String(input.deviceId)) + .where("id", "=", input.deviceId.toString()) .where("user_id", "=", context.user.id) .executeTakeFirst(); diff --git a/apps/api-server/src/procedures/me/passkeys.ts b/apps/api-server/src/procedures/me/passkeys.ts index b093580..a1d341a 100644 --- a/apps/api-server/src/procedures/me/passkeys.ts +++ b/apps/api-server/src/procedures/me/passkeys.ts @@ -38,7 +38,7 @@ export const renamePasskey = os.me.passkeys.rename const result = await context.db .updateTable("passkeys") .set({ name }) - .where("id", "=", String(passkeyId)) + .where("id", "=", passkeyId.toString()) .where("user_id", "=", context.user.id) .executeTakeFirst(); @@ -86,7 +86,7 @@ export const deletePasskey = os.me.passkeys.delete const result = await trx .deleteFrom("passkeys") - .where("id", "=", String(passkeyId)) + .where("id", "=", passkeyId.toString()) .where("user_id", "=", context.user.id) .executeTakeFirst(); diff --git a/apps/api-server/src/procedures/me/sessions.ts b/apps/api-server/src/procedures/me/sessions.ts index 4de90e8..27f59c6 100644 --- a/apps/api-server/src/procedures/me/sessions.ts +++ b/apps/api-server/src/procedures/me/sessions.ts @@ -48,7 +48,7 @@ export const revokeSession = os.me.sessions.revoke const { sessionId } = input; // Prevent revoking current session (use logout instead) - if (String(sessionId) === context.session.id) { + if (sessionId.toString() === context.session.id) { throw new ORPCError("BAD_REQUEST", { message: "Cannot revoke current session. Use logout instead.", }); @@ -57,7 +57,7 @@ export const revokeSession = os.me.sessions.revoke const result = await context.db .updateTable("sessions") .set({ revoked_at: new Date() }) - .where("id", "=", String(sessionId)) + .where("id", "=", sessionId.toString()) .where("user_id", "=", context.user.id) .where("revoked_at", "is", null) .executeTakeFirst(); diff --git a/apps/api-server/src/router.ts b/apps/api-server/src/router.ts index 839df1b..2c571ba 100644 --- a/apps/api-server/src/router.ts +++ b/apps/api-server/src/router.ts @@ -139,7 +139,7 @@ const verifyAuthentication = os.auth.webauthn.verifyAuthentication await context.db .updateTable("login_requests") .set({ completed_at: new Date() }) - .where("id", "=", String(context.loginRequestId)) + .where("id", "=", context.loginRequestId.toString()) .execute(); return { success: true }; diff --git a/apps/api-server/src/utils/webauthn.ts b/apps/api-server/src/utils/webauthn.ts index 28a1255..01ab18b 100644 --- a/apps/api-server/src/utils/webauthn.ts +++ b/apps/api-server/src/utils/webauthn.ts @@ -162,7 +162,7 @@ export const verifyRegistration = async ( const challengeRow = await db .selectFrom("webauthn_challenges") .select("options") - .where("id", "=", String(challengeId)) + .where("id", "=", challengeId.toString()) .executeTakeFirst(); if (!challengeRow) { @@ -189,7 +189,7 @@ export const verifyRegistration = async ( // Always delete the challenge await db .deleteFrom("webauthn_challenges") - .where("id", "=", String(challengeId)) + .where("id", "=", challengeId.toString()) .execute(); } @@ -278,7 +278,7 @@ export const verifyAuthentication = async ( const challengeRow = await db .selectFrom("webauthn_challenges") .select("options") - .where("id", "=", String(challengeId)) + .where("id", "=", challengeId.toString()) .executeTakeFirst(); if (!challengeRow) { @@ -321,7 +321,7 @@ export const verifyAuthentication = async ( counter: verification.authenticationInfo.newCounter.toString(), last_used_at: new Date(), }) - .where("id", "=", String(passkey.id)) + .where("id", "=", passkey.id.toString()) .execute(); return true; @@ -329,7 +329,7 @@ export const verifyAuthentication = async ( // Always delete the challenge await db .deleteFrom("webauthn_challenges") - .where("id", "=", String(challengeId)) + .where("id", "=", challengeId.toString()) .execute(); } }; diff --git a/apps/cli/src/routes/admin/complete-login.ts b/apps/cli/src/routes/admin/complete-login.ts index 908d575..19f3833 100644 --- a/apps/cli/src/routes/admin/complete-login.ts +++ b/apps/cli/src/routes/admin/complete-login.ts @@ -2,6 +2,7 @@ import type { LocalContext } from "../../context.js"; import { ORPCError } from "@orpc/client"; import { buildCommand } from "@stricli/core"; import { createApiClient } from "../../utils/api-client.js"; +import { formatError } from "../../utils/format-error.js"; interface CompleteLoginFlags { email: string; @@ -21,12 +22,10 @@ async function completeLogin( console.log(`Completed login request for: ${flags.email}`); } catch (error) { if (error instanceof ORPCError) { - console.error(`Error [${String(error.code)}]:`, error.message); + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- ORPCError.code is typed as any + console.error(`Error [${error.code}]:`, error.message); } else { - console.error( - "Error:", - error instanceof Error ? error.message : String(error), - ); + console.error("Error:", formatError(error)); } this.process.exit(1); } diff --git a/apps/cli/src/routes/auth/login.ts b/apps/cli/src/routes/auth/login.ts index 78f9df1..bbfc295 100644 --- a/apps/cli/src/routes/auth/login.ts +++ b/apps/cli/src/routes/auth/login.ts @@ -2,6 +2,7 @@ import type { LocalContext } from "../../context.js"; import { buildCommand } from "@stricli/core"; import { createApiClient } from "../../utils/api-client.js"; import { readConfig, writeConfig } from "../../utils/config.js"; +import { formatError } from "../../utils/format-error.js"; interface LoginFlags { token: string; @@ -47,10 +48,7 @@ async function login(this: LocalContext, flags: LoginFlags): Promise { console.log(`Logged in as ${authStatus.user.email}`); console.log("Credentials saved to ~/.config/reviq/credentials.json"); } catch (error) { - console.error( - "Login failed:", - error instanceof Error ? error.message : String(error), - ); + console.error("Login failed:", formatError(error)); console.log("\nMake sure your API token is valid."); console.log("You can create a new token at: /account/api-tokens"); this.process.exit(1); diff --git a/apps/cli/src/routes/auth/status.ts b/apps/cli/src/routes/auth/status.ts index 9edc729..2bd0233 100644 --- a/apps/cli/src/routes/auth/status.ts +++ b/apps/cli/src/routes/auth/status.ts @@ -2,6 +2,7 @@ import type { LocalContext } from "../../context.js"; import { buildCommand } from "@stricli/core"; import { createApiClient } from "../../utils/api-client.js"; import { getConfigPath, readConfig } from "../../utils/config.js"; +import { formatError } from "../../utils/format-error.js"; import { TOKEN_PREFIX } from "../../utils/token.js"; function formatDate(date: Date): string { @@ -14,19 +15,19 @@ function formatRelativeTime(date: Date): string { const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffDays < 0) { - return `${String(Math.abs(diffDays))} days ago`; + return `${Math.abs(diffDays).toLocaleString()} days ago`; } if (diffDays === 0) { const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); if (diffHours <= 0) { return "expired"; } - return `in ${String(diffHours)} hours`; + return `in ${diffHours.toLocaleString()} hours`; } if (diffDays === 1) { return "tomorrow"; } - return `in ${String(diffDays)} days`; + return `in ${diffDays.toLocaleString()} days`; } async function status(this: LocalContext): Promise { @@ -96,9 +97,7 @@ async function status(this: LocalContext): Promise { ); } } catch (error) { - console.log( - ` Error: ${error instanceof Error ? error.message : String(error)}`, - ); + console.log(` Error: ${formatError(error)}`); console.log( "\n Unable to connect to API. Local credentials may be invalid.", ); diff --git a/apps/cli/src/routes/bootstrap.ts b/apps/cli/src/routes/bootstrap.ts index a6d4ee6..52a7dcd 100644 --- a/apps/cli/src/routes/bootstrap.ts +++ b/apps/cli/src/routes/bootstrap.ts @@ -2,6 +2,7 @@ import type { LocalContext } from "../context.js"; import { createDb, executeBootstrap } from "@reviq/db"; import { buildCommand } from "@stricli/core"; import { writeConfig } from "../utils/config.js"; +import { formatError } from "../utils/format-error.js"; interface BootstrapFlags { email: string; @@ -47,10 +48,7 @@ async function bootstrap( await db.destroy(); } catch (error) { - console.error( - "Error:", - error instanceof Error ? error.message : String(error), - ); + console.error("Error:", formatError(error)); await db.destroy(); this.process.exit(1); } diff --git a/apps/cli/src/routes/org/add-site.ts b/apps/cli/src/routes/org/add-site.ts index 281e5e7..1c80d90 100644 --- a/apps/cli/src/routes/org/add-site.ts +++ b/apps/cli/src/routes/org/add-site.ts @@ -1,6 +1,7 @@ import type { LocalContext } from "../../context.js"; import { buildCommand } from "@stricli/core"; import { createApiClient } from "../../utils/api-client.js"; +import { formatError } from "../../utils/format-error.js"; interface AddSiteFlags { org: string; @@ -18,10 +19,7 @@ async function addSite(this: LocalContext, flags: AddSiteFlags): Promise { console.log(`Added site ${flags.domain} to org ${flags.org}`); } catch (error) { - console.error( - "Error:", - error instanceof Error ? error.message : String(error), - ); + console.error("Error:", formatError(error)); this.process.exit(1); } } diff --git a/apps/cli/src/routes/org/create.ts b/apps/cli/src/routes/org/create.ts index cc9b217..2bf696a 100644 --- a/apps/cli/src/routes/org/create.ts +++ b/apps/cli/src/routes/org/create.ts @@ -1,6 +1,7 @@ import type { LocalContext } from "../../context.js"; import { buildCommand } from "@stricli/core"; import { createApiClient } from "../../utils/api-client.js"; +import { formatError } from "../../utils/format-error.js"; interface CreateOrgFlags { slug: string; @@ -24,10 +25,7 @@ async function create( console.log(`Created org: ${result.slug}`); console.log(`Owner: ${flags.owner}`); } catch (error) { - console.error( - "Error:", - error instanceof Error ? error.message : String(error), - ); + console.error("Error:", formatError(error)); this.process.exit(1); } } diff --git a/apps/cli/src/routes/org/list.ts b/apps/cli/src/routes/org/list.ts index a1e91c0..cbd695f 100644 --- a/apps/cli/src/routes/org/list.ts +++ b/apps/cli/src/routes/org/list.ts @@ -1,6 +1,7 @@ import type { LocalContext } from "../../context.js"; import { buildCommand } from "@stricli/core"; import { createApiClient } from "../../utils/api-client.js"; +import { formatError } from "../../utils/format-error.js"; async function list(this: LocalContext): Promise { try { @@ -23,12 +24,9 @@ async function list(this: LocalContext): Promise { console.log(); } - console.log(`Total: ${String(orgs.length)} organization(s)`); + console.log(`Total: ${orgs.length.toLocaleString()} organization(s)`); } catch (error) { - console.error( - "Error:", - error instanceof Error ? error.message : String(error), - ); + console.error("Error:", formatError(error)); this.process.exit(1); } } diff --git a/apps/cli/src/routes/user/confirm-email.ts b/apps/cli/src/routes/user/confirm-email.ts index 1863376..a55d5f5 100644 --- a/apps/cli/src/routes/user/confirm-email.ts +++ b/apps/cli/src/routes/user/confirm-email.ts @@ -1,6 +1,7 @@ import type { LocalContext } from "../../context.js"; import { buildCommand } from "@stricli/core"; import { createApiClient } from "../../utils/api-client.js"; +import { formatError } from "../../utils/format-error.js"; interface ConfirmEmailFlags { email: string; @@ -19,10 +20,7 @@ async function confirmEmail( console.log(`Confirmed email for: ${flags.email}`); } catch (error) { - console.error( - "Error:", - error instanceof Error ? error.message : String(error), - ); + console.error("Error:", formatError(error)); this.process.exit(1); } } diff --git a/apps/cli/src/routes/user/create.ts b/apps/cli/src/routes/user/create.ts index 453b4c3..249ea49 100644 --- a/apps/cli/src/routes/user/create.ts +++ b/apps/cli/src/routes/user/create.ts @@ -1,6 +1,7 @@ import type { LocalContext } from "../../context.js"; import { buildCommand } from "@stricli/core"; import { createApiClient } from "../../utils/api-client.js"; +import { formatError } from "../../utils/format-error.js"; type OrgRole = "owner" | "admin" | "member"; @@ -45,10 +46,7 @@ async function create( console.log(`Added to org: ${flags.org} as ${flags.role ?? "member"}`); } } catch (error) { - console.error( - "Error:", - error instanceof Error ? error.message : String(error), - ); + console.error("Error:", formatError(error)); this.process.exit(1); } } diff --git a/apps/cli/src/utils/format-error.ts b/apps/cli/src/utils/format-error.ts new file mode 100644 index 0000000..a033552 --- /dev/null +++ b/apps/cli/src/utils/format-error.ts @@ -0,0 +1,14 @@ +/** + * Format an unknown error value into a string message. + * Handles Error instances, strings, and other types safely. + */ +export function formatError(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + if (typeof error === "string") { + return error; + } + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- intentional unknown coercion + return `${error}`; +} diff --git a/db/schema.sql b/db/schema.sql index 1087f14..386cd62 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1,4 +1,3 @@ -\restrict QhAmrcKtCrf6P0ZFpXVcs0q7Otge0aJb6nxv7mivONqZesSRXpctyFKRRYQlfqj -- Dumped from database version 17.7 -- Dumped by pg_dump version 17.7 @@ -1084,7 +1083,6 @@ ALTER TABLE ONLY public.user_devices -- PostgreSQL database dump complete -- -\unrestrict QhAmrcKtCrf6P0ZFpXVcs0q7Otge0aJb6nxv7mivONqZesSRXpctyFKRRYQlfqj -- diff --git a/package.json b/package.json index b69a4c4..a8d4654 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "build": "turbo build", "build:watch:packages": "turbo watch build --filter=./packages/*", "build:packages": "turbo build --filter=./packages/*", - "lint": "biome check && turbo run lint", - "lint:fix": "biome check --write --unsafe && turbo run lint -- --fix", + "lint": "biome check && ast-grep scan && turbo run lint", + "lint:fix": "biome check --write --unsafe && ast-grep scan --update-all && turbo run lint -- --fix", "typecheck": "turbo typecheck", "clean": "turbo clean", "test": "turbo test", diff --git a/packages/common/src/format-date.ts b/packages/common/src/format-date.ts index 1af514e..d35c96c 100644 --- a/packages/common/src/format-date.ts +++ b/packages/common/src/format-date.ts @@ -95,11 +95,11 @@ export function formatRelativeDate( return "Yesterday"; } if (diffDays < 7) { - return `${String(diffDays)} days ago`; + return `${diffDays.toLocaleString()} days ago`; } if (diffDays < 30) { const weeks = Math.floor(diffDays / 7); - return weeks === 1 ? "1 week ago" : `${String(weeks)} weeks ago`; + return weeks === 1 ? "1 week ago" : `${weeks.toLocaleString()} weeks ago`; } // For older dates, show the actual date diff --git a/packages/server-utils/src/hash-password.ts b/packages/server-utils/src/hash-password.ts index 08e8f13..0bef0a1 100644 --- a/packages/server-utils/src/hash-password.ts +++ b/packages/server-utils/src/hash-password.ts @@ -63,7 +63,7 @@ export const hashPassword = async (password: string): Promise => { const saltB64 = toBase64(salt.buffer); const hashB64 = toBase64(derivedBits); - return `$${ALGORITHM}$${String(ITERATIONS)}$${saltB64}$${hashB64}`; + return `$${ALGORITHM}$${ITERATIONS.toString()}$${saltB64}$${hashB64}`; }; export const verifyPassword = async ( diff --git a/packages/testing/test-helpers/src/index.ts b/packages/testing/test-helpers/src/index.ts index 6685378..2c39768 100644 --- a/packages/testing/test-helpers/src/index.ts +++ b/packages/testing/test-helpers/src/index.ts @@ -16,3 +16,4 @@ export { truncateAllTables, } from "./test-db.js"; export { withTestTransaction } from "./test-transaction.js"; +export { uniqueTestId } from "./test-utils.js"; diff --git a/packages/testing/test-helpers/src/test-db.ts b/packages/testing/test-helpers/src/test-db.ts index 7948bdd..3c89339 100644 --- a/packages/testing/test-helpers/src/test-db.ts +++ b/packages/testing/test-helpers/src/test-db.ts @@ -9,6 +9,7 @@ import { join } from "node:path"; import { createDb } from "@reviq/db"; import { sql } from "kysely"; import pg from "pg"; +import { uniqueTestId } from "./test-utils.js"; const { Client } = pg; @@ -192,7 +193,7 @@ export async function runMigrations(): Promise { if (exitCode !== 0) { const stderr = await new Response(proc.stderr).text(); throw new Error( - `Migration failed with code ${String(exitCode)}: ${stderr}`, + `Migration failed with code ${exitCode.toString()}: ${stderr}`, ); } } @@ -224,7 +225,7 @@ export async function createTestUser( isSuperuser: boolean; }> = {}, ): Promise<{ id: number; email: string }> { - const email = overrides.email ?? `test-${String(Date.now())}@example.com`; + const email = overrides.email ?? `test-${uniqueTestId()}@example.com`; const result = await db .insertInto("users") diff --git a/packages/testing/test-helpers/src/test-utils.ts b/packages/testing/test-helpers/src/test-utils.ts new file mode 100644 index 0000000..8f9ca4b --- /dev/null +++ b/packages/testing/test-helpers/src/test-utils.ts @@ -0,0 +1,15 @@ +/** + * Test utility functions + */ + +/** + * Generates a unique test ID using timestamp and random string. + * Useful for creating unique emails, slugs, tokens, etc. in tests. + * + * @example + * const email = `user-${uniqueTestId()}@example.com` + * const slug = `org-${uniqueTestId()}` + */ +export function uniqueTestId(): string { + return `${Date.now().toString()}-${Math.random().toString(36).slice(2, 8)}`; +}