{"record":{"id":"0a32951f63946e25","repo":"koala73/worldmonitor","slug":"year-must-be-0-or-between-1951-and-currentyear","errorCode":null,"errorMessage":"year must be 0 or between 1951 and ${currentYear}","messagePattern":"year must be 0 or between 1951 and (.+?)","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"warning","filePath":"server/worldmonitor/displacement/v1/get-displacement-summary.ts","lineNumber":16,"sourceCode":"import type {\n  ServerContext,\n  GetDisplacementSummaryRequest,\n  GetDisplacementSummaryResponse,\n} from '../../../../src/generated/server/worldmonitor/displacement/v1/service_server';\nimport { ValidationError } from '../../../../src/generated/server/worldmonitor/displacement/v1/service_server';\nimport { getCachedJson } from '../../../_shared/redis';\n\n// Railway owns fetching, aggregation and publication; RPC callers only select seed data.\nexport async function getDisplacementSummary(\n  _ctx: ServerContext,\n  req: GetDisplacementSummaryRequest,\n): Promise<GetDisplacementSummaryResponse> {\n  const currentYear = new Date().getFullYear();\n  if (!Number.isInteger(req.year) || (req.year !== 0 && (req.year < 1951 || req.year > currentYear))) {\n    throw new ValidationError([{ field: 'year', description: `year must be 0 or between 1951 and ${currentYear}` }]);\n  }\n  const emptyResponse: GetDisplacementSummaryResponse = {\n    summary: {\n      year: req.year || currentYear,\n      globalTotals: { refugees: 0, asylumSeekers: 0, idps: 0, stateless: 0, total: 0 },\n      countries: [],\n      topFlows: [],\n    },\n    fetchedAt: 0,\n    dataAvailable: false,\n  };\n\n  try {\n    // The current-year key can contain prior-year data when UNHCR has not published yet.\n    const [seedData, seedMeta] = await Promise.all([\n      getCachedJson(`displacement:summary:v1:${currentYear}`, true) as Promise<GetDisplacementSummaryResponse | null>,\n      getCachedJson('seed-meta:displacement:summary', true) as Promise<{ fetchedAt?: number } | null>,\n    ]);","sourceCodeStart":1,"sourceCodeEnd":34,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/server/worldmonitor/displacement/v1/get-displacement-summary.ts#L1-L34","documentation":"getDisplacementSummary requires req.year to be an integer that is either 0 (meaning 'latest') or within the supported UNHCR range 1951..currentYear. Anything else (floats, strings pre-coercion, years outside the range) throws a ValidationError carrying the field 'year' and the dynamic range message.","triggerScenarios":"Passing year=1949 (before UNHCR reporting starts), year=2077 (beyond currentYear), 2023.5, or a non-integer; client sending the year as a string when the schema expects a number.","commonSituations":"Hardcoded historical-year presets predating 1951; date pickers producing fractional epochs; forgetting that 0 is the sentinel for 'current year' and blocking it client-side.","solutions":["Clamp client-side: year = Math.round(year) and reject <1951 or >new Date().getFullYear(), allowing 0","Send 0 when you want the latest data instead of new Date().getFullYear()","Catch ValidationError and read the field/description array to prompt the user for a valid year"],"exampleFix":"// before\nawait getDisplacementSummary(ctx, { year: 1949 });\n// after\nconst year = requested < 1951 ? 0 : requested; // 0 = latest\nawait getDisplacementSummary(ctx, { year });","handlingStrategy":"validation","validationCode":"const y = req.year;\nconst valid = Number.isInteger(y) && (y === 0 || (y >= 1951 && y <= new Date().getFullYear()));\nif (!valid) throw new Error(`year must be 0 or between 1951 and ${new Date().getFullYear()}`);","typeGuard":"const isValidYear = (v: unknown): v is number =>\n  typeof v === 'number' && Number.isInteger(v) && (v === 0 || (v >= 1951 && v <= new Date().getFullYear()));","tryCatchPattern":"try {\n  return await getDisplacementSummary(ctx, { year });\n} catch (e) {\n  if (e instanceof ValidationError && e.issues.some(i => i.field === 'year')) {\n    return await getDisplacementSummary(ctx, { year: 0 }); // fall back to latest\n  }\n  throw e;\n}","preventionTips":["Remember 0 is the sentinel for 'latest year'","Clamp user-selected years into [1951, currentYear]","Send year as a number, never a string"],"tags":["validation","range","displacement"],"backgroundTag":"value-out-of-range","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}