Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a6ecff3
chore: adds custom error classes
suzuhe481 Oct 27, 2025
693e08e
chore: adds error handling by throwing, catching, and logging errors
suzuhe481 Oct 27, 2025
9cc8a9f
fix: fixes typo in JSDocs
suzuhe481 Oct 27, 2025
3ce8694
fix: removes unused import
suzuhe481 Oct 27, 2025
23271e8
chore: throws error for a BackendError
suzuhe481 Oct 27, 2025
59548f7
fix: removes unused import
suzuhe481 Oct 27, 2025
9ad4a2f
chore: throws errors for BackendError and SupabaseErrors
suzuhe481 Oct 27, 2025
e94ea47
chore: throws errors for OpenAiError
suzuhe481 Oct 27, 2025
be65dcb
feat: function to identify and log errors
suzuhe481 Oct 27, 2025
dcd254c
chore: refactoring catch blocks to implement logError()
suzuhe481 Oct 27, 2025
deede94
test: fixes test to account for SupabaseError class
suzuhe481 Oct 28, 2025
d550cda
test: fixes test to account for OpenAiError class and adjusts mock re…
suzuhe481 Oct 28, 2025
26b9738
fix: consolidates mockSupabase and adjusts the update status test
suzuhe481 Oct 28, 2025
60ceebe
fix: adds type string to SupabaseError's statusCode to allow postgres…
suzuhe481 Oct 30, 2025
0952c4e
fix: passes postgrest error code through SupabaseError class
suzuhe481 Oct 30, 2025
f04b042
chore: added type to SupabaseError JSDocs
suzuhe481 Oct 30, 2025
2b661f1
fix: removes status from returned SupabaseError response
suzuhe481 Oct 30, 2025
51089fd
Merge branch 'develop' of https://github.com/LetsGetTechnical/elecret…
suzuhe481 Nov 3, 2025
f2733e8
Merge branch 'develop' of https://github.com/LetsGetTechnical/elecret…
suzuhe481 Nov 5, 2025
583946e
fix: captures the stack trace instead of overwriting it to remove unn…
suzuhe481 Nov 10, 2025
6ea7a41
fix: improves readability by logging errors as a single object and ad…
suzuhe481 Nov 10, 2025
efcaec5
Merge branch 'develop' into hector/655-update-nextjs-error-responses
suzuhe481 Nov 13, 2025
f0364fd
Merge branch 'develop' into hector/655-update-nextjs-error-responses
shashilo Nov 13, 2025
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server';
import { fetchGiftExchanges } from './fetchGiftExchanges';
import { SupabaseClient } from '@supabase/supabase-js';
import { SupabaseError } from '@/lib/errors/CustomErrors';

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

Expand Down Expand Up @@ -32,11 +33,11 @@ describe('fetchGiftExchanges', () => {
it('throws an error when fetch fails', async () => {
mockSelect.mockResolvedValueOnce({
data: null,
error: new Error('Something went wrong'),
error: new SupabaseError('Something went wrong', 500),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this forcing a 500 error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes because when declaring a new custom error, the statusCode is required as a parameter. Since this only tests that an error is thrown, I just added any code.
image

});

await expect(
fetchGiftExchanges({ supabase: mockSupabase }),
).rejects.toThrow('Something went wrong');
).rejects.toThrow(SupabaseError);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

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

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

if (error) {
throw new Error(error.message);
throw new SupabaseError(
'Failed to fetch gift exchanges',
error.code,
error,
);
}

if (data.length === 0) {
throw new Error('No gift exchanges found.');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Are we forcing a status error instead of using the one given from the error object?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because the error object that's returned is a PostgrestError but it doesn't actually have a status code. They use their own code which has a format like PGRST000. Should I use their actual code instead and map it to an HTTP code with their chart?

https://docs.postgrest.org/en/v12/references/errors.html#errors-from-postgrest

throw new SupabaseError('No gift exchanges found', 500, error);
}

return data;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,12 @@ import { SupabaseClient } from '@supabase/supabase-js';
jest.mock('@/lib/drawGiftExchange', () => ({
drawGiftExchange: jest.fn(),
}));

const mockEq = jest.fn();
const mockUpdate = jest.fn(() => ({
eq: mockEq,
}));

const mockSupabase = {
from: jest.fn(() => ({
update: mockUpdate,
})),
from: jest.fn().mockReturnValue({
update: jest.fn().mockReturnValue({
eq: jest.fn().mockResolvedValue({}),
}),
}),
} as unknown as SupabaseClient;

describe('processGiftExchanges', () => {
Expand Down Expand Up @@ -67,8 +63,12 @@ describe('processGiftExchanges', () => {
});

expect(mockSupabase.from).toHaveBeenCalledWith('gift_exchanges');
expect(mockUpdate).toHaveBeenCalledWith({ status: 'completed' });
expect(mockEq).toHaveBeenCalledWith('id', '456');
expect(mockSupabase.from('gift_exchanges').update).toHaveBeenCalledWith({
status: 'completed',
});
expect(
mockSupabase.from('gift_exchanges').update({ status: 'completed' }).eq,
).toHaveBeenCalledWith('id', '456');
expect(drawnCount).toBe(0);
expect(completedCount).toBe(1);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
IGiftProcess,
IProcessGiftExchangesResult,
} from '@/app/types/giftExchange';
import { SupabaseError } from '@/lib/errors/CustomErrors';

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

if (drawDate === currentDate && exchange.status === 'pending') {
await drawGiftExchange(supabase, exchange.id);
drawnCount += 1;
}
try {
if (drawDate === currentDate && exchange.status === 'pending') {
await drawGiftExchange(supabase, exchange.id);
drawnCount += 1;
}

if (currentDate > exchangeDate && exchange.status === 'active') {
await supabase
.from('gift_exchanges')
.update({ status: 'completed' })
.eq('id', exchange.id);
completedCount += 1;
}
if (currentDate > exchangeDate && exchange.status === 'active') {
const { error } = await supabase
.from('gift_exchanges')
.update({ status: 'completed' })
.eq('id', exchange.id);
completedCount += 1;

return { drawnCount, completedCount };
if (error) {
throw new SupabaseError(
'Failed to process gift exchange',
error.code,
error,
);
}
}

return { drawnCount, completedCount };
} catch (error) {
throw error;
}
};
12 changes: 7 additions & 5 deletions app/api/cron/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ import { createClient } from '@/lib/supabase/server';
import { fetchGiftExchanges } from './functions/fetchGiftExchanges/fetchGiftExchanges';
import { NextResponse } from 'next/server';
import { processGiftExchanges } from './functions/processGiftExchanges/processGiftExchanges';
import { BackendError } from '@/lib/errors/CustomErrors';
import logError from '@/lib/errors/logError';

/**
* API function that gets the cron job header to execute once daily.
* @param {Request} request - API request.
* @returns {Promise<Response>} The rendered weekly picks page.
*/
export async function GET(request: Request): Promise<Response> {
if (!checkCronAuthorization(request)) {
return NextResponse.json({ status: false });
}

try {
if (!checkCronAuthorization(request)) {
throw new BackendError('Invalid authoritzation header', 500);
}

const supabase = await createClient();

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

return NextResponse.json({ success: true, drawnMessage, completedMessage });
} catch (error) {
return NextResponse.json({ error });
return logError(error);
}
}
41 changes: 30 additions & 11 deletions app/api/getUserAvatar/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,42 @@

import { NextResponse } from 'next/server';
import { createClient } from '../../../lib/supabase/server';
import { SupabaseError } from '@/lib/errors/CustomErrors';
import { BackendError } from '@/lib/errors/CustomErrors';
import logError from '@/lib/errors/logError';

/**
* Get user avatar URL
* @returns {Promise<NextResponse>} Promise that resolved to a Response object
*/
export async function GET(): Promise<NextResponse> {
const supabase = await createClient();
const {
data: { user },
error: userError,
} = await supabase.auth.getUser();

if (userError) {
return NextResponse.json({ error: 'Unable to retrieve user avatar URL' });
}
try {
const supabase = await createClient();

const {
data: { user },
error: userError,
} = await supabase.auth.getUser();

if (userError) {
const statusCode = userError.status || 500;
throw new SupabaseError('Failed to fetch user', statusCode, userError);
}

if (!user) {
throw new SupabaseError('User is not authenticated or exists', 500);
}

const avatarUrl = user?.user_metadata.avatar_url;
if (!user.user_metadata.avatar_url) {
console.error('User has no avatar');

return NextResponse.json(avatarUrl);
throw new BackendError('User has no avatar', 500);
}

const avatarUrl = user.user_metadata.avatar_url;

return NextResponse.json({ avatarUrl });
} catch (error) {
return logError(error);
}
}
26 changes: 11 additions & 15 deletions app/api/gift-exchanges/[id]/draw/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createClient } from '@/lib/supabase/server';
import { NextResponse } from 'next/server';
import { drawGiftExchange } from '@/lib/drawGiftExchange';
import { SupabaseError } from '@/lib/errors/CustomErrors';
import logError from '@/lib/errors/logError';

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

try {
const supabase = await createClient();

const {
data: { user },
error: userError,
} = await supabase.auth.getUser();

if (userError) {
const statusCode = userError.status || 500;
throw new SupabaseError('Failed to fetch user', statusCode, userError);
}

if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
throw new SupabaseError('User is not authenticated or exists', 500);
}

await drawGiftExchange(supabase, id);
return NextResponse.json({ success: true });
} catch (error) {
console.error(error);
const message =
error instanceof Error ? error.message : 'Internal server error';
let status = 500; // default status
if (message.includes('not found')) {
status = 404;
} else if (
message.includes('already been drawn') ||
message.includes('3 members')
) {
status = 400;
}

return NextResponse.json({ error: message }, { status });
return logError(error);
}
}
31 changes: 19 additions & 12 deletions app/api/gift-exchanges/[id]/giftSuggestions/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { createClient } from '@/lib/supabase/server';
import { NextResponse } from 'next/server';
import { SupabaseError } from '@/lib/errors/CustomErrors';
import logError from '@/lib/errors/logError';

export async function GET(
req: Request,
Expand All @@ -10,12 +12,19 @@ export async function GET(

try {
const supabase = await createClient();

const {
data: { user },
error: userError,
} = await supabase.auth.getUser();

if (userError) {
const statusCode = userError.status || 500;
throw new SupabaseError('Failed to fetch user', statusCode, userError);
}

if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
throw new SupabaseError('User is not authenticated or exists', 500);
}

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

if (matchError) {
return NextResponse.json(
{ error: 'Failed to fetch match' },
{ status: 500 },
throw new SupabaseError(
'Failed to fetch match',
matchError.code,
matchError,
);
}

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

if (suggestionsError) {
return NextResponse.json(
{ error: 'Failed to fetch suggestions' },
{ status: 500 },
throw new SupabaseError(
'Failed to fetch suggestions',
suggestionsError.code,
suggestionsError,
);
}

Expand All @@ -74,10 +85,6 @@ export async function GET(
})),
});
} catch (error) {
console.log(error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 },
);
return logError(error);
}
}
Loading