unit test fixes + error refactor
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 10m52s

This commit is contained in:
2025-12-11 18:23:59 -08:00
parent 5f1901b93d
commit 0bc65574c2
22 changed files with 112 additions and 676 deletions

View File

@@ -1,7 +1,7 @@
// src/services/db/admin.db.ts
import type { Pool, PoolClient } from 'pg';
import { getPool } from './connection.db';
import { ForeignKeyConstraintError } from './errors.db';
import { ForeignKeyConstraintError, NotFoundError } from './errors.db';
import { logger } from '../logger.server';
import { SuggestedCorrection, MostFrequentSaleItem, Recipe, RecipeComment, UnmatchedFlyerItem, ActivityLogItem, Receipt, User, AdminUserView } from '../../types';
@@ -103,7 +103,7 @@ export class AdminRepository {
[newSuggestedValue, correctionId]
);
if (res.rowCount === 0) {
throw new Error(`Correction with ID ${correctionId} not found or is not in 'pending' state.`);
throw new NotFoundError(`Correction with ID ${correctionId} not found or is not in 'pending' state.`);
}
return res.rows[0];
} catch (error) {
@@ -250,7 +250,7 @@ export class AdminRepository {
[status, commentId]
);
if (res.rowCount === 0) {
throw new Error(`Recipe comment with ID ${commentId} not found.`);
throw new NotFoundError(`Recipe comment with ID ${commentId} not found.`);
}
return res.rows[0];
} catch (error) {
@@ -303,7 +303,7 @@ export class AdminRepository {
[status, recipeId]
);
if (res.rowCount === 0) {
throw new Error(`Recipe with ID ${recipeId} not found.`);
throw new NotFoundError(`Recipe with ID ${recipeId} not found.`);
}
return res.rows[0];
} catch (error) {
@@ -330,7 +330,7 @@ export class AdminRepository {
);
if (unmatchedRes.rowCount === 0) {
throw new Error(`Unmatched flyer item with ID ${unmatchedFlyerItemId} not found.`);
throw new NotFoundError(`Unmatched flyer item with ID ${unmatchedFlyerItemId} not found.`);
}
const { flyer_item_id } = unmatchedRes.rows[0];
@@ -510,7 +510,7 @@ export class AdminRepository {
[role, userId]
);
if (res.rowCount === 0) {
throw new Error(`User with ID ${userId} not found.`);
throw new NotFoundError(`User with ID ${userId} not found.`);
}
return res.rows[0];
} catch (error) {

View File

@@ -1,7 +1,7 @@
// src/services/db/budget.db.ts
import type { Pool, PoolClient } from 'pg';
import { getPool } from './connection.db';
import { ForeignKeyConstraintError } from './errors.db';
import { ForeignKeyConstraintError, NotFoundError } from './errors.db';
import { logger } from '../logger.server';
import { Budget, SpendingByCategory } from '../../types';
@@ -83,12 +83,9 @@ export class BudgetRepository {
WHERE budget_id = $5 AND user_id = $6 RETURNING *`,
[name, amount_cents, period, start_date, budgetId, userId],
);
if (res.rowCount === 0) throw new Error('Budget not found or user does not have permission to update.');
if (res.rowCount === 0) throw new NotFoundError('Budget not found or user does not have permission to update.');
return res.rows[0];
} catch (error) {
if (error instanceof Error && error.message.includes('Budget not found')) {
throw error; // Re-throw the specific error to the caller
}
logger.error('Database error in updateBudget:', { error, budgetId, userId });
throw new Error('Failed to update budget.');
}
@@ -103,12 +100,9 @@ export class BudgetRepository {
try {
const result = await this.db.query('DELETE FROM public.budgets WHERE budget_id = $1 AND user_id = $2', [budgetId, userId]);
if (result.rowCount === 0) {
throw new Error('Budget not found or user does not have permission to delete.');
throw new NotFoundError('Budget not found or user does not have permission to delete.');
}
} catch (error) {
if (error instanceof Error && error.message.includes('Budget not found')) {
throw error; // Re-throw the specific error to the caller
}
logger.error('Database error in deleteBudget:', { error, budgetId, userId });
throw new Error('Failed to delete budget.');
}

View File

@@ -33,4 +33,22 @@ export class ForeignKeyConstraintError extends DatabaseError {
constructor(message = 'The referenced record does not exist.') {
super(message, 400); // 400 Bad Request
}
}
}
/**
* Thrown when a specific record is not found in the database.
*/
export class NotFoundError extends DatabaseError {
constructor(message = 'The requested resource was not found.') {
super(message, 404); // 404 Not Found
}
}
/**
* Thrown when request validation fails (e.g., missing body fields or invalid params).
*/
export class ValidationError extends DatabaseError {
constructor(message = 'The request data is invalid.') {
super(message, 400); // 400 Bad Request
}
}

View File

@@ -2,7 +2,7 @@
import type { Pool, PoolClient } from 'pg';
import { getPool } from './connection.db';
import { logger } from '../logger.server';
import { UniqueConstraintError, ForeignKeyConstraintError } from './errors.db';
import { UniqueConstraintError, ForeignKeyConstraintError, NotFoundError } from './errors.db';
import type { Flyer, FlyerItem, FlyerInsert, FlyerItemInsert, Brand, FlyerDbInsert } from '../../types';
export class FlyerRepository {
@@ -154,9 +154,12 @@ export class FlyerRepository {
* @param flyerId The ID of the flyer to retrieve.
* @returns A promise that resolves to the Flyer object or undefined if not found.
*/
async getFlyerById(flyerId: number): Promise<Flyer | undefined> {
async getFlyerById(flyerId: number): Promise<Flyer> {
try {
const res = await this.db.query<Flyer>('SELECT * FROM public.flyers WHERE flyer_id = $1', [flyerId]);
if (res.rowCount === 0) {
throw new NotFoundError(`Flyer with ID ${flyerId} not found.`);
}
return res.rows[0];
} catch (error) {
logger.error('Database error in getFlyerById:', { error, flyerId });

View File

@@ -1,7 +1,7 @@
// src/services/db/recipe.db.ts
import type { Pool, PoolClient } from 'pg';
import { getPool } from './connection.db';
import { ForeignKeyConstraintError } from './errors.db';
import { ForeignKeyConstraintError, NotFoundError } from './errors.db';
import { logger } from '../logger.server';
import { Recipe, FavoriteRecipe, RecipeComment } from '../../types';
@@ -87,9 +87,6 @@ export class RecipeRepository {
);
return res.rows[0];
} catch (error) {
if (error instanceof Error && 'code' in error && error.code === '23503') {
throw new ForeignKeyConstraintError('The specified user or recipe does not exist.');
}
logger.error('Database error in addFavoriteRecipe:', { error, userId, recipeId });
throw new Error('Failed to add favorite recipe.');
}
@@ -104,10 +101,9 @@ export class RecipeRepository {
try {
const res = await this.db.query('DELETE FROM public.favorite_recipes WHERE user_id = $1 AND recipe_id = $2', [userId, recipeId]);
if (res.rowCount === 0) {
throw new Error('Favorite recipe not found for this user.');
throw new NotFoundError('Favorite recipe not found for this user.');
}
} catch (error) {
if (error instanceof Error && error.message.startsWith('Favorite recipe not found')) throw error;
logger.error('Database error in removeFavoriteRecipe:', { error, userId, recipeId });
throw new Error('Failed to remove favorite recipe.');
}
@@ -131,11 +127,10 @@ export class RecipeRepository {
const res = await this.db.query(query, params);
if (res.rowCount === 0) {
throw new Error('Recipe not found or user does not have permission to delete.');
throw new NotFoundError('Recipe not found or user does not have permission to delete.');
}
} catch (error) {
if (error instanceof Error && error.message.startsWith('Recipe not found')) throw error;
logger.error('Database error in deleteRecipe:', { error, recipeId, userId });
if (error instanceof NotFoundError) throw error;
throw new Error('Failed to delete recipe.');
}
}
@@ -175,10 +170,14 @@ export class RecipeRepository {
const res = await this.db.query<Recipe>(query, values);
if (res.rowCount === 0) {
throw new Error('Recipe not found or user does not have permission to update.');
throw new NotFoundError('Recipe not found or user does not have permission to update.');
}
return res.rows[0];
} catch (error) {
// Re-throw specific, known errors to allow for more precise error handling in the calling code.
if (error instanceof NotFoundError || (error instanceof Error && error.message.includes('No fields provided'))) {
throw error;
}
logger.error('Database error in updateRecipe:', { error, recipeId, userId });
throw new Error('Failed to update recipe.');
}
@@ -189,7 +188,7 @@ export class RecipeRepository {
* @param recipeId The ID of the recipe to retrieve.
* @returns A promise that resolves to the Recipe object or undefined if not found.
*/
async getRecipeById(recipeId: number): Promise<Recipe | undefined> {
async getRecipeById(recipeId: number): Promise<Recipe> {
try {
const query = `
SELECT
@@ -205,6 +204,9 @@ export class RecipeRepository {
GROUP BY r.recipe_id;
`;
const res = await this.db.query<Recipe>(query, [recipeId]);
if (res.rowCount === 0) {
throw new NotFoundError(`Recipe with ID ${recipeId} not found.`);
}
return res.rows[0];
} catch (error) {
logger.error('Database error in getRecipeById:', { error, recipeId });

View File

@@ -1,7 +1,7 @@
// src/services/db/shopping.db.ts
import type { Pool, PoolClient } from 'pg';
import { getPool } from './connection.db';
import { ForeignKeyConstraintError, UniqueConstraintError } from './errors.db';
import { getPool } from './connection.db';
import { ForeignKeyConstraintError, UniqueConstraintError, NotFoundError } from './errors.db';
import { logger } from '../logger.server';
import {
ShoppingList,
@@ -88,7 +88,7 @@ export class ShoppingRepository {
* @param userId The ID of the user requesting the list.
* @returns A promise that resolves to the ShoppingList object or undefined if not found or not owned by the user.
*/
async getShoppingListById(listId: number, userId: string): Promise<ShoppingList | undefined> {
async getShoppingListById(listId: number, userId: string): Promise<ShoppingList> {
try {
const query = `
SELECT
@@ -112,6 +112,9 @@ export class ShoppingRepository {
GROUP BY sl.shopping_list_id;
`;
const res = await this.db.query<ShoppingList>(query, [listId, userId]);
if (res.rowCount === 0) {
throw new NotFoundError('Shopping list not found or you do not have permission to view it.');
}
return res.rows[0];
} catch (error) {
logger.error('Database error in getShoppingListById:', { error, listId, userId });
@@ -129,10 +132,10 @@ export class ShoppingRepository {
const res = await this.db.query('DELETE FROM public.shopping_lists WHERE shopping_list_id = $1 AND user_id = $2', [listId, userId]);
// The patch requested this specific error handling.
if (res.rowCount === 0) {
throw new Error('Shopping list not found or user does not have permission to delete.');
throw new NotFoundError('Shopping list not found or user does not have permission to delete.');
}
} catch (error) {
// The patch requested this specific error handling.
if (error instanceof NotFoundError) throw error;
logger.error('Database error in deleteShoppingList:', { error, listId, userId });
throw new Error('Failed to delete shopping list.');
}
@@ -175,11 +178,10 @@ export class ShoppingRepository {
const res = await this.db.query('DELETE FROM public.shopping_list_items WHERE shopping_list_item_id = $1', [itemId]);
// The patch requested this specific error handling.
if (res.rowCount === 0) {
throw new Error('Shopping list item not found.');
throw new NotFoundError('Shopping list item not found.');
}
} catch (error) {
// The patch requested this specific error handling.
if (error instanceof Error && error.message.startsWith('Shopping list item not found')) throw error;
if (error instanceof NotFoundError) throw error;
logger.error('Database error in removeShoppingListItem:', { error, itemId });
throw new Error('Failed to remove item from shopping list.');
}
@@ -246,9 +248,8 @@ export class ShoppingRepository {
);
return res.rows[0];
} catch (error) {
// The patch requested this specific error handling.
if ((error as any).code === '23505') {
throw new UniqueConstraintError('Location name exists');
throw new UniqueConstraintError('A pantry location with this name already exists.');
} else if ((error as any).code === '23503') {
throw new ForeignKeyConstraintError('User not found');
}
@@ -295,12 +296,12 @@ export class ShoppingRepository {
const res = await this.db.query<ShoppingListItem>(query, values);
// The patch requested this specific error handling.
if (res.rowCount === 0) {
throw new Error('Shopping list item not found.');
throw new NotFoundError('Shopping list item not found.');
}
return res.rows[0];
} catch (error) {
// Re-throw specific, known errors to allow for more precise error handling in the calling code.
if (error instanceof Error && (error.message.startsWith('Shopping list item not found') || error.message.startsWith('No valid fields'))) {
if (error instanceof NotFoundError || (error instanceof Error && error.message.startsWith('No valid fields'))) {
throw error;
}
logger.error('Database error in updateShoppingListItem:', { error, itemId, updates });

View File

@@ -2,7 +2,7 @@
import type { Pool, PoolClient } from 'pg';
import { getPool } from './connection.db';
import { logger } from '../logger.server';
import { UniqueConstraintError, ForeignKeyConstraintError } from './errors.db';
import { UniqueConstraintError, ForeignKeyConstraintError, NotFoundError } from './errors.db';
import { Profile, MasterGroceryItem, ShoppingList, ActivityLogItem, UserProfile, SearchQuery } from '../../types';
import { ShoppingRepository } from './shopping.db';
import { PersonalizationRepository } from './personalization.db';
@@ -114,12 +114,8 @@ export class UserRepository {
} catch (error) {
await client.query('ROLLBACK');
// Check for specific PostgreSQL error codes
if (error instanceof Error && 'code' in error && error.code === '23505') { // unique_violation
if (error instanceof Error && 'code' in error && error.code === '23505') {
logger.warn(`Attempted to create a user with an existing email: ${email}`);
// The patch requested this specific error handling.
if ((error as any).code === '23505') {
throw new UniqueConstraintError('A user with this email address already exists.');
}
throw new UniqueConstraintError('A user with this email address already exists.');
}
logger.error('Database transaction error in createUser:', { error });
@@ -197,7 +193,7 @@ export class UserRepository {
* @returns A promise that resolves to the user's profile object or undefined if not found.
*/
// prettier-ignore
async findUserProfileById(userId: string): Promise<Profile | undefined> {
async findUserProfileById(userId: string): Promise<Profile> {
try {
const res = await this.db.query<Profile>(
`SELECT
@@ -219,6 +215,9 @@ export class UserRepository {
WHERE p.user_id = $1`,
[userId]
);
if (res.rowCount === 0) {
throw new NotFoundError('Profile not found for this user.');
}
return res.rows[0];
} catch (error) {
logger.error('Database error in findUserProfileById:', { error });
@@ -260,11 +259,10 @@ export class UserRepository {
query, values
);
if (res.rowCount === 0) {
throw new Error('User not found or user does not have permission to update.');
throw new NotFoundError('User not found or user does not have permission to update.');
}
return res.rows[0];
} catch (error) {
if (error instanceof Error && error.message.includes('User not found')) throw error;
logger.error('Database error in updateUserProfile:', { error });
throw new Error('Failed to update user profile in database.');
}