DB refactor for easier testsing
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 5m16s
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 5m16s
App.ts refactor into hooks unit tests
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
// src/services/db/personalization.db.ts
|
||||
import { getPool } from './connection.db';
|
||||
import type { Pool, PoolClient } from 'pg';
|
||||
import { getPool } from './connection.db';
|
||||
import { UniqueConstraintError, ForeignKeyConstraintError } from './errors.db';
|
||||
import { logger } from '../logger';
|
||||
import { logger } from '../logger.server';
|
||||
import {
|
||||
MasterGroceryItem,
|
||||
PantryRecipe,
|
||||
@@ -14,357 +15,357 @@ import {
|
||||
Recipe,
|
||||
} from '../../types';
|
||||
|
||||
/**
|
||||
* Retrieves all master grocery items from the database.
|
||||
* This is used to provide a list of known items to the AI for better matching.
|
||||
* @returns A promise that resolves to an array of MasterGroceryItem objects.
|
||||
*/
|
||||
export async function getAllMasterItems(): Promise<MasterGroceryItem[]> {
|
||||
try {
|
||||
const res = await getPool().query<MasterGroceryItem>('SELECT * FROM public.master_grocery_items ORDER BY name ASC');
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getAllMasterItems:', { error });
|
||||
throw new Error('Failed to retrieve master grocery items.');
|
||||
export class PersonalizationRepository {
|
||||
private db: Pool | PoolClient;
|
||||
|
||||
constructor(db: Pool | PoolClient = getPool()) {
|
||||
this.db = db;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all watched master items for a specific user.
|
||||
* @param userId The UUID of the user.
|
||||
* @returns A promise that resolves to an array of MasterGroceryItem objects.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export async function getWatchedItems(userId: string): Promise<MasterGroceryItem[]> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT mgi.*
|
||||
FROM public.master_grocery_items mgi
|
||||
JOIN public.user_watched_items uwi ON mgi.master_grocery_item_id = uwi.master_item_id
|
||||
WHERE uwi.user_id = $1
|
||||
ORDER BY mgi.name ASC;
|
||||
`;
|
||||
const res = await getPool().query<MasterGroceryItem>(query, [userId]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getWatchedItems:', { error, userId });
|
||||
throw new Error('Failed to retrieve watched items.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an item to a user's watchlist. If the master item doesn't exist, it creates it.
|
||||
* @param userId The UUID of the user.
|
||||
* @param itemName The name of the item to watch.
|
||||
* @param categoryName The category of the item.
|
||||
* @returns A promise that resolves to the MasterGroceryItem that was added to the watchlist.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export async function addWatchedItem(userId: string, itemName: string, categoryName: string): Promise<MasterGroceryItem> {
|
||||
const client = await getPool().connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Find category ID
|
||||
const categoryRes = await client.query<{ category_id: number }>('SELECT category_id FROM public.categories WHERE name = $1', [categoryName]);
|
||||
const categoryId = categoryRes.rows[0]?.category_id;
|
||||
if (!categoryId) {
|
||||
throw new Error(`Category '${categoryName}' not found.`);
|
||||
}
|
||||
|
||||
// Find or create master item
|
||||
let masterItem: MasterGroceryItem;
|
||||
const masterItemRes = await client.query<MasterGroceryItem>('SELECT * FROM public.master_grocery_items WHERE name = $1', [itemName]);
|
||||
if (masterItemRes.rows.length > 0) {
|
||||
masterItem = masterItemRes.rows[0];
|
||||
} else {
|
||||
const newMasterItemRes = await client.query<MasterGroceryItem>(
|
||||
'INSERT INTO public.master_grocery_items (name, category_id) VALUES ($1, $2) RETURNING *',
|
||||
[itemName, categoryId]
|
||||
);
|
||||
masterItem = newMasterItemRes.rows[0];
|
||||
}
|
||||
|
||||
// Add to user's watchlist, ignoring if it's already there.
|
||||
await client.query(
|
||||
'INSERT INTO public.user_watched_items (user_id, master_item_id) VALUES ($1, $2) ON CONFLICT (user_id, master_item_id) DO NOTHING',
|
||||
[userId, masterItem.master_grocery_item_id]
|
||||
);
|
||||
|
||||
await client.query('COMMIT');
|
||||
return masterItem;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && 'code' in error && error.code === '23505') {
|
||||
// This case is handled by ON CONFLICT, but it's good practice for other functions.
|
||||
throw new UniqueConstraintError('This item is already in the watchlist.');
|
||||
}
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Database transaction error in addWatchedItem:', { error });
|
||||
throw new Error('Failed to add item to watchlist.');
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from a user's watchlist.
|
||||
* @param userId The UUID of the user.
|
||||
* @param masterItemId The ID of the master item to remove.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export async function removeWatchedItem(userId: string, masterItemId: number): Promise<void> {
|
||||
try {
|
||||
await getPool().query('DELETE FROM public.user_watched_items WHERE user_id = $1 AND master_item_id = $2', [userId, masterItemId]);
|
||||
} catch (error) {
|
||||
logger.error('Database error in removeWatchedItem:', { error });
|
||||
throw new Error('Failed to remove item from watchlist.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a database function to find recipes that can be made from a user's pantry.
|
||||
* @param userId The ID of the user.
|
||||
* @returns A promise that resolves to an array of recipes.
|
||||
*/
|
||||
export async function findRecipesFromPantry(userId: string): Promise<PantryRecipe[]> {
|
||||
try {
|
||||
const res = await getPool().query<PantryRecipe>('SELECT * FROM public.find_recipes_from_pantry($1)', [userId]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in findRecipesFromPantry:', { error, userId });
|
||||
throw new Error('Failed to find recipes from pantry.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a database function to recommend recipes for a user.
|
||||
* @param userId The ID of the user.
|
||||
* @param limit The maximum number of recipes to recommend.
|
||||
* @returns A promise that resolves to an array of recommended recipes.
|
||||
*/
|
||||
export async function recommendRecipesForUser(userId: string, limit: number): Promise<RecommendedRecipe[]> {
|
||||
try {
|
||||
const res = await getPool().query<RecommendedRecipe>('SELECT * FROM public.recommend_recipes_for_user($1, $2)', [userId, limit]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in recommendRecipesForUser:', { error, userId });
|
||||
throw new Error('Failed to recommend recipes.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a database function to get the best current sale prices for a user's watched items.
|
||||
* @param userId The ID of the user.
|
||||
* @returns A promise that resolves to an array of the best deals.
|
||||
*/
|
||||
export async function getBestSalePricesForUser(userId: string): Promise<WatchedItemDeal[]> {
|
||||
try {
|
||||
const res = await getPool().query<WatchedItemDeal>('SELECT * FROM public.get_best_sale_prices_for_user($1)', [userId]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getBestSalePricesForUser:', { error, userId });
|
||||
throw new Error('Failed to get best sale prices.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a database function to get the best current sale prices for all watched items across all users.
|
||||
* This is much more efficient than calling getBestSalePricesForUser for each user individually.
|
||||
* @returns A promise that resolves to an array of deals, each augmented with user information.
|
||||
*/
|
||||
export async function getBestSalePricesForAllUsers(): Promise<(WatchedItemDeal & { user_id: string; email: string; full_name: string | null })[]> {
|
||||
try {
|
||||
// This function assumes a corresponding PostgreSQL function `get_best_sale_prices_for_all_users` exists.
|
||||
const res = await getPool().query<(WatchedItemDeal & { user_id: string; email: string; full_name: string | null })>('SELECT * FROM public.get_best_sale_prices_for_all_users()');
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getBestSalePricesForAllUsers:', { error });
|
||||
throw new Error('Failed to get best sale prices for all users.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a database function to suggest unit conversions for a pantry item.
|
||||
* @param pantryItemId The ID of the pantry item.
|
||||
* @returns A promise that resolves to an array of suggested conversions.
|
||||
*/
|
||||
export async function suggestPantryItemConversions(pantryItemId: number): Promise<PantryItemConversion[]> {
|
||||
try {
|
||||
const res = await getPool().query<PantryItemConversion>('SELECT * FROM public.suggest_pantry_item_conversions($1)', [pantryItemId]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in suggestPantryItemConversions:', { error, pantryItemId });
|
||||
throw new Error('Failed to suggest pantry item conversions.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the owner of a specific pantry item.
|
||||
* @param pantryItemId The ID of the pantry item.
|
||||
* @returns A promise that resolves to an object containing the user_id, or undefined if not found.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export async function findPantryItemOwner(pantryItemId: number): Promise<{ user_id: string } | undefined> {
|
||||
try {
|
||||
const res = await getPool().query<{ user_id: string }>(
|
||||
'SELECT user_id FROM public.pantry_items WHERE pantry_item_id = $1',
|
||||
[pantryItemId]
|
||||
);
|
||||
return res.rows[0];
|
||||
} catch (error) {
|
||||
logger.error('Database error in findPantryItemOwner:', { error, pantryItemId });
|
||||
throw new Error('Failed to retrieve pantry item owner from database.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the master list of all available dietary restrictions.
|
||||
* @returns A promise that resolves to an array of DietaryRestriction objects.
|
||||
*/
|
||||
export async function getDietaryRestrictions(): Promise<DietaryRestriction[]> {
|
||||
/**
|
||||
* Retrieves all master grocery items from the database.
|
||||
* @returns A promise that resolves to an array of MasterGroceryItem objects.
|
||||
*/
|
||||
async getAllMasterItems(): Promise<MasterGroceryItem[]> {
|
||||
try {
|
||||
const res = await getPool().query<DietaryRestriction>('SELECT * FROM public.dietary_restrictions ORDER BY type, name');
|
||||
return res.rows;
|
||||
const res = await this.db.query<MasterGroceryItem>('SELECT * FROM public.master_grocery_items ORDER BY name ASC');
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getDietaryRestrictions:', { error });
|
||||
throw new Error('Failed to get dietary restrictions.');
|
||||
logger.error('Database error in getAllMasterItems:', { error });
|
||||
throw new Error('Failed to retrieve master grocery items.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the dietary restrictions for a specific user.
|
||||
* @param userId The ID of the user.
|
||||
* @returns A promise that resolves to an array of the user's selected DietaryRestriction objects.
|
||||
*/
|
||||
export async function getUserDietaryRestrictions(userId: string): Promise<DietaryRestriction[]> {
|
||||
/**
|
||||
* Retrieves all watched master items for a specific user.
|
||||
* @param userId The UUID of the user.
|
||||
* @returns A promise that resolves to an array of MasterGroceryItem objects.
|
||||
*/
|
||||
async getWatchedItems(userId: string): Promise<MasterGroceryItem[]> {
|
||||
try {
|
||||
const query = `
|
||||
const query = `
|
||||
SELECT mgi.*
|
||||
FROM public.master_grocery_items mgi
|
||||
JOIN public.user_watched_items uwi ON mgi.master_grocery_item_id = uwi.master_item_id
|
||||
WHERE uwi.user_id = $1
|
||||
ORDER BY mgi.name ASC;
|
||||
`;
|
||||
const res = await this.db.query<MasterGroceryItem>(query, [userId]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getWatchedItems:', { error, userId });
|
||||
throw new Error('Failed to retrieve watched items.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from a user's watchlist.
|
||||
* @param userId The UUID of the user.
|
||||
* @param masterItemId The ID of the master item to remove.
|
||||
*/
|
||||
async removeWatchedItem(userId: string, masterItemId: number): Promise<void> {
|
||||
try {
|
||||
await this.db.query('DELETE FROM public.user_watched_items WHERE user_id = $1 AND master_item_id = $2', [userId, masterItemId]);
|
||||
} catch (error) {
|
||||
logger.error('Database error in removeWatchedItem:', { error });
|
||||
throw new Error('Failed to remove item from watchlist.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the owner of a specific pantry item.
|
||||
* @param pantryItemId The ID of the pantry item.
|
||||
* @returns A promise that resolves to an object containing the user_id, or undefined if not found.
|
||||
*/
|
||||
async findPantryItemOwner(pantryItemId: number): Promise<{ user_id: string } | undefined> {
|
||||
try {
|
||||
const res = await this.db.query<{ user_id: string }>(
|
||||
'SELECT user_id FROM public.pantry_items WHERE pantry_item_id = $1',
|
||||
[pantryItemId]
|
||||
);
|
||||
return res.rows[0];
|
||||
} catch (error) {
|
||||
logger.error('Database error in findPantryItemOwner:', { error, pantryItemId });
|
||||
throw new Error('Failed to retrieve pantry item owner from database.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an item to a user's watchlist. If the master item doesn't exist, it creates it.
|
||||
* This method should be wrapped in a transaction by the calling service if other operations depend on it.
|
||||
* @param userId The UUID of the user.
|
||||
* @param itemName The name of the item to watch.
|
||||
* @param categoryName The category of the item.
|
||||
* @returns A promise that resolves to the MasterGroceryItem that was added to the watchlist.
|
||||
*/
|
||||
async addWatchedItem(userId: string, itemName: string, categoryName: string): Promise<MasterGroceryItem> {
|
||||
// This method assumes it might be part of a larger transaction, so it uses `this.db`.
|
||||
// The calling service is responsible for acquiring and releasing a client if needed.
|
||||
try {
|
||||
// Find category ID
|
||||
const categoryRes = await this.db.query<{ category_id: number }>('SELECT category_id FROM public.categories WHERE name = $1', [categoryName]);
|
||||
const categoryId = categoryRes.rows[0]?.category_id;
|
||||
if (!categoryId) {
|
||||
throw new Error(`Category '${categoryName}' not found.`);
|
||||
}
|
||||
|
||||
// Find or create master item
|
||||
let masterItem: MasterGroceryItem;
|
||||
const masterItemRes = await this.db.query<MasterGroceryItem>('SELECT * FROM public.master_grocery_items WHERE name = $1', [itemName]);
|
||||
if (masterItemRes.rows.length > 0) {
|
||||
masterItem = masterItemRes.rows[0];
|
||||
} else {
|
||||
const newMasterItemRes = await this.db.query<MasterGroceryItem>(
|
||||
'INSERT INTO public.master_grocery_items (name, category_id) VALUES ($1, $2) RETURNING *',
|
||||
[itemName, categoryId]
|
||||
);
|
||||
masterItem = newMasterItemRes.rows[0];
|
||||
}
|
||||
|
||||
// Add to user's watchlist, ignoring if it's already there.
|
||||
await this.db.query(
|
||||
'INSERT INTO public.user_watched_items (user_id, master_item_id) VALUES ($1, $2) ON CONFLICT (user_id, master_item_id) DO NOTHING',
|
||||
[userId, masterItem.master_grocery_item_id]
|
||||
);
|
||||
|
||||
return masterItem;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && 'code' in error) {
|
||||
if (error.code === '23505') { // unique_violation
|
||||
// This case is handled by ON CONFLICT, but it's good practice for other functions.
|
||||
throw new UniqueConstraintError('This item is already in the watchlist.');
|
||||
} else if (error.code === '23503') { // foreign_key_violation
|
||||
throw new ForeignKeyConstraintError('The specified user or category does not exist.');
|
||||
}
|
||||
}
|
||||
logger.error('Database error in addWatchedItem:', { error });
|
||||
throw new Error('Failed to add item to watchlist.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a database function to get the best current sale prices for all watched items across all users.
|
||||
* This is much more efficient than calling getBestSalePricesForUser for each user individually.
|
||||
* @returns A promise that resolves to an array of deals, each augmented with user information.
|
||||
*/
|
||||
async getBestSalePricesForAllUsers(): Promise<(WatchedItemDeal & { user_id: string; email: string; full_name: string | null })[]> {
|
||||
try {
|
||||
// This function assumes a corresponding PostgreSQL function `get_best_sale_prices_for_all_users` exists.
|
||||
const res = await this.db.query<(WatchedItemDeal & { user_id: string; email: string; full_name: string | null })>('SELECT * FROM public.get_best_sale_prices_for_all_users()');
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getBestSalePricesForAllUsers:', { error });
|
||||
throw new Error('Failed to get best sale prices for all users.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the master list of all available kitchen appliances.
|
||||
* @returns A promise that resolves to an array of Appliance objects.
|
||||
*/
|
||||
async getAppliances(): Promise<Appliance[]> {
|
||||
try {
|
||||
const res = await this.db.query<Appliance>('SELECT * FROM public.appliances ORDER BY name');
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getAppliances:', { error });
|
||||
throw new Error('Failed to get appliances.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the master list of all available dietary restrictions.
|
||||
* @returns A promise that resolves to an array of DietaryRestriction objects.
|
||||
*/
|
||||
async getDietaryRestrictions(): Promise<DietaryRestriction[]> {
|
||||
try {
|
||||
const res = await this.db.query<DietaryRestriction>('SELECT * FROM public.dietary_restrictions ORDER BY type, name');
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getDietaryRestrictions:', { error });
|
||||
throw new Error('Failed to get dietary restrictions.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the dietary restrictions for a specific user.
|
||||
* @param userId The ID of the user.
|
||||
* @returns A promise that resolves to an array of the user's selected DietaryRestriction objects.
|
||||
*/
|
||||
async getUserDietaryRestrictions(userId: string): Promise<DietaryRestriction[]> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT dr.* FROM public.dietary_restrictions dr
|
||||
JOIN public.user_dietary_restrictions udr ON dr.dietary_restriction_id = udr.restriction_id
|
||||
WHERE udr.user_id = $1 ORDER BY dr.type, dr.name;
|
||||
`;
|
||||
const res = await getPool().query<DietaryRestriction>(query, [userId]);
|
||||
return res.rows;
|
||||
const res = await this.db.query<DietaryRestriction>(query, [userId]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getUserDietaryRestrictions:', { error, userId });
|
||||
throw new Error('Failed to get user dietary restrictions.');
|
||||
logger.error('Database error in getUserDietaryRestrictions:', { error, userId });
|
||||
throw new Error('Failed to get user dietary restrictions.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the dietary restrictions for a user, replacing any existing ones.
|
||||
* @param userId The ID of the user.
|
||||
* @param restrictionIds An array of IDs for the selected dietary restrictions.
|
||||
* @returns A promise that resolves when the operation is complete.
|
||||
*/
|
||||
export async function setUserDietaryRestrictions(userId: string, restrictionIds: number[]): Promise<void> {
|
||||
const client = await getPool().connect();
|
||||
/**
|
||||
* Sets the dietary restrictions for a user, replacing any existing ones.
|
||||
* @param userId The ID of the user.
|
||||
* @param restrictionIds An array of IDs for the selected dietary restrictions.
|
||||
* @returns A promise that resolves when the operation is complete.
|
||||
*/
|
||||
async setUserDietaryRestrictions(userId: string, restrictionIds: number[]): Promise<void> {
|
||||
const client = await (this.db as Pool).connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
// 1. Clear existing restrictions for the user
|
||||
await client.query('DELETE FROM public.user_dietary_restrictions WHERE user_id = $1', [userId]);
|
||||
// 2. Insert new ones if any are provided
|
||||
if (restrictionIds.length > 0) {
|
||||
// Using unnest is safer than string concatenation and prevents SQL injection.
|
||||
const insertQuery = `INSERT INTO public.user_dietary_restrictions (user_id, restriction_id) SELECT $1, unnest($2::int[])`;
|
||||
await client.query(insertQuery, [userId, restrictionIds]);
|
||||
}
|
||||
// 3. Commit the transaction
|
||||
await client.query('COMMIT');
|
||||
await client.query('BEGIN');
|
||||
// 1. Clear existing restrictions for the user
|
||||
await client.query('DELETE FROM public.user_dietary_restrictions WHERE user_id = $1', [userId]);
|
||||
// 2. Insert new ones if any are provided
|
||||
if (restrictionIds.length > 0) {
|
||||
// Using unnest is safer than string concatenation and prevents SQL injection.
|
||||
const insertQuery = `INSERT INTO public.user_dietary_restrictions (user_id, restriction_id) SELECT $1, unnest($2::int[])`;
|
||||
await client.query(insertQuery, [userId, restrictionIds]);
|
||||
}
|
||||
// 3. Commit the transaction
|
||||
await client.query('COMMIT');
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
if (error instanceof Error && 'code' in error && error.code === '23503') {
|
||||
throw new ForeignKeyConstraintError('One or more of the specified restriction IDs are invalid.');
|
||||
}
|
||||
logger.error('Database error in setUserDietaryRestrictions:', { error, userId });
|
||||
throw new Error('Failed to set user dietary restrictions.');
|
||||
await client.query('ROLLBACK');
|
||||
if (error instanceof Error && 'code' in error && error.code === '23503') {
|
||||
throw new ForeignKeyConstraintError('One or more of the specified restriction IDs are invalid.');
|
||||
}
|
||||
logger.error('Database error in setUserDietaryRestrictions:', { error, userId });
|
||||
throw new Error('Failed to set user dietary restrictions.');
|
||||
} finally {
|
||||
client.release();
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the master list of all available kitchen appliances.
|
||||
* @returns A promise that resolves to an array of Appliance objects.
|
||||
*/
|
||||
export async function getAppliances(): Promise<Appliance[]> {
|
||||
/**
|
||||
* Sets the kitchen appliances for a user, replacing any existing ones.
|
||||
* @param userId The ID of the user.
|
||||
* @param applianceIds An array of IDs for the selected appliances.
|
||||
* @returns A promise that resolves when the operation is complete.
|
||||
*/
|
||||
async setUserAppliances(userId: string, applianceIds: number[]): Promise<UserAppliance[]> {
|
||||
const client = await (this.db as Pool).connect();
|
||||
try {
|
||||
const res = await getPool().query<Appliance>('SELECT * FROM public.appliances ORDER BY name');
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getAppliances:', { error });
|
||||
throw new Error('Failed to get appliances.');
|
||||
}
|
||||
}
|
||||
await client.query('BEGIN'); // Start transaction
|
||||
|
||||
/**
|
||||
* Retrieves the kitchen appliances for a specific user.
|
||||
* @param userId The ID of the user.
|
||||
* @returns A promise that resolves to an array of the user's selected Appliance objects.
|
||||
*/
|
||||
export async function getUserAppliances(userId: string): Promise<Appliance[]> {
|
||||
try {
|
||||
const query = `
|
||||
// 1. Clear existing appliances for the user
|
||||
await client.query('DELETE FROM public.user_appliances WHERE user_id = $1', [userId]);
|
||||
|
||||
let newAppliances: UserAppliance[] = [];
|
||||
// 2. Insert new ones if any are provided
|
||||
if (applianceIds.length > 0) {
|
||||
const insertQuery = `INSERT INTO public.user_appliances (user_id, appliance_id) SELECT $1, unnest($2::int[]) RETURNING *`;
|
||||
const res = await client.query<UserAppliance>(insertQuery, [userId, applianceIds]);
|
||||
newAppliances = res.rows;
|
||||
}
|
||||
|
||||
// 3. Commit the transaction
|
||||
await client.query('COMMIT');
|
||||
return newAppliances;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Database error in setUserAppliances:', { error, userId });
|
||||
throw new Error('Failed to set user appliances.');
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the kitchen appliances for a specific user.
|
||||
* @param userId The ID of the user.
|
||||
* @returns A promise that resolves to an array of the user's selected Appliance objects.
|
||||
*/
|
||||
async getUserAppliances(userId: string): Promise<Appliance[]> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT a.* FROM public.appliances a
|
||||
JOIN public.user_appliances ua ON a.appliance_id = ua.appliance_id
|
||||
WHERE ua.user_id = $1 ORDER BY a.name;
|
||||
`;
|
||||
const res = await getPool().query<Appliance>(query, [userId]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getUserAppliances:', { error, userId });
|
||||
throw new Error('Failed to get user appliances.');
|
||||
const res = await this.db.query<Appliance>(query, [userId]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getUserAppliances:', { error, userId });
|
||||
throw new Error('Failed to get user appliances.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the kitchen appliances for a user, replacing any existing ones.
|
||||
* @param userId The ID of the user.
|
||||
* @param applianceIds An array of IDs for the selected appliances.
|
||||
* @returns A promise that resolves when the operation is complete.
|
||||
*/
|
||||
export async function setUserAppliances(userId: string, applianceIds: number[]): Promise<UserAppliance[]> {
|
||||
const client = await getPool().connect();
|
||||
try {
|
||||
await client.query('BEGIN'); // Start transaction
|
||||
/**
|
||||
* Calls a database function to find recipes that can be made from a user's pantry.
|
||||
* @param userId The ID of the user.
|
||||
* @returns A promise that resolves to an array of recipes.
|
||||
*/
|
||||
async findRecipesFromPantry(userId: string): Promise<PantryRecipe[]> {
|
||||
try {
|
||||
const res = await this.db.query<PantryRecipe>('SELECT * FROM public.find_recipes_from_pantry($1)', [userId]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in findRecipesFromPantry:', { error, userId });
|
||||
throw new Error('Failed to find recipes from pantry.');
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Clear existing appliances for the user
|
||||
await client.query('DELETE FROM public.user_appliances WHERE user_id = $1', [userId]);
|
||||
|
||||
let newAppliances: UserAppliance[] = [];
|
||||
// 2. Insert new ones if any are provided
|
||||
if (applianceIds.length > 0) {
|
||||
const insertQuery = `INSERT INTO public.user_appliances (user_id, appliance_id) SELECT $1, unnest($2::int[]) RETURNING *`;
|
||||
const res = await client.query<UserAppliance>(insertQuery, [userId, applianceIds]);
|
||||
newAppliances = res.rows;
|
||||
}
|
||||
|
||||
// 3. Commit the transaction
|
||||
await client.query('COMMIT');
|
||||
return newAppliances;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
if (error instanceof Error && 'code' in error && error.code === '23503') {
|
||||
throw new ForeignKeyConstraintError('One or more of the specified appliance IDs are invalid.');
|
||||
}
|
||||
logger.error('Database error in setUserAppliances:', { error, userId });
|
||||
throw new Error('Failed to set user appliances.');
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Calls a database function to recommend recipes for a user.
|
||||
* @param userId The ID of the user.
|
||||
* @param limit The maximum number of recipes to recommend.
|
||||
* @returns A promise that resolves to an array of recommended recipes.
|
||||
*/
|
||||
async recommendRecipesForUser(userId: string, limit: number): Promise<RecommendedRecipe[]> {
|
||||
try {
|
||||
const res = await this.db.query<RecommendedRecipe>('SELECT * FROM public.recommend_recipes_for_user($1, $2)', [userId, limit]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in recommendRecipesForUser:', { error, userId, limit });
|
||||
throw new Error('Failed to recommend recipes.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a database function to get recipes that are compatible with a user's dietary restrictions.
|
||||
* @param userId The ID of the user.
|
||||
* @returns A promise that resolves to an array of compatible Recipe objects.
|
||||
*/
|
||||
export async function getRecipesForUserDiets(userId: string): Promise<Recipe[]> {
|
||||
try {
|
||||
const res = await getPool().query<Recipe>('SELECT * FROM public.get_recipes_for_user_diets($1)', [userId]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getRecipesForUserDiets:', { error, userId });
|
||||
throw new Error('Failed to get recipes compatible with user diet.');
|
||||
}
|
||||
/**
|
||||
* Calls a database function to get the best current sale prices for a user's watched items.
|
||||
* @param userId The ID of the user.
|
||||
* @returns A promise that resolves to an array of the best deals.
|
||||
*/
|
||||
async getBestSalePricesForUser(userId: string): Promise<WatchedItemDeal[]> {
|
||||
try {
|
||||
const res = await this.db.query<WatchedItemDeal>('SELECT * FROM public.get_best_sale_prices_for_user($1)', [userId]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getBestSalePricesForUser:', { error, userId });
|
||||
throw new Error('Failed to get best sale prices.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a database function to suggest unit conversions for a pantry item.
|
||||
* @param pantryItemId The ID of the pantry item.
|
||||
* @returns A promise that resolves to an array of suggested conversions.
|
||||
*/
|
||||
async suggestPantryItemConversions(pantryItemId: number): Promise<PantryItemConversion[]> {
|
||||
try {
|
||||
const res = await this.db.query<PantryItemConversion>('SELECT * FROM public.suggest_pantry_item_conversions($1)', [pantryItemId]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in suggestPantryItemConversions:', { error, pantryItemId });
|
||||
throw new Error('Failed to suggest pantry item conversions.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a database function to get recipes that are compatible with a user's dietary restrictions.
|
||||
* @param userId The ID of the user.
|
||||
* @returns A promise that resolves to an array of compatible Recipe objects.
|
||||
*/
|
||||
async getRecipesForUserDiets(userId: string): Promise<Recipe[]> {
|
||||
try {
|
||||
const res = await this.db.query<Recipe>('SELECT * FROM public.get_recipes_for_user_diets($1)', [userId]); // This is a standalone function, no change needed here.
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getRecipesForUserDiets:', { error, userId });
|
||||
throw new Error('Failed to get recipes compatible with user diet.');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user