more ts and break apart big ass files
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Has been cancelled
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Has been cancelled
This commit is contained in:
313
src/services/db/flyer.ts
Normal file
313
src/services/db/flyer.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import { pool } from './connection';
|
||||
import { logger } from '../logger';
|
||||
import { Flyer, Brand, MasterGroceryItem, FlyerItem } from '../../types';
|
||||
|
||||
/**
|
||||
* Retrieves all flyers from the database, joining with store information.
|
||||
* @returns A promise that resolves to an array of Flyer objects.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export async function getFlyers(): Promise<Flyer[]> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
f.id,
|
||||
f.created_at,
|
||||
f.file_name,
|
||||
f.image_url,
|
||||
f.checksum,
|
||||
f.store_id,
|
||||
f.valid_from,
|
||||
f.valid_to,
|
||||
f.store_address,
|
||||
json_build_object(
|
||||
'id', s.id,
|
||||
'name', s.name,
|
||||
'logo_url', s.logo_url
|
||||
) as store
|
||||
FROM public.flyers f
|
||||
LEFT JOIN public.stores s ON f.store_id = s.id
|
||||
ORDER BY f.valid_to DESC, s.name ASC;
|
||||
`;
|
||||
const res = await pool.query<Flyer>(query);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getFlyers:', { error });
|
||||
throw new Error('Failed to retrieve flyers from database.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all brands from the database, including the associated store name for store brands.
|
||||
* @returns A promise that resolves to an array of Brand objects.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export async function getAllBrands(): Promise<Brand[]> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT b.id, b.name, b.logo_url, b.store_id, s.name as store_name
|
||||
FROM public.brands b
|
||||
LEFT JOIN public.stores s ON b.store_id = s.id
|
||||
ORDER BY b.name ASC;
|
||||
`;
|
||||
const res = await pool.query<Brand>(query);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getAllBrands:', { error });
|
||||
throw new Error('Failed to retrieve brands from database.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all master grocery items from the database, joining with category information.
|
||||
* @returns A promise that resolves to an array of MasterGroceryItem objects.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export async function getAllMasterItems(): Promise<MasterGroceryItem[]> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
m.id,
|
||||
m.created_at,
|
||||
m.name,
|
||||
m.category_id,
|
||||
c.name as category_name
|
||||
FROM public.master_grocery_items m
|
||||
LEFT JOIN public.categories c ON m.category_id = c.id
|
||||
ORDER BY m.name ASC;
|
||||
`;
|
||||
const res = await pool.query<MasterGroceryItem>(query);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getAllMasterItems:', { error });
|
||||
throw new Error('Failed to retrieve master items from database.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all categories from the database.
|
||||
* @returns A promise that resolves to an array of Category objects.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export async function getAllCategories(): Promise<{id: number, name: string}[]> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT id, name FROM public.categories ORDER BY name ASC;
|
||||
`;
|
||||
const res = await pool.query<{id: number, name: string}>(query);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getAllCategories:', { error });
|
||||
throw new Error('Failed to retrieve categories from database.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a flyer by its checksum to prevent duplicate processing.
|
||||
* @param checksum The SHA-256 checksum of the flyer file.
|
||||
* @returns A promise that resolves to the Flyer object if found, otherwise undefined.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export async function findFlyerByChecksum(checksum: string): Promise<Flyer | undefined> {
|
||||
try {
|
||||
const res = await pool.query<Flyer>('SELECT * FROM public.flyers WHERE checksum = $1', [checksum]);
|
||||
return res.rows[0];
|
||||
} catch (error) {
|
||||
logger.error('Database error in findFlyerByChecksum:', { error });
|
||||
throw new Error('Failed to check for existing flyer.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new flyer and all its associated items in a single database transaction.
|
||||
* @param flyerData The metadata for the flyer.
|
||||
* @param items The array of flyer items extracted from the flyer.
|
||||
* @returns A promise that resolves to the newly created Flyer object.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export async function createFlyerAndItems(
|
||||
flyerData: Omit<Flyer, 'id' | 'created_at' | 'store'> & { store_name: string },
|
||||
items: Omit<FlyerItem, 'id' | 'flyer_id' | 'created_at'>[]
|
||||
): Promise<Flyer> {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Find or create the store
|
||||
let storeId: number;
|
||||
const storeRes = await client.query<{ id: number }>('SELECT id FROM public.stores WHERE name = $1', [flyerData.store_name]);
|
||||
if (storeRes.rows.length > 0) {
|
||||
storeId = storeRes.rows[0].id;
|
||||
} else {
|
||||
const newStoreRes = await client.query<{ id: number }>('INSERT INTO public.stores (name) VALUES ($1) RETURNING id', [flyerData.store_name]);
|
||||
storeId = newStoreRes.rows[0].id;
|
||||
}
|
||||
|
||||
// Create the flyer record
|
||||
const flyerQuery = `
|
||||
INSERT INTO public.flyers (file_name, image_url, checksum, store_id, valid_from, valid_to, store_address)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *;
|
||||
`;
|
||||
const flyerValues = [flyerData.file_name, flyerData.image_url, flyerData.checksum, storeId, flyerData.valid_from, flyerData.valid_to, flyerData.store_address];
|
||||
const newFlyerRes = await client.query<Flyer>(flyerQuery, flyerValues);
|
||||
const newFlyer = newFlyerRes.rows[0];
|
||||
|
||||
// Prepare and insert all flyer items
|
||||
if (items.length > 0) {
|
||||
const itemInsertQuery = `
|
||||
INSERT INTO public.flyer_items (
|
||||
flyer_id, item, price_display, price_in_cents, quantity,
|
||||
master_item_id, -- This will be populated by our suggestion function
|
||||
category_name, unit_price
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, public.suggest_master_item_for_flyer_item($2), $6, $7)
|
||||
`;
|
||||
|
||||
// Loop through each item and execute the insert query.
|
||||
// The query now directly calls the `suggest_master_item_for_flyer_item` function
|
||||
// on the database side, passing the item name (`item.item`) as the argument.
|
||||
// This is more efficient than making a separate DB call for each item to get the suggestion.
|
||||
for (const item of items) {
|
||||
const itemValues = [
|
||||
newFlyer.id,
|
||||
item.item,
|
||||
item.price_display,
|
||||
item.price_in_cents,
|
||||
item.quantity,
|
||||
item.category_name,
|
||||
item.unit_price ? JSON.stringify(item.unit_price) : null // Ensure JSONB is correctly stringified
|
||||
];
|
||||
await client.query(itemInsertQuery, itemValues);
|
||||
}
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
return newFlyer;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Database transaction error in createFlyerAndItems:', { error });
|
||||
throw new Error('Failed to save flyer and its items.');
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all items for a specific flyer.
|
||||
* @param flyerId The ID of the flyer.
|
||||
* @returns A promise that resolves to an array of FlyerItem objects.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export async function getFlyerItems(flyerId: number): Promise<FlyerItem[]> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT * FROM public.flyer_items
|
||||
WHERE flyer_id = $1
|
||||
ORDER BY id ASC;
|
||||
`;
|
||||
const res = await pool.query<FlyerItem>(query, [flyerId]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getFlyerItems:', { error, flyerId });
|
||||
throw new Error('Failed to retrieve flyer items.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all flyer items for a given list of flyer IDs.
|
||||
* @param flyerIds An array of flyer IDs.
|
||||
* @returns A promise that resolves to an array of FlyerItem objects.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export async function getFlyerItemsForFlyers(flyerIds: number[]): Promise<FlyerItem[]> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT * FROM public.flyer_items
|
||||
WHERE flyer_id = ANY($1::bigint[]);
|
||||
`;
|
||||
const res = await pool.query<FlyerItem>(query, [flyerIds]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getFlyerItemsForFlyers:', { error });
|
||||
throw new Error('Failed to retrieve items for multiple flyers.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the total number of flyer items for a given list of flyer IDs.
|
||||
* @param flyerIds An array of flyer IDs.
|
||||
* @returns A promise that resolves to the total count of items.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export async function countFlyerItemsForFlyers(flyerIds: number[]): Promise<number> {
|
||||
try {
|
||||
const query = `SELECT COUNT(*) FROM public.flyer_items WHERE flyer_id = ANY($1::bigint[])`;
|
||||
const res = await pool.query<{ count: string }>(query, [flyerIds]);
|
||||
return parseInt(res.rows[0].count, 10);
|
||||
} catch (error) {
|
||||
logger.error('Database error in countFlyerItemsForFlyers:', { error });
|
||||
throw new Error('Failed to count items for multiple flyers.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the logo URL for a specific store.
|
||||
* @param storeId The ID of the store to update.
|
||||
* @param logoUrl The new URL for the store's logo.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export async function updateStoreLogo(storeId: number, logoUrl: string): Promise<void> {
|
||||
try {
|
||||
await pool.query(
|
||||
'UPDATE public.stores SET logo_url = $1 WHERE id = $2',
|
||||
[logoUrl, storeId]
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Database error in updateStoreLogo:', { error, storeId });
|
||||
throw new Error('Failed to update store logo in database.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks a user interaction with a flyer item (view or click).
|
||||
* @param itemId The ID of the flyer item.
|
||||
* @param type The type of interaction ('view' or 'click').
|
||||
*/
|
||||
export async function trackFlyerItemInteraction(itemId: number, type: 'view' | 'click'): Promise<void> {
|
||||
try {
|
||||
const column = type === 'view' ? 'view_count' : 'click_count';
|
||||
// Use the || operator to concatenate the column name safely into the query.
|
||||
const query = `UPDATE public.flyer_items SET ${column} = ${column} + 1 WHERE id = $1`;
|
||||
await pool.query(query, [itemId]);
|
||||
} catch (error) {
|
||||
logger.error('Database error in trackFlyerItemInteraction:', { error, itemId, type });
|
||||
// This is a non-critical operation, so we don't throw an error that would crash the user's request.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves historical price data for a given list of master item IDs.
|
||||
* This function queries the pre-aggregated `item_price_history` table for efficiency.
|
||||
* @param masterItemIds An array of master grocery item IDs.
|
||||
* @returns A promise that resolves to an array of historical price records.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export async function getHistoricalPriceDataForItems(masterItemIds: number[]): Promise<{ master_item_id: number; summary_date: string; avg_price_in_cents: number | null; }[]> {
|
||||
if (masterItemIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const query = `
|
||||
SELECT master_item_id, summary_date, avg_price_in_cents
|
||||
FROM public.item_price_history
|
||||
WHERE master_item_id = ANY($1::bigint[])
|
||||
ORDER BY summary_date ASC;
|
||||
`;
|
||||
const res = await pool.query(query, [masterItemIds]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error('Database error in getHistoricalPriceDataForItems:', { error });
|
||||
throw new Error('Failed to retrieve historical price data.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user