Files
flyer-crawler.projectium.com/src/controllers/flyer.controller.ts
Torben Sorensen 2d2cd52011
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 3m58s
Massive Dependency Modernization Project
2026-02-13 00:34:22 -08:00

283 lines
9.7 KiB
TypeScript

// src/controllers/flyer.controller.ts
// ============================================================================
// FLYER CONTROLLER
// ============================================================================
// Provides endpoints for managing flyers and flyer items.
// Implements endpoints for:
// - Listing flyers with pagination
// - Getting individual flyer details
// - Getting items for a flyer
// - Batch fetching items for multiple flyers
// - Batch counting items for multiple flyers
// - Tracking item interactions (fire-and-forget)
// ============================================================================
import {
Get,
Post,
Route,
Tags,
Path,
Query,
Body,
Request,
SuccessResponse,
Response,
} from 'tsoa';
import type { Request as ExpressRequest } from 'express';
import { BaseController } from './base.controller';
import type { SuccessResponse as SuccessResponseType, ErrorResponse } from './types';
import * as db from '../services/db/index.db';
import type { FlyerDto, FlyerItemDto } from '../dtos/common.dto';
// ============================================================================
// REQUEST/RESPONSE TYPES
// ============================================================================
// Types for request bodies and custom response shapes that will appear in
// the OpenAPI specification.
// ============================================================================
/**
* Request body for batch fetching flyer items.
*/
interface BatchFetchRequest {
/**
* Array of flyer IDs to fetch items for.
* @minItems 1
* @example [1, 2, 3]
*/
flyerIds: number[];
}
/**
* Request body for batch counting flyer items.
*/
interface BatchCountRequest {
/**
* Array of flyer IDs to count items for.
* @example [1, 2, 3]
*/
flyerIds: number[];
}
/**
* Request body for tracking item interactions.
*/
interface TrackInteractionRequest {
/**
* Type of interaction to track.
* @example "view"
*/
type: 'view' | 'click';
}
/**
* Response for batch item count.
*/
interface BatchCountResponse {
/**
* Total number of items across all requested flyers.
*/
count: number;
}
/**
* Response for tracking confirmation.
*/
interface TrackingResponse {
/**
* Confirmation message.
*/
message: string;
}
// ============================================================================
// FLYER CONTROLLER
// ============================================================================
/**
* Controller for flyer management endpoints.
*
* Provides read-only access to flyers and flyer items for all users,
* with analytics tracking capabilities.
*/
@Route('flyers')
@Tags('Flyers')
export class FlyerController extends BaseController {
// ==========================================================================
// LIST ENDPOINTS
// ==========================================================================
/**
* Get all flyers.
*
* Returns a paginated list of all flyers, ordered by creation date (newest first).
* Includes store information and location data for each flyer.
*
* @summary List all flyers
* @param limit Maximum number of flyers to return (default: 20)
* @param offset Number of flyers to skip for pagination (default: 0)
* @returns Array of flyer objects with store information
*/
@Get()
@SuccessResponse(200, 'List of flyers retrieved successfully')
public async getFlyers(
@Request() req: ExpressRequest,
@Query() limit?: number,
@Query() offset?: number,
): Promise<SuccessResponseType<FlyerDto[]>> {
// Apply defaults and bounds for pagination
// Note: Using offset-based pagination to match existing API behavior
const normalizedLimit = Math.min(100, Math.max(1, Math.floor(limit ?? 20)));
const normalizedOffset = Math.max(0, Math.floor(offset ?? 0));
const flyers = await db.flyerRepo.getFlyers(req.log, normalizedLimit, normalizedOffset);
// The Flyer type from the repository is structurally compatible with FlyerDto
// (FlyerDto just omits the GeoJSONPoint type that tsoa can't handle)
return this.success(flyers as unknown as FlyerDto[]);
}
// ==========================================================================
// SINGLE RESOURCE ENDPOINTS
// ==========================================================================
/**
* Get flyer by ID.
*
* Returns a single flyer with its full details, including store information
* and all associated store locations.
*
* @summary Get a single flyer
* @param id The unique identifier of the flyer
* @returns The flyer object with full details
*/
@Get('{id}')
@SuccessResponse(200, 'Flyer retrieved successfully')
@Response<ErrorResponse>(404, 'Flyer not found')
public async getFlyerById(
@Path() id: number,
@Request() req: ExpressRequest,
): Promise<SuccessResponseType<FlyerDto>> {
// getFlyerById throws NotFoundError if flyer doesn't exist
// The global error handler converts this to a 404 response
const flyer = await db.flyerRepo.getFlyerById(id);
req.log.debug({ flyerId: id }, 'Retrieved flyer by ID');
return this.success(flyer as unknown as FlyerDto);
}
/**
* Get flyer items.
*
* Returns all items (deals) associated with a specific flyer.
* Items are ordered by their position in the flyer.
*
* @summary Get items for a flyer
* @param id The unique identifier of the flyer
* @returns Array of flyer items with pricing and category information
*/
@Get('{id}/items')
@SuccessResponse(200, 'Flyer items retrieved successfully')
@Response<ErrorResponse>(404, 'Flyer not found')
public async getFlyerItems(
@Path() id: number,
@Request() req: ExpressRequest,
): Promise<SuccessResponseType<FlyerItemDto[]>> {
const items = await db.flyerRepo.getFlyerItems(id, req.log);
req.log.debug({ flyerId: id, itemCount: items.length }, 'Retrieved flyer items');
return this.success(items as unknown as FlyerItemDto[]);
}
// ==========================================================================
// BATCH ENDPOINTS
// ==========================================================================
/**
* Batch fetch flyer items.
*
* Returns all items for multiple flyers in a single request.
* This is more efficient than making separate requests for each flyer.
* Items are ordered by flyer ID, then by item position within each flyer.
*
* @summary Batch fetch items for multiple flyers
* @param body Request body containing array of flyer IDs
* @returns Array of all flyer items for the requested flyers
*/
@Post('items/batch-fetch')
@SuccessResponse(200, 'Batch items retrieved successfully')
public async batchFetchItems(
@Body() body: BatchFetchRequest,
@Request() req: ExpressRequest,
): Promise<SuccessResponseType<FlyerItemDto[]>> {
const items = await db.flyerRepo.getFlyerItemsForFlyers(body.flyerIds, req.log);
req.log.debug(
{ flyerCount: body.flyerIds.length, itemCount: items.length },
'Batch fetched flyer items',
);
return this.success(items as unknown as FlyerItemDto[]);
}
/**
* Batch count flyer items.
*
* Returns the total item count for multiple flyers.
* Useful for displaying item counts without fetching all item data.
*
* @summary Batch count items for multiple flyers
* @param body Request body containing array of flyer IDs
* @returns Object with total count of items across all requested flyers
*/
@Post('items/batch-count')
@SuccessResponse(200, 'Batch count retrieved successfully')
public async batchCountItems(
@Body() body: BatchCountRequest,
@Request() req: ExpressRequest,
): Promise<SuccessResponseType<BatchCountResponse>> {
const count = await db.flyerRepo.countFlyerItemsForFlyers(body.flyerIds, req.log);
req.log.debug({ flyerCount: body.flyerIds.length, totalItems: count }, 'Batch counted items');
return this.success({ count });
}
// ==========================================================================
// TRACKING ENDPOINTS
// ==========================================================================
/**
* Track item interaction.
*
* Records a view or click interaction with a flyer item for analytics purposes.
* This endpoint uses a fire-and-forget pattern: it returns immediately with a
* 202 Accepted response while the tracking is processed asynchronously.
*
* This design ensures that tracking does not slow down the user experience,
* and any tracking failures are logged but do not affect the client.
*
* @summary Track a flyer item interaction
* @param itemId The unique identifier of the flyer item
* @param body The interaction type (view or click)
* @returns Confirmation that tracking was accepted
*/
@Post('items/{itemId}/track')
@SuccessResponse(202, 'Tracking accepted')
public async trackItemInteraction(
@Path() itemId: number,
@Body() body: TrackInteractionRequest,
@Request() req: ExpressRequest,
): Promise<SuccessResponseType<TrackingResponse>> {
// Fire-and-forget: start the tracking operation but don't await it.
// We explicitly handle errors in the .catch() to prevent unhandled rejections
// and to log any failures without affecting the client response.
db.flyerRepo.trackFlyerItemInteraction(itemId, body.type, req.log).catch((error) => {
// Log the error but don't propagate it - this is intentional
// as tracking failures should not impact user experience
req.log.error(
{ error, itemId, interactionType: body.type },
'Flyer item interaction tracking failed (fire-and-forget)',
);
});
// Return immediately with 202 Accepted
this.setStatus(202);
return this.success({ message: 'Tracking accepted' });
}
}