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

- 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:
2025-12-14 17:14:44 -08:00
parent eb0e183f61
commit f73b1422ab
42 changed files with 530 additions and 365 deletions

View File

@@ -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 () => {

View File

@@ -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]
);

View File

@@ -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');

View File

@@ -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', () => {

View File

@@ -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;

View File

@@ -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);