{"record":{"id":"490b8ad577e13f39","repo":"actualbudget/actual","slug":"the-provided-userids-must-be-a-non-empty-array","errorCode":null,"errorMessage":"The provided userIds must be a non-empty array.","messagePattern":"The provided userIds must be a non-empty array\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/sync-server/src/services/user-service.ts","lineNumber":207,"sourceCode":"    const fileExists = getFileById(fileId);\n    if (!userExists || !fileExists) {\n      throw new Error('User or file not found');\n    }\n    getAccountDb().mutate(\n      'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)',\n      [userId, fileId],\n    );\n  } catch (error) {\n    if (error.message.includes('UNIQUE constraint')) {\n      throw new Error('Access already exists');\n    }\n    throw new Error(`Failed to add user access: ${error.message}`);\n  }\n}\n\nexport function deleteUserAccessByFileId(userIds, fileId) {\n  if (!Array.isArray(userIds) || userIds.length === 0) {\n    throw new Error('The provided userIds must be a non-empty array.');\n  }\n\n  const CHUNK_SIZE = 999;\n  let totalChanges = 0;\n\n  try {\n    getAccountDb().transaction(() => {\n      for (let i = 0; i < userIds.length; i += CHUNK_SIZE) {\n        const chunk = userIds.slice(i, i + CHUNK_SIZE);\n        const placeholders = chunk.map(() => '?').join(',');\n\n        const sql = `DELETE FROM user_access WHERE user_id IN (${placeholders}) AND file_id = ?`;\n\n        const result = getAccountDb().mutate(sql, [...chunk, fileId]);\n        totalChanges += result.changes;\n      }\n    });\n  } catch (error) {","sourceCodeStart":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/services/user-service.ts#L189-L225","documentation":"deleteUserAccessByFileId requires userIds to be a non-empty array and throws this error otherwise. The function builds an IN (...) clause from the array, so an empty or non-array value would produce invalid SQL or a pointless transaction. The check happens before any database work.","triggerScenarios":"Calling deleteUserAccessByFileId(userIds, fileId) with a single id string instead of an array, an empty array after filtering, undefined/null when a caller had nothing to delete, or a destructured variable that is not an array.","commonSituations":"Scripts passing one userId without wrapping it in an array; batch-revocation jobs computing an empty selection and still calling the function; API callers sending a single id in the request body; refactors changing the signature from (userId, fileId) to (userIds, fileId).","solutions":["Always pass an array of user ids, e.g. deleteUserAccessByFileId([userId], fileId)","Check userIds.length > 0 at the call site and skip the call when nothing needs deleting","Update callers still using the old single-id signature","Return early (or a 204) when the batch is empty instead of treating it as an error"],"exampleFix":"// before\ndeleteUserAccessByFileId(userId, fileId); // throws: not an array\n// after\nconst userIds = Array.isArray(userId) ? userId : [userId];\nif (userIds.length > 0) {\n  deleteUserAccessByFileId(userIds, fileId);\n}","handlingStrategy":"validation","validationCode":"function assertNonEmptyArray(value, name) {\n  if (!Array.isArray(value) || value.length === 0) {\n    throw new Error(`${name} must be a non-empty array`);\n  }\n}\nassertNonEmptyArray(userIds, 'userIds');","typeGuard":"function isUserIdArray(value) {\n  return Array.isArray(value) && value.length > 0 &&\n         value.every(id => typeof id === 'string' && id.length > 0);\n}","tryCatchPattern":"try {\n  deleteUserAccessByFileId(userIds, fileId);\n} catch (e) {\n  if (e.message.includes('must be a non-empty array')) {\n    logger.warn('Nothing to revoke; skipping');\n    return 0;\n  }\n  throw e;\n}","preventionTips":["Wrap single ids in an array before calling","Skip the call (or return 204) when the computed batch is empty","Update callers still on the old (userId, fileId) signature","Validate request bodies normalize single ids into arrays at the API layer"],"tags":["sync-server","validation","arguments","array"],"backgroundTag":"invalid-argument-type","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}