feat: Update AI service to use new Google Generative AI SDK
Some checks are pending
Deploy to Test Environment / deploy-to-test (push) Has started running
Some checks are pending
Deploy to Test Environment / deploy-to-test (push) Has started running
- Refactored AIService to integrate with the latest GoogleGenAI SDK, updating the generateContent method signature and response handling. - Adjusted error handling and logging for improved clarity and consistency. - Enhanced mock implementations in tests to align with the new SDK structure. refactor: Modify Admin DB service to use Profile type - Updated AdminRepository to replace User type with Profile in relevant methods. - Enhanced test cases to utilize mock factories for creating Profile and AdminUserView objects. fix: Improve error handling in BudgetRepository - Implemented type-safe checks for PostgreSQL error codes to enhance error handling in createBudget method. test: Refactor Deals DB tests for type safety - Updated DealsRepository tests to use Pool type for mock instances, ensuring type safety. chore: Add new mock factories for testing - Introduced mock factories for UserWithPasswordHash, Profile, WatchedItemDeal, LeaderboardUser, and UnmatchedFlyerItem to streamline test data creation. style: Clean up queue service tests - Refactored queue service tests to improve readability and maintainability, including better handling of mock worker instances. docs: Update types to include UserWithPasswordHash - Added UserWithPasswordHash interface to types for better clarity on user authentication data structure. chore: Remove deprecated Google AI SDK references - Updated code and documentation to reflect the migration to the new Google Generative AI SDK, removing references to the deprecated SDK.
This commit is contained in:
@@ -4,8 +4,8 @@ import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
||||
import type { Pool, PoolClient } from 'pg';
|
||||
import { ForeignKeyConstraintError, NotFoundError } from './errors.db';
|
||||
import { AdminRepository } from './admin.db';
|
||||
import type { SuggestedCorrection, AdminUserView, User } from '../../types';
|
||||
|
||||
import type { SuggestedCorrection, AdminUserView, Profile } from '../../types';
|
||||
import { createMockSuggestedCorrection, createMockAdminUserView, createMockProfile } from '../../tests/utils/mockFactories';
|
||||
// Un-mock the module we are testing
|
||||
vi.unmock('./admin.db');
|
||||
|
||||
@@ -45,9 +45,7 @@ describe('Admin DB Service', () => {
|
||||
|
||||
describe('getSuggestedCorrections', () => {
|
||||
it('should execute the correct query and return corrections', async () => {
|
||||
const mockCorrections: SuggestedCorrection[] = [
|
||||
{ suggested_correction_id: 1, flyer_item_id: 101, user_id: 'user-1', correction_type: 'WRONG_PRICE', suggested_value: '250', status: 'pending', created_at: new Date().toISOString() },
|
||||
];
|
||||
const mockCorrections: SuggestedCorrection[] = [createMockSuggestedCorrection({ suggested_correction_id: 1 })];
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: mockCorrections });
|
||||
|
||||
const result = await adminRepo.getSuggestedCorrections(mockLogger);
|
||||
@@ -106,7 +104,7 @@ describe('Admin DB Service', () => {
|
||||
|
||||
describe('updateSuggestedCorrection', () => {
|
||||
it('should update the suggested value and return the updated correction', async () => {
|
||||
const mockCorrection: SuggestedCorrection = { suggested_correction_id: 1, flyer_item_id: 101, user_id: 'user-1', correction_type: 'WRONG_PRICE', suggested_value: '300', status: 'pending', created_at: new Date().toISOString() };
|
||||
const mockCorrection = createMockSuggestedCorrection({ suggested_correction_id: 1, suggested_value: '300' });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [mockCorrection], rowCount: 1 });
|
||||
|
||||
const result = await adminRepo.updateSuggestedCorrection(1, '300', mockLogger);
|
||||
@@ -281,18 +279,19 @@ describe('Admin DB Service', () => {
|
||||
|
||||
describe('resolveUnmatchedFlyerItem', () => {
|
||||
it('should execute a transaction to resolve an unmatched item', async () => {
|
||||
// Create a mock client that we can reference both inside and outside the transaction mock.
|
||||
const mockClient = { query: vi.fn() };
|
||||
(mockClient.query as Mock)
|
||||
.mockResolvedValueOnce({ rows: [{ flyer_item_id: 55 }] }) // SELECT flyer_item_id from unmatched_flyer_items
|
||||
.mockResolvedValueOnce({ rowCount: 1 }) // UPDATE flyer_items table
|
||||
.mockResolvedValueOnce({ rowCount: 1 }); // UPDATE unmatched_flyer_items table
|
||||
|
||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||
const mockClient = { query: vi.fn() };
|
||||
(mockClient.query as Mock)
|
||||
.mockResolvedValueOnce({ rows: [{ flyer_item_id: 55 }] }) // SELECT flyer_item_id from unmatched_flyer_items
|
||||
.mockResolvedValueOnce({ rowCount: 1 }) // UPDATE flyer_items table
|
||||
.mockResolvedValueOnce({ rowCount: 1 }); // UPDATE unmatched_flyer_items table
|
||||
return callback(mockClient as unknown as PoolClient);
|
||||
});
|
||||
|
||||
await adminRepo.resolveUnmatchedFlyerItem(1, 101, mockLogger);
|
||||
|
||||
const mockClient = (vi.mocked(withTransaction).mock.calls[0][0] as any).mock.instances[0];
|
||||
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('SELECT flyer_item_id FROM public.unmatched_flyer_items'), [1]);
|
||||
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('UPDATE public.flyer_items'), [101, 55]);
|
||||
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining("UPDATE public.unmatched_flyer_items SET status = 'resolved'"), [1]);
|
||||
@@ -450,7 +449,7 @@ describe('Admin DB Service', () => {
|
||||
|
||||
describe('getAllUsers', () => {
|
||||
it('should return a list of all users for the admin view', async () => {
|
||||
const mockUsers: AdminUserView[] = [{ user_id: '1', email: 'test@test.com', created_at: '', role: 'user', full_name: 'Test', avatar_url: null }];
|
||||
const mockUsers: AdminUserView[] = [createMockAdminUserView({ user_id: '1', email: 'test@test.com' })];
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: mockUsers });
|
||||
const result = await adminRepo.getAllUsers(mockLogger);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('FROM public.users u JOIN public.profiles p'));
|
||||
@@ -460,11 +459,11 @@ describe('Admin DB Service', () => {
|
||||
|
||||
describe('updateUserRole', () => {
|
||||
it('should update the user role and return the updated user', async () => {
|
||||
const mockUser: User = { user_id: '1', email: 'test@test.com' };
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [mockUser], rowCount: 1 });
|
||||
const mockProfile: Profile = createMockProfile({ user_id: '1', role: 'admin' });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [mockProfile], rowCount: 1 });
|
||||
const result = await adminRepo.updateUserRole('1', 'admin', mockLogger);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith('UPDATE public.profiles SET role = $1 WHERE user_id = $2 RETURNING *', ['admin', '1']);
|
||||
expect(result).toEqual(mockUser);
|
||||
expect(result).toEqual(mockProfile);
|
||||
});
|
||||
|
||||
it('should throw an error if the user is not found (rowCount is 0)', async () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Pool, PoolClient } from 'pg';
|
||||
import { getPool, withTransaction } from './connection.db';
|
||||
import { ForeignKeyConstraintError, NotFoundError } from './errors.db';
|
||||
import type { Logger } from 'pino';
|
||||
import { SuggestedCorrection, MostFrequentSaleItem, Recipe, RecipeComment, UnmatchedFlyerItem, ActivityLogItem, Receipt, User, AdminUserView } from '../../types';
|
||||
import { SuggestedCorrection, MostFrequentSaleItem, Recipe, RecipeComment, UnmatchedFlyerItem, ActivityLogItem, Receipt, User, AdminUserView, Profile } from '../../types';
|
||||
|
||||
export class AdminRepository {
|
||||
private db: Pool | PoolClient;
|
||||
@@ -395,7 +395,7 @@ export class AdminRepository {
|
||||
action: string;
|
||||
displayText: string;
|
||||
icon?: string | null;
|
||||
details?: Record<string, any> | null; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
details?: Record<string, any> | null;
|
||||
}, logger: Logger): Promise<void> {
|
||||
const { userId, action, displayText, icon, details } = logData;
|
||||
try {
|
||||
@@ -515,9 +515,9 @@ export class AdminRepository {
|
||||
* @param role The new role to assign ('user' or 'admin').
|
||||
* @returns A promise that resolves to the updated Profile object.
|
||||
*/
|
||||
async updateUserRole(userId: string, role: 'user' | 'admin', logger: Logger): Promise<User> {
|
||||
async updateUserRole(userId: string, role: 'user' | 'admin', logger: Logger): Promise<Profile> {
|
||||
try {
|
||||
const res = await this.db.query<User>(
|
||||
const res = await this.db.query<Profile>(
|
||||
'UPDATE public.profiles SET role = $1 WHERE user_id = $2 RETURNING *',
|
||||
[role, userId]
|
||||
);
|
||||
|
||||
@@ -54,7 +54,9 @@ export class BudgetRepository {
|
||||
});
|
||||
} catch (error) {
|
||||
// The patch requested this specific error handling.
|
||||
if ((error as any).code === '23503') {
|
||||
// Type-safe check for a PostgreSQL error code.
|
||||
// This ensures 'error' is an object with a 'code' property before we access it.
|
||||
if (error instanceof Error && 'code' in error && error.code === '23503') {
|
||||
throw new ForeignKeyConstraintError('The specified user does not exist.');
|
||||
}
|
||||
logger.error({ err: error, budgetData, userId }, 'Database error in createBudget');
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
||||
import { DealsRepository } from './deals.db';
|
||||
import type { WatchedItemDeal } from '../../types';
|
||||
import type { Pool } from 'pg';
|
||||
|
||||
// Un-mock the module we are testing to ensure we use the real implementation.
|
||||
vi.unmock('./deals.db');
|
||||
@@ -19,12 +20,13 @@ vi.mock('../logger.server', () => ({
|
||||
import { logger as mockLogger } from '../logger.server';
|
||||
|
||||
describe('Deals DB Service', () => {
|
||||
// Import the Pool type to use for casting the mock instance.
|
||||
let dealsRepo: DealsRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Instantiate the repository with the mock pool for each test
|
||||
dealsRepo = new DealsRepository(mockPoolInstance as any);
|
||||
dealsRepo = new DealsRepository(mockPoolInstance as unknown as Pool);
|
||||
});
|
||||
|
||||
describe('findBestPricesForWatchedItems', () => {
|
||||
|
||||
@@ -44,13 +44,22 @@ export class NotFoundError extends DatabaseError {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the structure for a single validation issue, often from a library like Zod.
|
||||
*/
|
||||
export interface ValidationIssue {
|
||||
path: (string | number)[];
|
||||
message: string;
|
||||
[key: string]: any; // Allow other properties that might exist on the error object
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when request validation fails (e.g., missing body fields or invalid params).
|
||||
*/
|
||||
export class ValidationError extends DatabaseError {
|
||||
public validationErrors: any[];
|
||||
public validationErrors: ValidationIssue[];
|
||||
|
||||
constructor(errors: any[], message = 'The request data is invalid.') {
|
||||
constructor(errors: ValidationIssue[], message = 'The request data is invalid.') {
|
||||
super(message, 400); // 400 Bad Request
|
||||
this.name = 'ValidationError';
|
||||
this.validationErrors = errors;
|
||||
|
||||
@@ -217,8 +217,8 @@ describe('Flyer DB Service', () => {
|
||||
.mockResolvedValueOnce({ rows: [{ store_id: 1 }] }) // findOrCreateStore
|
||||
.mockResolvedValueOnce({ rows: [mockFlyer] }) // insertFlyer
|
||||
.mockResolvedValueOnce({ rows: mockItems }); // insertFlyerItems
|
||||
return callback(mockClient as any);
|
||||
}); // Cast to any is acceptable here as we are mocking the implementation
|
||||
return callback(mockClient as unknown as PoolClient);
|
||||
});
|
||||
|
||||
const result = await createFlyerAndItems(flyerData, itemsData, mockLogger);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user