From f51e9b85744eb99210c3be88381b559ba6534f8f Mon Sep 17 00:00:00 2001 From: Bert Hausmans Date: Wed, 14 Jan 2026 16:50:33 +0100 Subject: [PATCH] Fix all req.params and req.query type errors - Add getParamString helper function for req.params - Replace all req.params destructuring with getParamString - Fix remaining req.query.* direct usage errors - All TypeScript compilation errors now resolved --- backend/src/routes/applications.ts | 13 +- backend/src/routes/cache.ts | 30 ++-- backend/src/routes/classifications.ts | 14 +- backend/src/routes/objects.ts | 15 +- backend/src/utils/queryHelpers.ts | 11 +- docs/TYPESCRIPT-LOCAL-VS-CI.md | 192 ++++++++++++++++++++++++++ 6 files changed, 241 insertions(+), 34 deletions(-) create mode 100644 docs/TYPESCRIPT-LOCAL-VS-CI.md diff --git a/backend/src/routes/applications.ts b/backend/src/routes/applications.ts index 6ef8472..971654a 100644 --- a/backend/src/routes/applications.ts +++ b/backend/src/routes/applications.ts @@ -6,7 +6,7 @@ import { logger } from '../services/logger.js'; import { calculateRequiredEffortApplicationManagementWithBreakdown } from '../services/effortCalculation.js'; import { findBIAMatch, loadBIAData, clearBIACache, calculateSimilarity } from '../services/biaMatchingService.js'; import { calculateApplicationCompleteness } from '../services/dataCompletenessConfig.js'; -import { getQueryString } from '../utils/queryHelpers.js'; +import { getQueryString, getParamString } from '../utils/queryHelpers.js'; import type { SearchFilters, ReferenceValue, ClassificationResult, ApplicationDetails, ApplicationStatus } from '../types/index.js'; import type { Server, Flows, Certificate, Domain, AzureSubscription, CMDBObjectTypeName } from '../generated/jira-types.js'; @@ -323,7 +323,7 @@ router.get('/bia-comparison', async (req: Request, res: Response) => { // - mode=edit: Force refresh from Jira for editing (includes _jiraUpdatedAt for conflict detection) router.get('/:id', async (req: Request, res: Response) => { try { - const { id } = req.params; + const id = getParamString(req, 'id'); const mode = getQueryString(req, 'mode'); // Don't treat special routes as application IDs @@ -359,7 +359,7 @@ router.get('/:id', async (req: Request, res: Response) => { // Update application with conflict detection router.put('/:id', async (req: Request, res: Response) => { try { - const { id } = req.params; + const id = getParamString(req, 'id'); const { updates, _jiraUpdatedAt } = req.body as { updates?: { applicationFunctions?: ReferenceValue[]; @@ -471,7 +471,7 @@ router.put('/:id', async (req: Request, res: Response) => { // Force update (ignore conflicts) router.put('/:id/force', async (req: Request, res: Response) => { try { - const { id } = req.params; + const id = getParamString(req, 'id'); const updates = req.body; const application = await dataService.getApplicationById(id); @@ -552,7 +552,7 @@ router.post('/calculate-effort', async (req: Request, res: Response) => { // Get application classification history router.get('/:id/history', async (req: Request, res: Response) => { try { - const { id } = req.params; + const id = getParamString(req, 'id'); const history = await databaseService.getClassificationsByApplicationId(id); res.json(history); } catch (error) { @@ -564,7 +564,8 @@ router.get('/:id/history', async (req: Request, res: Response) => { // Get related objects for an application (from cache) router.get('/:id/related/:objectType', async (req: Request, res: Response) => { try { - const { id, objectType } = req.params; + const id = getParamString(req, 'id'); + const objectType = getParamString(req, 'objectType'); // Map object type string to CMDBObjectTypeName const typeMap: Record = { diff --git a/backend/src/routes/cache.ts b/backend/src/routes/cache.ts index 42757c8..4a4b206 100644 --- a/backend/src/routes/cache.ts +++ b/backend/src/routes/cache.ts @@ -8,7 +8,7 @@ import { Router, Request, Response } from 'express'; import { cacheStore } from '../services/cacheStore.js'; import { syncEngine } from '../services/syncEngine.js'; import { logger } from '../services/logger.js'; -import { getQueryString } from '../utils/queryHelpers.js'; +import { getQueryString, getParamString } from '../utils/queryHelpers.js'; import { OBJECT_TYPES } from '../generated/jira-schema.js'; import type { CMDBObjectTypeName } from '../generated/jira-types.js'; @@ -77,13 +77,12 @@ router.post('/sync', async (req: Request, res: Response) => { // Trigger sync for a specific object type router.post('/sync/:objectType', async (req: Request, res: Response) => { try { - const { objectType } = req.params; + const objectType = getParamString(req, 'objectType'); - // Validate object type - convert to string if array - const objectTypeStr = Array.isArray(objectType) ? objectType[0] : objectType; - if (!OBJECT_TYPES[objectTypeStr]) { + // Validate object type + if (!OBJECT_TYPES[objectType]) { res.status(400).json({ - error: `Unknown object type: ${objectTypeStr}`, + error: `Unknown object type: ${objectType}`, supportedTypes: Object.keys(OBJECT_TYPES), }); return; @@ -91,22 +90,23 @@ router.post('/sync/:objectType', async (req: Request, res: Response) => { logger.info(`Manual sync triggered for ${objectType}`); - const result = await syncEngine.syncType(objectTypeStr as CMDBObjectTypeName); + const result = await syncEngine.syncType(objectType as CMDBObjectTypeName); res.json({ status: 'completed', - objectType: objectTypeStr, + objectType: objectType, stats: result, }); } catch (error) { + const objectType = getParamString(req, 'objectType'); const errorMessage = error instanceof Error ? error.message : 'Failed to sync object type'; - logger.error(`Failed to sync object type ${req.params.objectType}`, error); + logger.error(`Failed to sync object type ${objectType}`, error); // Return 409 (Conflict) if sync is already in progress, otherwise 500 const statusCode = errorMessage.includes('already in progress') ? 409 : 500; res.status(statusCode).json({ error: errorMessage, - objectType: req.params.objectType, + objectType: objectType, }); } }); @@ -114,12 +114,11 @@ router.post('/sync/:objectType', async (req: Request, res: Response) => { // Clear cache for a specific type router.delete('/clear/:objectType', async (req: Request, res: Response) => { try { - const { objectType } = req.params; - const objectTypeStr = Array.isArray(objectType) ? objectType[0] : objectType; + const objectType = getParamString(req, 'objectType'); - if (!OBJECT_TYPES[objectTypeStr]) { + if (!OBJECT_TYPES[objectType]) { res.status(400).json({ - error: `Unknown object type: ${objectTypeStr}`, + error: `Unknown object type: ${objectType}`, supportedTypes: Object.keys(OBJECT_TYPES), }); return; @@ -135,7 +134,8 @@ router.delete('/clear/:objectType', async (req: Request, res: Response) => { deletedCount: deleted, }); } catch (error) { - logger.error(`Failed to clear cache for ${req.params.objectType}`, error); + const objectType = getParamString(req, 'objectType'); + logger.error(`Failed to clear cache for ${objectType}`, error); res.status(500).json({ error: 'Failed to clear cache' }); } }); diff --git a/backend/src/routes/classifications.ts b/backend/src/routes/classifications.ts index b70e801..97187e2 100644 --- a/backend/src/routes/classifications.ts +++ b/backend/src/routes/classifications.ts @@ -4,14 +4,14 @@ import { dataService } from '../services/dataService.js'; import { databaseService } from '../services/database.js'; import { logger } from '../services/logger.js'; import { config } from '../config/env.js'; -import { getQueryString, getQueryNumber } from '../utils/queryHelpers.js'; +import { getQueryString, getQueryNumber, getParamString } from '../utils/queryHelpers.js'; const router = Router(); // Get AI classification for an application router.post('/suggest/:id', async (req: Request, res: Response) => { try { - const { id } = req.params; + const id = getParamString(req, 'id'); // Get provider from query parameter or request body, default to config const provider = (getQueryString(req, 'provider') as AIProvider) || (req.body.provider as AIProvider) || config.defaultAIProvider; @@ -53,7 +53,7 @@ router.get('/taxonomy', (req: Request, res: Response) => { // Get function by code router.get('/function/:code', (req: Request, res: Response) => { try { - const { code } = req.params; + const code = getParamString(req, 'code'); const func = aiService.getFunctionByCode(code); if (!func) { @@ -112,7 +112,7 @@ router.get('/ai-status', (req: Request, res: Response) => { // Get the AI prompt for an application (for debugging/copy-paste) router.get('/prompt/:id', async (req: Request, res: Response) => { try { - const { id } = req.params; + const id = getParamString(req, 'id'); const application = await dataService.getApplicationById(id); if (!application) { @@ -131,7 +131,7 @@ router.get('/prompt/:id', async (req: Request, res: Response) => { // Chat with AI about an application router.post('/chat/:id', async (req: Request, res: Response) => { try { - const { id } = req.params; + const id = getParamString(req, 'id'); const { message, conversationId, provider: requestProvider } = req.body; if (!message || typeof message !== 'string' || message.trim().length === 0) { @@ -168,7 +168,7 @@ router.post('/chat/:id', async (req: Request, res: Response) => { // Get conversation history router.get('/chat/conversation/:conversationId', (req: Request, res: Response) => { try { - const { conversationId } = req.params; + const conversationId = getParamString(req, 'conversationId'); const messages = aiService.getConversationHistory(conversationId); if (messages.length === 0) { @@ -186,7 +186,7 @@ router.get('/chat/conversation/:conversationId', (req: Request, res: Response) = // Clear a conversation router.delete('/chat/conversation/:conversationId', (req: Request, res: Response) => { try { - const { conversationId } = req.params; + const conversationId = getParamString(req, 'conversationId'); const deleted = aiService.clearConversation(conversationId); if (!deleted) { diff --git a/backend/src/routes/objects.ts b/backend/src/routes/objects.ts index f334635..1cb6c80 100644 --- a/backend/src/routes/objects.ts +++ b/backend/src/routes/objects.ts @@ -7,7 +7,7 @@ import { Router, Request, Response } from 'express'; import { cmdbService } from '../services/cmdbService.js'; import { logger } from '../services/logger.js'; -import { getQueryString, getQueryNumber } from '../utils/queryHelpers.js'; +import { getQueryString, getQueryNumber, getParamString } from '../utils/queryHelpers.js'; import { OBJECT_TYPES } from '../generated/jira-schema.js'; import type { CMDBObject, CMDBObjectTypeName } from '../generated/jira-types.js'; @@ -32,7 +32,7 @@ router.get('/', (req: Request, res: Response) => { // Get all objects of a type router.get('/:type', async (req: Request, res: Response) => { try { - const { type } = req.params; + const type = getParamString(req, 'type'); const limit = getQueryNumber(req, 'limit', 1000); const offset = getQueryNumber(req, 'offset', 0); const search = getQueryString(req, 'search'); @@ -71,7 +71,8 @@ router.get('/:type', async (req: Request, res: Response) => { // Get a specific object by ID router.get('/:type/:id', async (req: Request, res: Response) => { try { - const { type, id } = req.params; + const type = getParamString(req, 'type'); + const id = getParamString(req, 'id'); const forceRefresh = getQueryString(req, 'refresh') === 'true'; // Validate type @@ -102,7 +103,9 @@ router.get('/:type/:id', async (req: Request, res: Response) => { // Get related objects router.get('/:type/:id/related/:relationType', async (req: Request, res: Response) => { try { - const { type, id, relationType } = req.params; + const type = getParamString(req, 'type'); + const id = getParamString(req, 'id'); + const relationType = getParamString(req, 'relationType'); const attribute = getQueryString(req, 'attribute'); // Validate types @@ -139,7 +142,9 @@ router.get('/:type/:id/related/:relationType', async (req: Request, res: Respons // Get objects referencing this object (inbound references) router.get('/:type/:id/referenced-by/:sourceType', async (req: Request, res: Response) => { try { - const { type, id, sourceType } = req.params; + const type = getParamString(req, 'type'); + const id = getParamString(req, 'id'); + const sourceType = getParamString(req, 'sourceType'); const attribute = getQueryString(req, 'attribute'); // Validate types diff --git a/backend/src/utils/queryHelpers.ts b/backend/src/utils/queryHelpers.ts index d4bbbcb..febe233 100644 --- a/backend/src/utils/queryHelpers.ts +++ b/backend/src/utils/queryHelpers.ts @@ -1,5 +1,5 @@ /** - * Helper functions for Express request query parameters + * Helper functions for Express request query and params */ import { Request } from 'express'; @@ -32,3 +32,12 @@ export function getQueryBoolean(req: Request, key: string, defaultValue = false) if (value === undefined) return defaultValue; return value === 'true' || value === '1'; } + +/** + * Get a route parameter as a string, handling both string and string[] types + */ +export function getParamString(req: Request, key: string): string { + const value = req.params[key]; + if (Array.isArray(value)) return value[0] as string; + return value as string; +} diff --git a/docs/TYPESCRIPT-LOCAL-VS-CI.md b/docs/TYPESCRIPT-LOCAL-VS-CI.md new file mode 100644 index 0000000..1df6cdc --- /dev/null +++ b/docs/TYPESCRIPT-LOCAL-VS-CI.md @@ -0,0 +1,192 @@ +# Waarom TypeScript Errors Lokaal Niet Optreden Maar Wel in CI/CD + +## Het Probleem + +TypeScript compilation errors die lokaal niet optreden, maar wel in Azure DevOps pipelines of Docker builds. Dit is een veelvoorkomend probleem met verschillende oorzaken. + +## Belangrijkste Oorzaken + +### 1. **tsx vs tsc - Development vs Production Build** + +**Lokaal (Development):** +```bash +npm run dev # Gebruikt: tsx watch src/index.ts +``` + +**In Docker/CI (Production Build):** +```bash +npm run build # Gebruikt: tsc (TypeScript Compiler) +``` + +**Verschil:** +- **`tsx`** (TypeScript Execute): Een runtime TypeScript executor die code direct uitvoert zonder volledige type checking. Het is **minder strict** en laat veel type errors door. +- **`tsc`** (TypeScript Compiler): De officiële TypeScript compiler die **volledige type checking** doet en alle errors rapporteert. + +**Oplossing:** +- Test altijd lokaal met `npm run build` voordat je pusht +- Of gebruik `tsc --noEmit` om type checking te doen zonder te builden + +### 2. **TypeScript Versie Verschillen** + +**Lokaal:** +```bash +npx tsc --version # Kan een andere versie zijn +``` + +**In Docker:** +- Gebruikt de versie uit `package.json` (`"typescript": "^5.6.3"`) +- Maar zonder `package-lock.json` kan een andere patch versie geïnstalleerd worden + +**Oplossing:** +- Genereer `package-lock.json` met `npm install` +- Commit `package-lock.json` naar Git +- Dit zorgt voor consistente dependency versies + +### 3. **tsconfig.json Strictness** + +Je `tsconfig.json` heeft `"strict": true`, wat betekent: +- Alle strict type checking opties zijn aan +- Dit is goed voor productie, maar kan lokaal vervelend zijn + +**Mogelijke verschillen:** +- Lokaal kan je IDE/editor andere TypeScript settings hebben +- Lokaal kan je `tsconfig.json` overrides hebben +- CI/CD gebruikt altijd de exacte `tsconfig.json` uit de repo + +### 4. **Node.js Versie Verschillen** + +**Lokaal:** +- Kan een andere Node.js versie hebben +- TypeScript gedrag kan verschillen tussen Node versies + +**In Docker:** +```dockerfile +FROM node:20-alpine # Specifieke Node versie +``` + +**Oplossing:** +- Gebruik `.nvmrc` of `package.json` engines field om Node versie te specificeren +- Zorg dat lokaal dezelfde Node versie gebruikt wordt + +### 5. **Cached Builds** + +**Lokaal:** +- Oude compiled files in `dist/` kunnen nog werken +- IDE kan gecachte type informatie gebruiken +- `tsx` gebruikt geen build output, dus errors worden niet altijd gezien + +**In CI/CD:** +- Schone build elke keer +- Geen cache, dus alle errors worden gezien + +### 6. **Incremental Compilation** + +**Lokaal:** +- TypeScript kan incremental compilation gebruiken +- Alleen gewijzigde files worden gecheckt + +**In CI/CD:** +- Volledige rebuild elke keer +- Alle files worden gecheckt + +## Best Practices om Dit Te Voorkomen + +### 1. Test Lokaal Met Production Build + +```bash +# Voordat je pusht, test altijd: +cd backend +npm run build # Dit gebruikt tsc, net als in Docker +``` + +### 2. Type Checking Zonder Build + +```bash +# Alleen type checking, geen build: +npx tsc --noEmit +``` + +### 3. Pre-commit Hooks + +Voeg een pre-commit hook toe die `tsc --noEmit` draait: + +```json +// package.json +{ + "scripts": { + "type-check": "tsc --noEmit", + "precommit": "npm run type-check" + } +} +``` + +### 4. Genereer package-lock.json + +```bash +# Genereer lock file voor consistente dependencies: +npm install + +# Commit package-lock.json naar Git +git add package-lock.json +git commit -m "Add package-lock.json for consistent builds" +``` + +### 5. Gebruik CI/CD Lokaal + +Test je pipeline lokaal met: +- **act** (voor GitHub Actions) +- **Azure DevOps Pipeline Agent** lokaal +- **Docker build** lokaal: `docker build -f backend/Dockerfile.prod -t test-build ./backend` + +### 6. IDE TypeScript Settings + +Zorg dat je IDE dezelfde TypeScript versie gebruikt: +- VS Code: Check "TypeScript: Select TypeScript Version" +- Gebruik "Use Workspace Version" + +## Voor Dit Project Specifiek + +### Huidige Situatie + +1. **Development:** `tsx watch` - minder strict +2. **Production Build:** `tsc` - volledig strict +3. **Geen package-lock.json** - verschillende dependency versies mogelijk +4. **TypeScript 5.6.3** in package.json, maar lokaal mogelijk 5.9.3 + +### Aanbevolen Acties + +1. **Genereer package-lock.json:** + ```bash + cd backend + npm install + git add package-lock.json + git commit -m "Add package-lock.json" + ``` + +2. **Test altijd met build:** + ```bash + npm run build # Voordat je pusht + ``` + +3. **Voeg type-check script toe:** + ```json + { + "scripts": { + "type-check": "tsc --noEmit" + } + } + ``` + +4. **Test Docker build lokaal:** + ```bash + docker build -f backend/Dockerfile.prod -t test-backend ./backend + ``` + +## Conclusie + +Het verschil komt vooral door: +- **tsx (dev) vs tsc (build)** - tsx is minder strict +- **Geen package-lock.json** - verschillende dependency versies +- **Cached builds lokaal** - oude code werkt nog + +**Oplossing:** Test altijd met `npm run build` lokaal voordat je pusht, en genereer `package-lock.json` voor consistente builds.