From 6aa8da41714118016c36cab810a4b8ecf25ec375 Mon Sep 17 00:00:00 2001 From: Liam Phung Date: Fri, 16 Oct 2026 18:29:31 +0700 Subject: [PATCH] fix(entrypoint): deduplicate conflicting imports and route mounts in src/index.ts (#1200) --- src/index.test.ts | 41 ++++++++++++++++++++++++++++++++++++++++- src/index.ts | 42 ++++++++++++------------------------------ 2 files changed, 52 insertions(+), 31 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index fe750e9e..4c3ce04c 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -7,7 +7,9 @@ import app, { createGracefulShutdownHandler, createInFlightDrainTracker } from ' jest.mock('./db/index.js', () => ({ db: {}, initializeDb: jest.fn(), - schema: {}, + schema: { + apis: { deleted_at: 'deleted_at' }, + }, })); describe('Health API', () => { it('should return ok status', async () => { @@ -348,3 +350,40 @@ describe('refresh-token drain tracker', () => { expect(tracker.subsystem.name).toBe('refresh-token'); }); }); + +import fs from 'node:fs'; +import path from 'node:path'; + +describe('entrypoint hygiene — src/index.ts (#1200)', () => { + const indexSource = fs.readFileSync(path.join(process.cwd(), 'src', 'index.ts'), 'utf8'); + + it('has no duplicate top-level module import paths', () => { + const importRegex = /^import\s+[\s\S]*?from\s+["']([^"']+)["'];/gm; + const seenModules = new Set(); + const duplicates: string[] = []; + let match: RegExpExecArray | null; + + while ((match = importRegex.exec(indexSource)) !== null) { + const mod = match[1]; + if (seenModules.has(mod)) { + duplicates.push(mod); + } + seenModules.add(mod); + } + + expect(duplicates).toEqual([]); + }); + + it('imports createApiKeyRouter, createWebhooksRouter, and defaultApiRepository', () => { + expect(indexSource).toMatch(/import\s+\{\s*createApiKeyRouter\s*\}\s+from\s+["']\.\/routes\/apiKeyRoutes\.js["']/); + expect(indexSource).toMatch(/import\s+\{\s*createWebhooksRouter\s*\}\s+from\s+["']\.\/routes\/webhooks\.js["']/); + expect(indexSource).toMatch(/import\s+\{\s*defaultApiRepository\s*\}\s+from\s+["']\.\/repositories\/apiRepository\.js["']/); + }); + + it('mounts /api/admin and /api/admin/usage/anomalies only once', () => { + const adminMounts = indexSource.match(/app\.use\(\s*["']\/api\/admin["']/g) ?? []; + const anomaliesMounts = indexSource.match(/app\.use\(\s*["']\/api\/admin\/usage\/anomalies["']/g) ?? []; + expect(adminMounts).toHaveLength(1); + expect(anomaliesMounts).toHaveLength(1); + }); +}); diff --git a/src/index.ts b/src/index.ts index b5b90c95..62489c0c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,17 +22,21 @@ import { createInFlightDrainTracker, type DrainableSubsystem, } from "./lifecycle/shutdown.js"; -import { quotasDrainTracker } from "./routes/quotas/counts.js"; import type { Socket } from "net"; - import { createDeveloperRouter } from "./routes/developerRoutes.js"; import { createGatewayRouter } from "./routes/gatewayRoutes.js"; import { createProxyRouter } from "./routes/proxyRoutes.js"; +import { createApiKeyRouter } from "./routes/apiKeyRoutes.js"; import { createWebhooksRouter } from "./routes/webhooks.js"; +import { createRefreshTokenRouter } from "./routes/refresh-token.js"; import adminRouter from "./routes/admin.js"; import logsRouter from "./routes/logs.js"; import { createUsageAnomaliesRouter } from "./routes/admin/usage/anomalies.js"; import refundsRouter from "./routes/refunds.js"; +import { AuthController } from "./controllers/authController.js"; +import { RefreshTokenService } from "./services/refreshTokenService.js"; +import { DatabaseRefreshTokenRepository } from "./repositories/refreshTokenRepository.js"; +import { defaultApiRepository } from "./repositories/apiRepository.js"; import { defaultDeveloperRepository } from "./repositories/developerRepository.js"; import { createBillingService } from "./services/billingService.js"; import { @@ -47,7 +51,7 @@ import { createIdempotencySweeperJob } from "./services/idempotencySweeper.js"; import { createPostgresUsageStore } from "./services/usageStore.js"; import { createPostgresSettlementStore } from "./services/settlementStore.js"; import { createApiRegistry } from "./data/apiRegistry.js"; -import { ApiKey } from "./types/gateway.js"; +import type { ApiKey } from "./types/gateway.js"; import { listingsCache } from "./lib/listingsCache.js"; import { createSlowQueryAlerterJob } from "./workers/slowQueryAlerter.js"; import { createAnomalyDetectorJob } from "./workers/anomalyDetector.js"; @@ -58,29 +62,6 @@ import { import { createSloAlertJob } from "./workers/sloAlertJob.js"; import { createMonthlyInvoiceJob } from "./workers/monthlyInvoiceJob.js"; import { createSettlementReconWorker } from "./workers/settlementRecon.js"; -import { createDeveloperRouter } from './routes/developerRoutes.js'; -import { createGatewayRouter } from './routes/gatewayRoutes.js'; -import { createProxyRouter } from './routes/proxyRoutes.js'; -import { createRefreshTokenRouter } from './routes/refresh-token.js'; -import { AuthController } from './controllers/authController.js'; -import { RefreshTokenService } from './services/refreshTokenService.js'; -import { DatabaseRefreshTokenRepository } from './repositories/refreshTokenRepository.js'; -import { defaultDeveloperRepository } from './repositories/developerRepository.js'; -import { createBillingService } from './services/billingService.js'; -import { createRateLimiter } from './services/rateLimiter.js'; -import { PgUsageEventsRepository } from './repositories/usageEventsRepository.pg.js'; -import { createRevenueLedgerIndexerJob } from './services/revenueLedgerIndexer.js'; -import { RevenueSettlementService } from './services/revenueSettlementService.js'; -import { createSettlementStatusSyncJob } from './services/settlementStatusSyncJob.js'; -import { createSettlementReconciliationJob } from './services/settlementReconciliationJob.js'; -import { createIdempotencySweeperJob } from './services/idempotencySweeper.js'; -import { createPostgresUsageStore } from './services/usageStore.js'; -import { createPostgresSettlementStore } from './services/settlementStore.js'; -import { createApiRegistry } from './data/apiRegistry.js'; -import { ApiKey } from './types/gateway.js'; -import { listingsCache } from './lib/listingsCache.js'; -import { createSlowQueryAlerterJob } from './workers/slowQueryAlerter.js'; -import { createAnomalyDetectorJob } from './workers/anomalyDetector.js'; // Helper for Jest/CommonJS compat const isDirectExecution = @@ -273,9 +254,6 @@ if (isDirectExecution) { app.use("/api/admin", adminRouter); app.use("/api/refunds", refundsRouter); app.use("/api/logs", logsRouter); - app.use('/api/admin/usage/anomalies', createUsageAnomaliesRouter({ pool })); - - app.use('/api/admin', adminRouter); // Legacy gateway route (existing) const gatewayRouter = createGatewayRouter({ @@ -285,7 +263,7 @@ if (isDirectExecution) { upstreamUrl: config.proxy.upstreamUrl, apiKeys, }); - app.use("/api/gateway", createGatewayIpAllowlist(), gatewayRouter); + app.use("/api/gateway", createGatewayIpAllowlist(), legacyV1DeprecationMiddleware, gatewayRouter); // New proxy route: /v1/call/:apiSlugOrId/* // @@ -309,11 +287,14 @@ if (isDirectExecution) { // during the graceful shutdown window. drainState: { isDraining: proxyDrainTracker.isDraining }, }); + app.use("/v1/call", proxyDrainTracker.middleware, proxyRouter); + const keysDrainTracker = createInFlightDrainTracker("api-keys"); const apiKeyRouter = createApiKeyRouter({ apiRepository: defaultApiRepository, developerRepository: defaultDeveloperRepository, }); + app.use("/api", keysDrainTracker.middleware, apiKeyRouter); // --- Refresh-token drain tracker --- // Tracks in-flight POST /api/refresh-token requests so that a SIGTERM during @@ -335,6 +316,7 @@ if (isDirectExecution) { const shutdownSubsystems: DrainableSubsystem[] = [ proxyDrainTracker.subsystem, + keysDrainTracker.subsystem, // Drain in-flight refresh-token requests before the process exits. refreshTokenDrainTracker.subsystem, {