Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/lib/server/services/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function isUniqueViolation(err: unknown): boolean {
const msg = errorMessage(err);
const cause = errorMessage((err as { cause?: unknown } | null)?.cause);
const haystack = `${msg}\n${cause}`.toLowerCase();
return haystack.includes('duplicate key') || haystack.includes('unique');
}

function errorMessage(value: unknown): string {
if (value instanceof Error) return value.message;
if (typeof value === 'string') return value;
if (value && typeof value === 'object' && 'message' in value) {
const m = (value as { message?: unknown }).message;
if (typeof m === 'string') return m;
}
return '';
}
16 changes: 2 additions & 14 deletions src/routes/(admin)/admin/organizations/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createOrganization,
logAdminAction
} from '$lib/server/services/admin';
import { isUniqueViolation } from '$lib/server/services/errors';
import { isEnabled } from '$lib/feature-flags';

export const load: PageServerLoad = async ({ url }) => {
Expand Down Expand Up @@ -45,7 +46,7 @@ export const actions: Actions = {
);
return { created: true, createdName: values.name };
} catch (err: unknown) {
if (isDuplicateKeyError(err)) {
if (isUniqueViolation(err)) {
return fail(409, {
createErrors: { domain: 'create_duplicate_domain' } as Record<string, string>,
createValues: values
Expand Down Expand Up @@ -94,16 +95,3 @@ function validateCreateInput(input: CreateInput): Record<string, string> {
else if (!EMAIL_RE.test(input.signingEmail)) errors.signingEmail = 'create_invalid_email';
return errors;
}

function errMessage(value: unknown): string {
if (value instanceof Error) return value.message;
if (typeof value === 'string') return value;
return '';
}

function isDuplicateKeyError(err: unknown): boolean {
const top = errMessage(err);
const cause = errMessage((err as { cause?: unknown })?.cause);
const haystack = `${top}\n${cause}`;
return haystack.includes('duplicate key') || haystack.includes('unique');
}
5 changes: 2 additions & 3 deletions src/routes/(admin)/admin/organizations/[id]/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
logAdminAction
} from '$lib/server/services/admin';
import { setImpersonation } from '$lib/server/auth/session';
import { isUniqueViolation } from '$lib/server/services/errors';
import { isEnabled } from '$lib/feature-flags';

export const load: PageServerLoad = async ({ params }) => {
Expand Down Expand Up @@ -179,9 +180,7 @@ export const actions: Actions = {
);
return { userAdded: true, addedUserName: fullName };
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : '';
const cause = (err as { cause?: { message?: string } })?.cause?.message ?? '';
if (`${msg}\n${cause}`.match(/duplicate key|unique/i)) {
if (isUniqueViolation(err)) {
return fail(409, {
addUserErrors: { email: 'addUser_duplicate_email' } as Record<string, string>,
addUserValues: { email, fullName, phone }
Expand Down
10 changes: 2 additions & 8 deletions src/routes/(marketing)/register/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { db } from '$lib/server/db';
import { organizations, users } from '$lib/server/db/schema';
import { eq } from 'drizzle-orm';
import { isEnabled } from '$lib/feature-flags';
import { isUniqueViolation } from '$lib/server/services/errors';
import { error } from '@sveltejs/kit';

export function load() {
Expand Down Expand Up @@ -74,14 +75,7 @@ export const actions: Actions = {
.set({ contactUserId: user.id })
.where(eq(organizations.id, org.id));
} catch (err: unknown) {
const errStr = String(err instanceof Error ? (err.stack ?? err.message) : err);
const cause = (err as { cause?: { message?: string } } | null)?.cause;
const causeStr = cause ? String(cause.message ?? cause) : '';
if (
errStr.includes('unique') ||
errStr.includes('duplicate key') ||
causeStr.includes('duplicate key')
) {
if (isUniqueViolation(err)) {
return fail(409, {
errors: { domain: 'This domain is already registered' } as Record<string, string>,
values: { name }
Expand Down
3 changes: 2 additions & 1 deletion src/routes/(portal)/portal/members/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Actions, PageServerLoad } from './$types';
import { error, fail } from '@sveltejs/kit';
import { isEnabled } from '$lib/feature-flags';
import { listOrgUsers, addUser, removeUser, setContactPerson } from '$lib/server/services/users';
import { isUniqueViolation } from '$lib/server/services/errors';

export const load: PageServerLoad = async ({ parent }) => {
if (!isEnabled('portalMembers')) error(404, 'Not found');
Expand Down Expand Up @@ -31,7 +32,7 @@ export const actions: Actions = {
try {
await addUser(orgId, email, fullName, phone);
} catch (err: unknown) {
if (err instanceof Error && err.message.includes('unique')) {
if (isUniqueViolation(err)) {
return fail(409, { error: 'A user with this email already exists' });
}
throw err;
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, it, expect } from 'vitest';
import { isUniqueViolation } from '$lib/server/services/errors';

describe('isUniqueViolation', () => {
it('matches postgres "duplicate key value violates unique constraint" wording on message', () => {
const err = new Error('duplicate key value violates unique constraint "users_email_key"');
expect(isUniqueViolation(err)).toBe(true);
});

it('matches the bare "unique constraint" wording on message', () => {
const err = new Error('unique constraint violation on column email');
expect(isUniqueViolation(err)).toBe(true);
});

it('matches "duplicate key" wording wrapped in a cause', () => {
const err = new Error('insert failed') as Error & { cause: Error };
err.cause = new Error(
'duplicate key value violates unique constraint "organizations_domain_key"'
);
expect(isUniqueViolation(err)).toBe(true);
});

it('matches "unique" wording wrapped in a cause', () => {
const err = new Error('insert failed') as Error & { cause: Error };
err.cause = new Error('Unique constraint failed');
expect(isUniqueViolation(err)).toBe(true);
});

it('is case-insensitive', () => {
const err = new Error('DUPLICATE KEY value violates UNIQUE constraint');
expect(isUniqueViolation(err)).toBe(true);
});

it('reads message off a plain object with a message field', () => {
const err = { message: 'duplicate key value violates unique constraint' };
expect(isUniqueViolation(err)).toBe(true);
});

it('returns false for unrelated errors', () => {
expect(isUniqueViolation(new Error('connection refused'))).toBe(false);
});

it('returns false for non-error values', () => {
expect(isUniqueViolation(null)).toBe(false);
expect(isUniqueViolation(undefined)).toBe(false);
expect(isUniqueViolation(42)).toBe(false);
expect(isUniqueViolation({})).toBe(false);
});
});
Loading