Files
flyer-crawler.projectium.com/src/services/db/flyerLocation.db.ts
Torben Sorensen cf476e7afc
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 18m47s
ADR-022 - websocket notificaitons - also more test fixes with stores
2026-01-19 10:53:42 -08:00

210 lines
6.1 KiB
TypeScript

// src/services/db/flyerLocation.db.ts
/**
* Repository for managing flyer_locations (many-to-many relationship between flyers and store_locations).
*/
import type { Logger } from 'pino';
import type { PoolClient, Pool } from 'pg';
import { handleDbError } from './errors.db';
import type { FlyerLocation } from '../../types';
import { getPool } from './connection.db';
export class FlyerLocationRepository {
private db: Pool | PoolClient;
constructor(dbClient?: PoolClient) {
this.db = dbClient || getPool();
}
/**
* Links a flyer to one or more store locations.
* @param flyerId The ID of the flyer
* @param storeLocationIds Array of store_location_ids to associate with this flyer
* @param logger Logger instance
* @returns Promise that resolves when all links are created
*/
async linkFlyerToLocations(
flyerId: number,
storeLocationIds: number[],
logger: Logger,
): Promise<void> {
try {
if (storeLocationIds.length === 0) {
logger.warn({ flyerId }, 'No store locations provided for flyer linkage');
return;
}
// Use VALUES with multiple rows for efficient bulk insert
const values = storeLocationIds.map((_, index) => `($1, $${index + 2})`).join(', ');
const query = `
INSERT INTO public.flyer_locations (flyer_id, store_location_id)
VALUES ${values}
ON CONFLICT (flyer_id, store_location_id) DO NOTHING
`;
await this.db.query(query, [flyerId, ...storeLocationIds]);
logger.info(
{ flyerId, locationCount: storeLocationIds.length },
'Linked flyer to store locations',
);
} catch (error) {
handleDbError(
error,
logger,
'Database error in linkFlyerToLocations',
{ flyerId, storeLocationIds },
{
defaultMessage: 'Failed to link flyer to store locations.',
},
);
}
}
/**
* Links a flyer to all locations of a given store.
* This is a convenience method for the common case where a flyer is valid at all store locations.
* @param flyerId The ID of the flyer
* @param storeId The ID of the store
* @param logger Logger instance
* @returns Promise that resolves to the number of locations linked
*/
async linkFlyerToAllStoreLocations(
flyerId: number,
storeId: number,
logger: Logger,
): Promise<number> {
try {
const query = `
INSERT INTO public.flyer_locations (flyer_id, store_location_id)
SELECT $1, store_location_id
FROM public.store_locations
WHERE store_id = $2
ON CONFLICT (flyer_id, store_location_id) DO NOTHING
RETURNING store_location_id
`;
const res = await this.db.query(query, [flyerId, storeId]);
const linkedCount = res.rowCount || 0;
logger.info({ flyerId, storeId, linkedCount }, 'Linked flyer to all store locations');
return linkedCount;
} catch (error) {
handleDbError(
error,
logger,
'Database error in linkFlyerToAllStoreLocations',
{ flyerId, storeId },
{
defaultMessage: 'Failed to link flyer to all store locations.',
},
);
}
}
/**
* Removes all location links for a flyer.
* @param flyerId The ID of the flyer
* @param logger Logger instance
*/
async unlinkAllLocations(flyerId: number, logger: Logger): Promise<void> {
try {
await this.db.query('DELETE FROM public.flyer_locations WHERE flyer_id = $1', [flyerId]);
logger.info({ flyerId }, 'Unlinked all locations from flyer');
} catch (error) {
handleDbError(
error,
logger,
'Database error in unlinkAllLocations',
{ flyerId },
{
defaultMessage: 'Failed to unlink locations from flyer.',
},
);
}
}
/**
* Removes a specific location link from a flyer.
* @param flyerId The ID of the flyer
* @param storeLocationId The ID of the store location to unlink
* @param logger Logger instance
*/
async unlinkLocation(flyerId: number, storeLocationId: number, logger: Logger): Promise<void> {
try {
await this.db.query(
'DELETE FROM public.flyer_locations WHERE flyer_id = $1 AND store_location_id = $2',
[flyerId, storeLocationId],
);
logger.info({ flyerId, storeLocationId }, 'Unlinked location from flyer');
} catch (error) {
handleDbError(
error,
logger,
'Database error in unlinkLocation',
{ flyerId, storeLocationId },
{
defaultMessage: 'Failed to unlink location from flyer.',
},
);
}
}
/**
* Gets all location IDs associated with a flyer.
* @param flyerId The ID of the flyer
* @param logger Logger instance
* @returns Promise that resolves to an array of store_location_ids
*/
async getLocationIdsByFlyerId(flyerId: number, logger: Logger): Promise<number[]> {
try {
const res = await this.db.query<{ store_location_id: number }>(
'SELECT store_location_id FROM public.flyer_locations WHERE flyer_id = $1',
[flyerId],
);
return res.rows.map((row) => row.store_location_id);
} catch (error) {
handleDbError(
error,
logger,
'Database error in getLocationIdsByFlyerId',
{ flyerId },
{
defaultMessage: 'Failed to get location IDs for flyer.',
},
);
}
}
/**
* Gets all flyer_location records for a flyer.
* @param flyerId The ID of the flyer
* @param logger Logger instance
* @returns Promise that resolves to an array of FlyerLocation objects
*/
async getFlyerLocationsByFlyerId(flyerId: number, logger: Logger): Promise<FlyerLocation[]> {
try {
const res = await this.db.query<FlyerLocation>(
'SELECT * FROM public.flyer_locations WHERE flyer_id = $1',
[flyerId],
);
return res.rows;
} catch (error) {
handleDbError(
error,
logger,
'Database error in getFlyerLocationsByFlyerId',
{ flyerId },
{
defaultMessage: 'Failed to get flyer locations.',
},
);
}
}
}