Skip to content

Commit 7d0f0b3

Browse files
suzuhe481shashilo
andauthored
feat #655: implements an error handling system with custom errors (#657)
* chore: adds custom error classes * chore: adds error handling by throwing, catching, and logging errors * fix: fixes typo in JSDocs * fix: removes unused import * chore: throws error for a BackendError * fix: removes unused import * chore: throws errors for BackendError and SupabaseErrors * chore: throws errors for OpenAiError * feat: function to identify and log errors * chore: refactoring catch blocks to implement logError() * test: fixes test to account for SupabaseError class * test: fixes test to account for OpenAiError class and adjusts mock resolved value for bad open ai response * fix: consolidates mockSupabase and adjusts the update status test * fix: adds type string to SupabaseError's statusCode to allow postgrest error code * fix: passes postgrest error code through SupabaseError class * chore: added type to SupabaseError JSDocs * fix: removes status from returned SupabaseError response * fix: captures the stack trace instead of overwriting it to remove unnecessary lines in the log * fix: improves readability by logging errors as a single object and adjusts log for unknown errors --------- Co-authored-by: Shashi Lo <362527+shashilo@users.noreply.github.com>
1 parent 33f10ab commit 7d0f0b3

File tree

24 files changed

+694
-312
lines changed

24 files changed

+694
-312
lines changed

app/api/cron/functions/fetchGiftExchanges/fetchGiftExchanges.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createClient } from '@/lib/supabase/server';
22
import { fetchGiftExchanges } from './fetchGiftExchanges';
33
import { SupabaseClient } from '@supabase/supabase-js';
4+
import { SupabaseError } from '@/lib/errors/CustomErrors';
45

56
jest.mock('@/lib/supabase/server');
67

@@ -32,11 +33,11 @@ describe('fetchGiftExchanges', () => {
3233
it('throws an error when fetch fails', async () => {
3334
mockSelect.mockResolvedValueOnce({
3435
data: null,
35-
error: new Error('Something went wrong'),
36+
error: new SupabaseError('Something went wrong', 500),
3637
});
3738

3839
await expect(
3940
fetchGiftExchanges({ supabase: mockSupabase }),
40-
).rejects.toThrow('Something went wrong');
41+
).rejects.toThrow(SupabaseError);
4142
});
4243
});

app/api/cron/functions/fetchGiftExchanges/fetchGiftExchanges.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import { GiftExchange } from '@/app/types/giftExchange';
55
import { SupabaseClient } from '@supabase/supabase-js';
6+
import { SupabaseError } from '@/lib/errors/CustomErrors';
67

78
/**
89
* Fetches all gift exchanges in the database.
@@ -17,11 +18,15 @@ export const fetchGiftExchanges = async ({
1718
const { data, error } = await supabase.from('gift_exchanges').select('*');
1819

1920
if (error) {
20-
throw new Error(error.message);
21+
throw new SupabaseError(
22+
'Failed to fetch gift exchanges',
23+
error.code,
24+
error,
25+
);
2126
}
2227

2328
if (data.length === 0) {
24-
throw new Error('No gift exchanges found.');
29+
throw new SupabaseError('No gift exchanges found', 500, error);
2530
}
2631

2732
return data;

app/api/cron/functions/processGiftExchanges/processGiftExchanges.test.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,12 @@ import { SupabaseClient } from '@supabase/supabase-js';
55
jest.mock('@/lib/drawGiftExchange', () => ({
66
drawGiftExchange: jest.fn(),
77
}));
8-
9-
const mockEq = jest.fn();
10-
const mockUpdate = jest.fn(() => ({
11-
eq: mockEq,
12-
}));
13-
148
const mockSupabase = {
15-
from: jest.fn(() => ({
16-
update: mockUpdate,
17-
})),
9+
from: jest.fn().mockReturnValue({
10+
update: jest.fn().mockReturnValue({
11+
eq: jest.fn().mockResolvedValue({}),
12+
}),
13+
}),
1814
} as unknown as SupabaseClient;
1915

2016
describe('processGiftExchanges', () => {
@@ -67,8 +63,12 @@ describe('processGiftExchanges', () => {
6763
});
6864

6965
expect(mockSupabase.from).toHaveBeenCalledWith('gift_exchanges');
70-
expect(mockUpdate).toHaveBeenCalledWith({ status: 'completed' });
71-
expect(mockEq).toHaveBeenCalledWith('id', '456');
66+
expect(mockSupabase.from('gift_exchanges').update).toHaveBeenCalledWith({
67+
status: 'completed',
68+
});
69+
expect(
70+
mockSupabase.from('gift_exchanges').update({ status: 'completed' }).eq,
71+
).toHaveBeenCalledWith('id', '456');
7272
expect(drawnCount).toBe(0);
7373
expect(completedCount).toBe(1);
7474
});

app/api/cron/functions/processGiftExchanges/processGiftExchanges.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
IGiftProcess,
77
IProcessGiftExchangesResult,
88
} from '@/app/types/giftExchange';
9+
import { SupabaseError } from '@/lib/errors/CustomErrors';
910

1011
/**
1112
* Checks the dates to know how to update gift exchange status.
@@ -28,18 +29,30 @@ export const processGiftExchanges = async ({
2829
let drawnCount = 0;
2930
let completedCount = 0;
3031

31-
if (drawDate === currentDate && exchange.status === 'pending') {
32-
await drawGiftExchange(supabase, exchange.id);
33-
drawnCount += 1;
34-
}
32+
try {
33+
if (drawDate === currentDate && exchange.status === 'pending') {
34+
await drawGiftExchange(supabase, exchange.id);
35+
drawnCount += 1;
36+
}
3537

36-
if (currentDate > exchangeDate && exchange.status === 'active') {
37-
await supabase
38-
.from('gift_exchanges')
39-
.update({ status: 'completed' })
40-
.eq('id', exchange.id);
41-
completedCount += 1;
42-
}
38+
if (currentDate > exchangeDate && exchange.status === 'active') {
39+
const { error } = await supabase
40+
.from('gift_exchanges')
41+
.update({ status: 'completed' })
42+
.eq('id', exchange.id);
43+
completedCount += 1;
4344

44-
return { drawnCount, completedCount };
45+
if (error) {
46+
throw new SupabaseError(
47+
'Failed to process gift exchange',
48+
error.code,
49+
error,
50+
);
51+
}
52+
}
53+
54+
return { drawnCount, completedCount };
55+
} catch (error) {
56+
throw error;
57+
}
4558
};

app/api/cron/route.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,20 @@ import { createClient } from '@/lib/supabase/server';
66
import { fetchGiftExchanges } from './functions/fetchGiftExchanges/fetchGiftExchanges';
77
import { NextResponse } from 'next/server';
88
import { processGiftExchanges } from './functions/processGiftExchanges/processGiftExchanges';
9+
import { BackendError } from '@/lib/errors/CustomErrors';
10+
import logError from '@/lib/errors/logError';
911

1012
/**
1113
* API function that gets the cron job header to execute once daily.
1214
* @param {Request} request - API request.
1315
* @returns {Promise<Response>} The rendered weekly picks page.
1416
*/
1517
export async function GET(request: Request): Promise<Response> {
16-
if (!checkCronAuthorization(request)) {
17-
return NextResponse.json({ status: false });
18-
}
19-
2018
try {
19+
if (!checkCronAuthorization(request)) {
20+
throw new BackendError('Invalid authoritzation header', 500);
21+
}
22+
2123
const supabase = await createClient();
2224

2325
const giftExchanges = await fetchGiftExchanges({ supabase });
@@ -48,6 +50,6 @@ export async function GET(request: Request): Promise<Response> {
4850

4951
return NextResponse.json({ success: true, drawnMessage, completedMessage });
5052
} catch (error) {
51-
return NextResponse.json({ error });
53+
return logError(error);
5254
}
5355
}

app/api/getUserAvatar/route.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,42 @@
33

44
import { NextResponse } from 'next/server';
55
import { createClient } from '../../../lib/supabase/server';
6+
import { SupabaseError } from '@/lib/errors/CustomErrors';
7+
import { BackendError } from '@/lib/errors/CustomErrors';
8+
import logError from '@/lib/errors/logError';
69

710
/**
811
* Get user avatar URL
912
* @returns {Promise<NextResponse>} Promise that resolved to a Response object
1013
*/
1114
export async function GET(): Promise<NextResponse> {
12-
const supabase = await createClient();
13-
const {
14-
data: { user },
15-
error: userError,
16-
} = await supabase.auth.getUser();
17-
18-
if (userError) {
19-
return NextResponse.json({ error: 'Unable to retrieve user avatar URL' });
20-
}
15+
try {
16+
const supabase = await createClient();
17+
18+
const {
19+
data: { user },
20+
error: userError,
21+
} = await supabase.auth.getUser();
22+
23+
if (userError) {
24+
const statusCode = userError.status || 500;
25+
throw new SupabaseError('Failed to fetch user', statusCode, userError);
26+
}
27+
28+
if (!user) {
29+
throw new SupabaseError('User is not authenticated or exists', 500);
30+
}
2131

22-
const avatarUrl = user?.user_metadata.avatar_url;
32+
if (!user.user_metadata.avatar_url) {
33+
console.error('User has no avatar');
2334

24-
return NextResponse.json(avatarUrl);
35+
throw new BackendError('User has no avatar', 500);
36+
}
37+
38+
const avatarUrl = user.user_metadata.avatar_url;
39+
40+
return NextResponse.json({ avatarUrl });
41+
} catch (error) {
42+
return logError(error);
43+
}
2544
}
Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { createClient } from '@/lib/supabase/server';
22
import { NextResponse } from 'next/server';
33
import { drawGiftExchange } from '@/lib/drawGiftExchange';
4+
import { SupabaseError } from '@/lib/errors/CustomErrors';
5+
import logError from '@/lib/errors/logError';
46

57
/**
68
* API Route for drawing gift exchange names
@@ -16,30 +18,24 @@ export async function POST(
1618

1719
try {
1820
const supabase = await createClient();
21+
1922
const {
2023
data: { user },
24+
error: userError,
2125
} = await supabase.auth.getUser();
2226

27+
if (userError) {
28+
const statusCode = userError.status || 500;
29+
throw new SupabaseError('Failed to fetch user', statusCode, userError);
30+
}
31+
2332
if (!user) {
24-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
33+
throw new SupabaseError('User is not authenticated or exists', 500);
2534
}
2635

2736
await drawGiftExchange(supabase, id);
2837
return NextResponse.json({ success: true });
2938
} catch (error) {
30-
console.error(error);
31-
const message =
32-
error instanceof Error ? error.message : 'Internal server error';
33-
let status = 500; // default status
34-
if (message.includes('not found')) {
35-
status = 404;
36-
} else if (
37-
message.includes('already been drawn') ||
38-
message.includes('3 members')
39-
) {
40-
status = 400;
41-
}
42-
43-
return NextResponse.json({ error: message }, { status });
39+
return logError(error);
4440
}
4541
}

app/api/gift-exchanges/[id]/giftSuggestions/route.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { createClient } from '@/lib/supabase/server';
22
import { NextResponse } from 'next/server';
3+
import { SupabaseError } from '@/lib/errors/CustomErrors';
4+
import logError from '@/lib/errors/logError';
35

46
export async function GET(
57
req: Request,
@@ -10,12 +12,19 @@ export async function GET(
1012

1113
try {
1214
const supabase = await createClient();
15+
1316
const {
1417
data: { user },
18+
error: userError,
1519
} = await supabase.auth.getUser();
1620

21+
if (userError) {
22+
const statusCode = userError.status || 500;
23+
throw new SupabaseError('Failed to fetch user', statusCode, userError);
24+
}
25+
1726
if (!user) {
18-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
27+
throw new SupabaseError('User is not authenticated or exists', 500);
1928
}
2029

2130
// Get match with full profile info
@@ -45,9 +54,10 @@ export async function GET(
4554
.single();
4655

4756
if (matchError) {
48-
return NextResponse.json(
49-
{ error: 'Failed to fetch match' },
50-
{ status: 500 },
57+
throw new SupabaseError(
58+
'Failed to fetch match',
59+
matchError.code,
60+
matchError,
5161
);
5262
}
5363

@@ -59,9 +69,10 @@ export async function GET(
5969
.eq('giver_id', user.id);
6070

6171
if (suggestionsError) {
62-
return NextResponse.json(
63-
{ error: 'Failed to fetch suggestions' },
64-
{ status: 500 },
72+
throw new SupabaseError(
73+
'Failed to fetch suggestions',
74+
suggestionsError.code,
75+
suggestionsError,
6576
);
6677
}
6778

@@ -74,10 +85,6 @@ export async function GET(
7485
})),
7586
});
7687
} catch (error) {
77-
console.log(error);
78-
return NextResponse.json(
79-
{ error: 'Internal server error' },
80-
{ status: 500 },
81-
);
88+
return logError(error);
8289
}
8390
}

0 commit comments

Comments
 (0)