All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 17m56s
479 lines
15 KiB
TypeScript
479 lines
15 KiB
TypeScript
// src/routes/versioned.ts
|
|
/**
|
|
* @file Version Router Factory - ADR-008 Phase 2 Implementation
|
|
*
|
|
* Creates version-specific Express routers that manage route registration
|
|
* for different API versions. This factory ensures consistent middleware
|
|
* application and proper route ordering across all API versions.
|
|
*
|
|
* Key responsibilities:
|
|
* - Create routers for each supported API version
|
|
* - Apply version detection and deprecation middleware
|
|
* - Register domain routers in correct precedence order
|
|
* - Support version-specific route availability
|
|
* - Add X-API-Version header to all responses
|
|
*
|
|
* @see docs/architecture/api-versioning-infrastructure.md
|
|
* @see docs/adr/0008-api-versioning-strategy.md
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // In server.ts:
|
|
* import { createApiRouter, createVersionedRouter } from './src/routes/versioned';
|
|
*
|
|
* // Option 1: Mount all versions at once
|
|
* app.use('/api', createApiRouter());
|
|
*
|
|
* // Option 2: Mount versions individually
|
|
* app.use('/api/v1', createVersionedRouter('v1'));
|
|
* app.use('/api/v2', createVersionedRouter('v2'));
|
|
* ```
|
|
*/
|
|
|
|
import { Router } from 'express';
|
|
import { ApiVersion, API_VERSIONS, SUPPORTED_VERSIONS } from '../config/apiVersions';
|
|
import { detectApiVersion } from '../middleware/apiVersion.middleware';
|
|
import { addDeprecationHeaders } from '../middleware/deprecation.middleware';
|
|
import { createScopedLogger } from '../services/logger.server';
|
|
|
|
// --- Domain Router Imports ---
|
|
// These are imported in the order they are registered in server.ts
|
|
import authRouter from './auth.routes';
|
|
import healthRouter from './health.routes';
|
|
import systemRouter from './system.routes';
|
|
import userRouter from './user.routes';
|
|
import aiRouter from './ai.routes';
|
|
import adminRouter from './admin.routes';
|
|
import budgetRouter from './budget.routes';
|
|
import gamificationRouter from './gamification.routes';
|
|
import flyerRouter from './flyer.routes';
|
|
import recipeRouter from './recipe.routes';
|
|
import personalizationRouter from './personalization.routes';
|
|
import priceRouter from './price.routes';
|
|
import statsRouter from './stats.routes';
|
|
import upcRouter from './upc.routes';
|
|
import inventoryRouter from './inventory.routes';
|
|
import receiptRouter from './receipt.routes';
|
|
import dealsRouter from './deals.routes';
|
|
import reactionsRouter from './reactions.routes';
|
|
import storeRouter from './store.routes';
|
|
import categoryRouter from './category.routes';
|
|
|
|
// Module-scoped logger for versioned router operations
|
|
const versionedRouterLogger = createScopedLogger('versioned-router');
|
|
|
|
// --- Type Definitions ---
|
|
|
|
/**
|
|
* Configuration for registering a route under versioned API.
|
|
*
|
|
* @property path - The URL path segment (e.g., 'auth', 'users', 'flyers')
|
|
* @property router - The Express router instance handling this path
|
|
* @property description - Human-readable description of the route's purpose
|
|
* @property versions - Optional array of versions where this route is available.
|
|
* If omitted, the route is available in all versions.
|
|
*/
|
|
export interface RouteRegistration {
|
|
/** URL path segment (mounted at /{version}/{path}) */
|
|
path: string;
|
|
/** Express router instance for this domain */
|
|
router: Router;
|
|
/** Human-readable description for documentation and logging */
|
|
description: string;
|
|
/** Optional: Specific versions where this route is available (defaults to all) */
|
|
versions?: ApiVersion[];
|
|
}
|
|
|
|
/**
|
|
* Options for creating a versioned router.
|
|
*/
|
|
export interface VersionedRouterOptions {
|
|
/** Whether to apply version detection middleware (default: true) */
|
|
applyVersionDetection?: boolean;
|
|
/** Whether to apply deprecation headers middleware (default: true) */
|
|
applyDeprecationHeaders?: boolean;
|
|
}
|
|
|
|
// --- Route Registration Configuration ---
|
|
|
|
/**
|
|
* Master list of all route registrations.
|
|
*
|
|
* IMPORTANT: The order of routes is critical for correct matching.
|
|
* More specific routes should be registered before more general ones.
|
|
* This order mirrors the registration in server.ts exactly.
|
|
*
|
|
* Each entry includes:
|
|
* - path: The URL segment (e.g., 'auth' -> /api/v1/auth)
|
|
* - router: The Express router handling the routes
|
|
* - description: Purpose documentation
|
|
* - versions: Optional array to restrict availability to specific versions
|
|
*/
|
|
export const ROUTES: RouteRegistration[] = [
|
|
// 1. Authentication routes for login, registration, etc.
|
|
{
|
|
path: 'auth',
|
|
router: authRouter,
|
|
description: 'Authentication routes for login, registration, password reset',
|
|
},
|
|
|
|
// 2. Health check routes for monitoring and liveness probes
|
|
{
|
|
path: 'health',
|
|
router: healthRouter,
|
|
description: 'Health check endpoints for monitoring and liveness probes',
|
|
},
|
|
|
|
// 3. System routes for PM2 status, server info, etc.
|
|
{
|
|
path: 'system',
|
|
router: systemRouter,
|
|
description: 'System administration routes for PM2 status and server info',
|
|
},
|
|
|
|
// 4. General authenticated user routes
|
|
{
|
|
path: 'users',
|
|
router: userRouter,
|
|
description: 'User profile and account management routes',
|
|
},
|
|
|
|
// 5. AI routes, some of which use optional authentication
|
|
{
|
|
path: 'ai',
|
|
router: aiRouter,
|
|
description: 'AI-powered features including flyer processing and analysis',
|
|
},
|
|
|
|
// 6. Admin routes, protected by admin-level checks
|
|
{
|
|
path: 'admin',
|
|
router: adminRouter,
|
|
description: 'Administrative routes for user and system management',
|
|
},
|
|
|
|
// 7. Budgeting and spending analysis routes
|
|
{
|
|
path: 'budgets',
|
|
router: budgetRouter,
|
|
description: 'Budget management and spending analysis routes',
|
|
},
|
|
|
|
// 8. Gamification routes for achievements
|
|
{
|
|
path: 'achievements',
|
|
router: gamificationRouter,
|
|
description: 'Gamification and achievement system routes',
|
|
},
|
|
|
|
// 9. Public flyer routes
|
|
{
|
|
path: 'flyers',
|
|
router: flyerRouter,
|
|
description: 'Flyer listing, search, and item management routes',
|
|
},
|
|
|
|
// 10. Public recipe routes
|
|
{
|
|
path: 'recipes',
|
|
router: recipeRouter,
|
|
description: 'Recipe discovery, saving, and recommendation routes',
|
|
},
|
|
|
|
// 11. Public personalization data routes (master items, etc.)
|
|
{
|
|
path: 'personalization',
|
|
router: personalizationRouter,
|
|
description: 'Personalization data including master items and preferences',
|
|
},
|
|
|
|
// 12. Price history routes
|
|
{
|
|
path: 'price-history',
|
|
router: priceRouter,
|
|
description: 'Price history tracking and trend analysis routes',
|
|
},
|
|
|
|
// 13. Public statistics routes
|
|
{
|
|
path: 'stats',
|
|
router: statsRouter,
|
|
description: 'Public statistics and analytics routes',
|
|
},
|
|
|
|
// 14. UPC barcode scanning routes
|
|
{
|
|
path: 'upc',
|
|
router: upcRouter,
|
|
description: 'UPC barcode scanning and product lookup routes',
|
|
},
|
|
|
|
// 15. Inventory and expiry tracking routes
|
|
{
|
|
path: 'inventory',
|
|
router: inventoryRouter,
|
|
description: 'Inventory management and expiry tracking routes',
|
|
},
|
|
|
|
// 16. Receipt scanning routes
|
|
{
|
|
path: 'receipts',
|
|
router: receiptRouter,
|
|
description: 'Receipt scanning and purchase history routes',
|
|
},
|
|
|
|
// 17. Deals and best prices routes
|
|
{
|
|
path: 'deals',
|
|
router: dealsRouter,
|
|
description: 'Deal discovery and best price comparison routes',
|
|
},
|
|
|
|
// 18. Reactions/social features routes
|
|
{
|
|
path: 'reactions',
|
|
router: reactionsRouter,
|
|
description: 'Social features including reactions and sharing',
|
|
},
|
|
|
|
// 19. Store management routes
|
|
{
|
|
path: 'stores',
|
|
router: storeRouter,
|
|
description: 'Store discovery, favorites, and location routes',
|
|
},
|
|
|
|
// 20. Category discovery routes (ADR-023: Database Normalization)
|
|
{
|
|
path: 'categories',
|
|
router: categoryRouter,
|
|
description: 'Category browsing and product categorization routes',
|
|
},
|
|
];
|
|
|
|
// --- Factory Functions ---
|
|
|
|
/**
|
|
* Creates a versioned Express router for a specific API version.
|
|
*
|
|
* This factory function:
|
|
* 1. Creates a new Router instance with merged params
|
|
* 2. Applies deprecation headers middleware (adds X-API-Version header)
|
|
* 3. Registers all routes that are available for the specified version
|
|
* 4. Maintains correct route registration order (specific before general)
|
|
*
|
|
* @param version - The API version to create a router for (e.g., 'v1', 'v2')
|
|
* @param options - Optional configuration for middleware application
|
|
* @returns Configured Express Router for the specified version
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // Create a v1 router
|
|
* const v1Router = createVersionedRouter('v1');
|
|
* app.use('/api/v1', v1Router);
|
|
*
|
|
* // Create a v2 router with custom options
|
|
* const v2Router = createVersionedRouter('v2', {
|
|
* applyDeprecationHeaders: true,
|
|
* });
|
|
* ```
|
|
*/
|
|
export function createVersionedRouter(
|
|
version: ApiVersion,
|
|
options: VersionedRouterOptions = {},
|
|
): Router {
|
|
const { applyDeprecationHeaders: shouldApplyDeprecationHeaders = true } = options;
|
|
|
|
const router = Router({ mergeParams: true });
|
|
|
|
versionedRouterLogger.info({ version, routeCount: ROUTES.length }, 'Creating versioned router');
|
|
|
|
// Apply deprecation headers middleware.
|
|
// This adds X-API-Version header to all responses and deprecation headers
|
|
// when the version is marked as deprecated.
|
|
if (shouldApplyDeprecationHeaders) {
|
|
router.use(addDeprecationHeaders(version));
|
|
}
|
|
|
|
// Register all routes that are available for this version
|
|
let registeredCount = 0;
|
|
for (const route of ROUTES) {
|
|
// Check if this route is available for the specified version.
|
|
// If versions array is not specified, the route is available for all versions.
|
|
if (route.versions && !route.versions.includes(version)) {
|
|
versionedRouterLogger.debug(
|
|
{ version, path: route.path },
|
|
'Skipping route not available for this version',
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// Mount the router at the specified path
|
|
router.use(`/${route.path}`, route.router);
|
|
registeredCount++;
|
|
|
|
versionedRouterLogger.debug(
|
|
{ version, path: route.path, description: route.description },
|
|
'Registered route',
|
|
);
|
|
}
|
|
|
|
versionedRouterLogger.info(
|
|
{ version, registeredCount, totalRoutes: ROUTES.length },
|
|
'Versioned router created successfully',
|
|
);
|
|
|
|
return router;
|
|
}
|
|
|
|
/**
|
|
* Creates the main API router that mounts all versioned routers.
|
|
*
|
|
* This function creates a parent router that:
|
|
* 1. Applies version detection middleware at the /api/:version level
|
|
* 2. Mounts versioned routers for each supported API version
|
|
* 3. Returns 404 for unsupported versions via detectApiVersion middleware
|
|
*
|
|
* The router is designed to be mounted at `/api` in the main application:
|
|
* - `/api/v1/*` routes to v1 router
|
|
* - `/api/v2/*` routes to v2 router
|
|
* - `/api/v99/*` returns 404 (unsupported version)
|
|
*
|
|
* @returns Express Router configured with all version-specific sub-routers
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // In server.ts:
|
|
* import { createApiRouter } from './src/routes/versioned';
|
|
*
|
|
* // Mount at /api - handles /api/v1/*, /api/v2/*, etc.
|
|
* app.use('/api', createApiRouter());
|
|
*
|
|
* // Then add backwards compatibility redirect for unversioned paths:
|
|
* app.use('/api', (req, res, next) => {
|
|
* if (!req.path.startsWith('/v1') && !req.path.startsWith('/v2')) {
|
|
* return res.redirect(301, `/api/v1${req.path}`);
|
|
* }
|
|
* next();
|
|
* });
|
|
* ```
|
|
*/
|
|
export function createApiRouter(): Router {
|
|
const router = Router({ mergeParams: true });
|
|
|
|
versionedRouterLogger.info(
|
|
{ supportedVersions: SUPPORTED_VERSIONS },
|
|
'Creating API router with all versions',
|
|
);
|
|
|
|
// Mount versioned routers under /:version path.
|
|
// The detectApiVersion middleware validates the version and returns 404 for
|
|
// unsupported versions before the domain routers are reached.
|
|
router.use('/:version', detectApiVersion, (req, res, next) => {
|
|
// At this point, req.apiVersion is guaranteed to be valid
|
|
// (detectApiVersion returns 404 for invalid versions).
|
|
// Route to the appropriate versioned router based on the detected version.
|
|
const version = req.apiVersion;
|
|
|
|
if (!version) {
|
|
// This should not happen if detectApiVersion ran correctly,
|
|
// but handle it defensively.
|
|
return next('route');
|
|
}
|
|
|
|
// Get or create the versioned router.
|
|
// We use a cache to avoid recreating routers on every request.
|
|
const versionedRouter = versionedRouterCache.get(version);
|
|
if (versionedRouter) {
|
|
return versionedRouter(req, res, next);
|
|
}
|
|
|
|
// Fallback: version not in cache (should not happen with proper setup)
|
|
versionedRouterLogger.warn(
|
|
{ version },
|
|
'Versioned router not found in cache, creating on-demand',
|
|
);
|
|
const newRouter = createVersionedRouter(version);
|
|
versionedRouterCache.set(version, newRouter);
|
|
return newRouter(req, res, next);
|
|
});
|
|
|
|
versionedRouterLogger.info('API router created successfully');
|
|
|
|
return router;
|
|
}
|
|
|
|
// --- Router Cache ---
|
|
|
|
/**
|
|
* Cache for versioned routers to avoid recreation on every request.
|
|
* Pre-populated with routers for all supported versions.
|
|
*/
|
|
const versionedRouterCache = new Map<ApiVersion, Router>();
|
|
|
|
// Pre-populate the cache with all supported versions
|
|
for (const version of API_VERSIONS) {
|
|
versionedRouterCache.set(version, createVersionedRouter(version));
|
|
}
|
|
|
|
versionedRouterLogger.debug(
|
|
{ cachedVersions: Array.from(versionedRouterCache.keys()) },
|
|
'Versioned router cache initialized',
|
|
);
|
|
|
|
// --- Utility Functions ---
|
|
|
|
/**
|
|
* Gets the list of all registered route paths.
|
|
* Useful for documentation and debugging.
|
|
*
|
|
* @returns Array of registered route paths
|
|
*/
|
|
export function getRegisteredPaths(): string[] {
|
|
return ROUTES.map((route) => route.path);
|
|
}
|
|
|
|
/**
|
|
* Gets route registration details for a specific path.
|
|
*
|
|
* @param path - The route path to look up
|
|
* @returns RouteRegistration if found, undefined otherwise
|
|
*/
|
|
export function getRouteByPath(path: string): RouteRegistration | undefined {
|
|
return ROUTES.find((route) => route.path === path);
|
|
}
|
|
|
|
/**
|
|
* Gets all routes available for a specific API version.
|
|
*
|
|
* @param version - The API version to filter by
|
|
* @returns Array of RouteRegistrations available for the version
|
|
*/
|
|
export function getRoutesForVersion(version: ApiVersion): RouteRegistration[] {
|
|
return ROUTES.filter((route) => !route.versions || route.versions.includes(version));
|
|
}
|
|
|
|
/**
|
|
* Clears the versioned router cache.
|
|
* Primarily useful for testing to ensure fresh router instances.
|
|
*/
|
|
export function clearRouterCache(): void {
|
|
versionedRouterCache.clear();
|
|
versionedRouterLogger.debug('Versioned router cache cleared');
|
|
}
|
|
|
|
/**
|
|
* Refreshes the versioned router cache by recreating all routers.
|
|
* Useful after configuration changes.
|
|
*/
|
|
export function refreshRouterCache(): void {
|
|
clearRouterCache();
|
|
for (const version of API_VERSIONS) {
|
|
versionedRouterCache.set(version, createVersionedRouter(version));
|
|
}
|
|
versionedRouterLogger.debug(
|
|
{ cachedVersions: Array.from(versionedRouterCache.keys()) },
|
|
'Versioned router cache refreshed',
|
|
);
|
|
}
|