Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
460adb9506 | ||
| 7aa1f756a9 | |||
|
|
c484a8ca9b | ||
| 28d2c9f4ec | |||
|
|
ee253e9449 | ||
| b6c15e53d0 | |||
|
|
722162c2c3 | ||
| 02a76fe996 | |||
|
|
0ebb03a7ab | ||
| 748ac9e049 | |||
|
|
495edd621c | ||
| 4ffca19db6 | |||
|
|
717427c5d7 | ||
| cc438a0e36 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.7.7",
|
||||
"version": "0.7.14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.7.7",
|
||||
"version": "0.7.14",
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.14.2",
|
||||
"@bull-board/express": "^6.14.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"private": true,
|
||||
"version": "0.7.7",
|
||||
"version": "0.7.14",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||
|
||||
@@ -111,7 +111,7 @@ async function main() {
|
||||
|
||||
const flyerQuery = `
|
||||
INSERT INTO public.flyers (file_name, image_url, checksum, store_id, valid_from, valid_to)
|
||||
VALUES ('safeway-flyer.jpg', '/sample-assets/safeway-flyer.jpg', 'sample-checksum-123', ${storeMap.get('Safeway')}, $1, $2)
|
||||
VALUES ('safeway-flyer.jpg', '/flyer-images/safeway-flyer.jpg', 'a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0', ${storeMap.get('Safeway')}, $1, $2)
|
||||
RETURNING flyer_id;
|
||||
`;
|
||||
const flyerRes = await client.query<{ flyer_id: number }>(flyerQuery, [
|
||||
|
||||
@@ -933,7 +933,7 @@ describe('API Client', () => {
|
||||
|
||||
it('logSearchQuery should send a POST request with query data', async () => {
|
||||
const queryData = createMockSearchQueryPayload({ query_text: 'apples', result_count: 10, was_successful: true });
|
||||
await apiClient.logSearchQuery(queryData);
|
||||
await apiClient.logSearchQuery(queryData as any);
|
||||
expect(capturedUrl?.pathname).toBe('/api/search/log');
|
||||
expect(capturedBody).toEqual(queryData);
|
||||
});
|
||||
@@ -960,7 +960,7 @@ describe('API Client', () => {
|
||||
result_count: 0,
|
||||
was_successful: false,
|
||||
});
|
||||
await apiClient.logSearchQuery(queryData);
|
||||
await apiClient.logSearchQuery(queryData as any);
|
||||
expect(logger.warn).toHaveBeenCalledWith('Failed to log search query', { error: apiError });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -203,7 +203,11 @@ describe('Admin DB Service', () => {
|
||||
.mockRejectedValueOnce(new Error('DB Read Error'));
|
||||
|
||||
// The Promise.all should reject, and the function should re-throw the error
|
||||
await expect(adminRepo.getApplicationStats(mockLogger)).rejects.toThrow('DB Read Error');
|
||||
// The handleDbError function wraps the original error in a new one with a default message,
|
||||
// so we should test for that specific message.
|
||||
await expect(adminRepo.getApplicationStats(mockLogger)).rejects.toThrow(
|
||||
'Failed to retrieve application statistics.',
|
||||
);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
{ err: expect.any(Error) },
|
||||
'Database error in getApplicationStats',
|
||||
@@ -277,7 +281,7 @@ describe('Admin DB Service', () => {
|
||||
'Failed to get most frequent sale items.',
|
||||
);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
{ err: dbError },
|
||||
{ err: dbError, days: 30, limit: 10 },
|
||||
'Database error in getMostFrequentSaleItems',
|
||||
);
|
||||
});
|
||||
@@ -688,7 +692,9 @@ describe('Admin DB Service', () => {
|
||||
it('should re-throw a generic error if the database query fails for other reasons', async () => {
|
||||
const dbError = new Error('DB Error');
|
||||
mockDb.query.mockRejectedValue(dbError);
|
||||
await expect(adminRepo.updateUserRole('1', 'admin', mockLogger)).rejects.toThrow('DB Error');
|
||||
await expect(adminRepo.updateUserRole('1', 'admin', mockLogger)).rejects.toThrow(
|
||||
'Failed to update user role.',
|
||||
);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
{ err: dbError, userId: '1', role: 'admin' },
|
||||
'Database error in updateUserRole',
|
||||
|
||||
@@ -82,15 +82,15 @@ describe('Deals DB Service', () => {
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should re-throw the error if the database query fails', async () => {
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockDb.query.mockRejectedValue(dbError);
|
||||
|
||||
await expect(dealsRepo.findBestPricesForWatchedItems('user-1', mockLogger)).rejects.toThrow(
|
||||
dbError,
|
||||
'Failed to find best prices for watched items.',
|
||||
);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
{ err: dbError },
|
||||
{ err: dbError, userId: 'user-1' },
|
||||
'Database error in findBestPricesForWatchedItems',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -274,7 +274,7 @@ describe('Flyer DB Service', () => {
|
||||
ForeignKeyConstraintError,
|
||||
);
|
||||
await expect(flyerRepo.insertFlyerItems(999, itemsData, mockLogger)).rejects.toThrow(
|
||||
'The specified flyer does not exist.',
|
||||
'The specified flyer, category, master item, or product does not exist.',
|
||||
);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
{ err: dbError, flyerId: 999 },
|
||||
@@ -285,10 +285,10 @@ describe('Flyer DB Service', () => {
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||
// The implementation now re-throws the original error, so we should expect that.
|
||||
// The implementation wraps the error using handleDbError
|
||||
await expect(
|
||||
flyerRepo.insertFlyerItems(1, [{ item: 'Test' } as FlyerItemInsert], mockLogger),
|
||||
).rejects.toThrow(dbError);
|
||||
).rejects.toThrow('An unknown error occurred while inserting flyer items.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
{ err: dbError, flyerId: 1 },
|
||||
'Database error in insertFlyerItems',
|
||||
@@ -691,11 +691,7 @@ describe('Flyer DB Service', () => {
|
||||
);
|
||||
|
||||
await expect(flyerRepo.deleteFlyer(999, mockLogger)).rejects.toThrow(
|
||||
'Failed to delete flyer.',
|
||||
);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
{ err: expect.any(NotFoundError), flyerId: 999 },
|
||||
'Database transaction error in deleteFlyer',
|
||||
'Flyer with ID 999 not found.',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -103,10 +103,17 @@ export class FlyerRepository {
|
||||
const result = await this.db.query<Flyer>(query, values);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
const isChecksumError =
|
||||
error instanceof Error && error.message.includes('flyers_checksum_check');
|
||||
|
||||
handleDbError(error, logger, 'Database error in insertFlyer', { flyerData }, {
|
||||
uniqueMessage: 'A flyer with this checksum already exists.',
|
||||
fkMessage: 'The specified user or store for this flyer does not exist.',
|
||||
checkMessage: 'Invalid status provided for flyer.',
|
||||
// Provide a more specific message for the checksum constraint violation,
|
||||
// which is a common issue during seeding or testing with placeholder data.
|
||||
checkMessage: isChecksumError
|
||||
? 'The provided checksum is invalid or does not meet format requirements (e.g., must be a 64-character SHA-256 hash).'
|
||||
: 'Invalid status provided for flyer.',
|
||||
defaultMessage: 'Failed to insert flyer into database.',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ describe('Notification DB Service', () => {
|
||||
notificationRepo.createBulkNotifications(notificationsToCreate, mockLogger),
|
||||
).rejects.toThrow(ForeignKeyConstraintError);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
{ err: dbError },
|
||||
{ err: dbError, notifications: notificationsToCreate },
|
||||
'Database error in createBulkNotifications',
|
||||
);
|
||||
});
|
||||
@@ -208,7 +208,7 @@ describe('Notification DB Service', () => {
|
||||
notificationRepo.createBulkNotifications(notificationsToCreate, mockLogger),
|
||||
).rejects.toThrow('Failed to create bulk notifications.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
{ err: dbError },
|
||||
{ err: dbError, notifications: notificationsToCreate },
|
||||
'Database error in createBulkNotifications',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -442,10 +442,6 @@ describe('Recipe DB Service', () => {
|
||||
await expect(recipeRepo.forkRecipe('user-123', 1, mockLogger)).rejects.toThrow(
|
||||
'Recipe is not public and cannot be forked.',
|
||||
);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
{ err: dbError, userId: 'user-123', originalRecipeId: 1 },
|
||||
'Database error in forkRecipe',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
|
||||
@@ -239,6 +239,10 @@ export class RecipeRepository {
|
||||
}
|
||||
return res.rows[0];
|
||||
} catch (error) {
|
||||
// Explicitly re-throw the "No fields" error before it gets caught by the generic handler.
|
||||
if (error instanceof Error && error.message === 'No fields provided to update.') {
|
||||
throw error;
|
||||
}
|
||||
handleDbError(error, logger, 'Database error in updateRecipe', { recipeId, userId, updates }, {
|
||||
defaultMessage: 'Failed to update recipe.',
|
||||
});
|
||||
|
||||
@@ -233,9 +233,7 @@ describe('User DB Service', () => {
|
||||
}
|
||||
|
||||
expect(withTransaction).toHaveBeenCalledTimes(1);
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
`Attempted to create a user with an existing email: exists@example.com`,
|
||||
);
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(`Attempted to create a user with an existing email: exists@example.com`);
|
||||
});
|
||||
|
||||
it('should throw an error if profile is not found after user creation', async () => {
|
||||
@@ -836,9 +834,7 @@ describe('User DB Service', () => {
|
||||
);
|
||||
|
||||
// Act & Assert: The outer function catches the NotFoundError and re-throws it.
|
||||
await expect(exportUserData('123', mockLogger)).rejects.toThrow(
|
||||
'Failed to export user data.',
|
||||
);
|
||||
await expect(exportUserData('123', mockLogger)).rejects.toThrow('Profile not found');
|
||||
expect(withTransaction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Pool, PoolClient } from 'pg';
|
||||
import { getPool } from './connection.db';
|
||||
import type { Logger } from 'pino';
|
||||
import { NotFoundError, handleDbError } from './errors.db';
|
||||
import { NotFoundError, handleDbError, UniqueConstraintError } from './errors.db';
|
||||
import {
|
||||
Profile,
|
||||
MasterGroceryItem,
|
||||
@@ -127,6 +127,12 @@ export class UserRepository {
|
||||
logger.debug({ user: fullUserProfile }, `[DB createUser] Fetched full profile for new user:`);
|
||||
return fullUserProfile;
|
||||
}).catch((error) => {
|
||||
// Specific handling for unique constraint violation on user creation
|
||||
if (error instanceof Error && 'code' in error && (error as any).code === '23505') {
|
||||
logger.warn(`Attempted to create a user with an existing email: ${email}`);
|
||||
throw new UniqueConstraintError('A user with this email address already exists.');
|
||||
}
|
||||
// Fallback to generic handler for all other errors
|
||||
handleDbError(error, logger, 'Error during createUser transaction', { email }, {
|
||||
uniqueMessage: 'A user with this email address already exists.',
|
||||
defaultMessage: 'Failed to create user in database.',
|
||||
|
||||
@@ -109,7 +109,7 @@ export const createMockUser = (overrides: Partial<User> = {}): User => {
|
||||
* @returns A complete and type-safe UserProfile object.
|
||||
*/
|
||||
export const createMockUserProfile = (
|
||||
overrides: Partial<UserProfile & { user: Partial<User> }> = {},
|
||||
overrides: Partial<Omit<UserProfile, 'user'>> & { user?: Partial<User> } = {},
|
||||
): UserProfile => {
|
||||
// The user object is the source of truth for user_id and email.
|
||||
const user = createMockUser(overrides.user);
|
||||
|
||||
Reference in New Issue
Block a user