{"record":{"id":"ffa077ceb21f05e3","repo":"koala73/worldmonitor","slug":"batch-rows-must-share-one-clientimportid","errorCode":null,"errorMessage":"batch rows must share one clientImportId","messagePattern":"batch rows must share one clientImportId","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"shared/company-monitoring-contract.ts","lineNumber":318,"sourceCode":"\n  if (utf8Bytes(JSON.stringify(normalized)) > COMPANY_MONITORING_LIMITS.maxImportRowBytes) {\n    throw new Error(`row exceeds ${COMPANY_MONITORING_LIMITS.maxImportRowBytes} bytes`);\n  }\n  return normalized;\n}\n\nexport function normalizeCompanyImportBatch(inputs: CompanyImportRowInput[]): NormalizedCompanyImportRow[] {\n  if (!Array.isArray(inputs)) throw new Error('import batch must be a list');\n  if (inputs.length === 0) throw new Error('import batch requires at least one row');\n  if (inputs.length > COMPANY_MONITORING_LIMITS.maxImportRows) {\n    throw new Error(`batch exceeds ${COMPANY_MONITORING_LIMITS.maxImportRows} rows`);\n  }\n\n  const normalized = inputs.map(normalizeCompanyImportRow).sort((left, right) => left.ordinal - right.ordinal);\n  const importId = normalized[0]?.clientImportId;\n  for (let index = 0; index < normalized.length; index += 1) {\n    const row = normalized[index]!;\n    if (row.clientImportId !== importId) throw new Error('batch rows must share one clientImportId');\n    if (row.ordinal !== index) throw new Error('batch ordinals must be contiguous from 0');\n  }\n  if (utf8Bytes(JSON.stringify(normalized)) > COMPANY_MONITORING_LIMITS.maxImportBatchBytes) {\n    throw new Error(`batch exceeds ${COMPANY_MONITORING_LIMITS.maxImportBatchBytes} bytes`);\n  }\n  return normalized;\n}\n\nexport function assertCompanyMonitoringPayloadSize(byteLength: number): void {\n  if (!Number.isSafeInteger(byteLength) || byteLength < 0) throw new Error('request byte length is invalid');\n  if (byteLength > COMPANY_MONITORING_LIMITS.maxRequestBytes) {\n    throw new Error(`request exceeds ${COMPANY_MONITORING_LIMITS.maxRequestBytes} bytes`);\n  }\n}\n\nexport function assertCompanyMonitoringAccountContext(\n  context: { ownerAccountId?: string } | null | undefined,\n): string {","sourceCodeStart":300,"sourceCodeEnd":336,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/shared/company-monitoring-contract.ts#L300-L336","documentation":"Thrown by normalizeCompanyImportBatch() when, after per-row normalization and sorting, any row carries a clientImportId different from the first row's. The contract requires every row in one batch to share a single clientImportId because that ID identifies the whole import session (idempotency and attribution unit), not individual rows.","triggerScenarios":"Concatenating rows prepared for two different import sessions into one call; generating a fresh clientImportId per row instead of once per batch; retry logic that mixes leftover rows from a previous partial import with new rows.","commonSituations":"Client code that creates the ID inside the per-row mapping function instead of once before the loop; merging 'retry the failed rows' with 'continue with new rows' in one request; a CSV uploader that restarts ID generation mid-file after an error.","solutions":["Generate one clientImportId (e.g. crypto.randomUUID()) once per batch and stamp it onto every row before calling normalizeCompanyImportBatch()","Pre-validate: const id = rows[0]?.clientImportId; assert rows.every(r => r.clientImportId === id)","When retrying or resuming an import, keep rows from different sessions in separate calls rather than concatenating"],"exampleFix":"// before\nconst rows = inputs.map((input, i) => ({ ...input, ordinal: i, clientImportId: crypto.randomUUID() }));\nconst normalized = normalizeCompanyImportBatch(rows); // every row has a different ID -> throws\n\n// after\nconst clientImportId = crypto.randomUUID(); // one ID for the whole batch\nconst rows = inputs.map((input, i) => ({ ...input, ordinal: i, clientImportId }));\nconst normalized = normalizeCompanyImportBatch(rows);","handlingStrategy":"validation","validationCode":"const clientImportId = crypto.randomUUID();\nconst rows = rawRows.map((r, i) => ({ ...r, ordinal: i, clientImportId }));\nconst shared = rows.length === 0 || rows.every(r => r.clientImportId === rows[0]!.clientImportId);\nif (!shared) throw new Error('rows carry mixed clientImportId — regenerate before submit');","typeGuard":"function hasSingleImportId(rows: { clientImportId: string }[]): boolean {\n  const first = rows[0]?.clientImportId;\n  return first !== undefined && rows.every(r => r.clientImportId === first);\n}","tryCatchPattern":"catch (e) { if (e instanceof Error && e.message === 'batch rows must share one clientImportId') { rebuildBatchWithOneId(); } else throw e; }","preventionTips":["Create clientImportId once per batch, before the per-row map — never inside it","Never concat rows from different import sessions; send separate calls","Assert the single-ID invariant in the import client's unit tests"],"tags":["validation","import","idempotency","company-monitoring"],"backgroundTag":"schema-validation-failed","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}