{"record":{"id":"4f9ac9026fb3a50a","repo":"actualbudget/actual","slug":"access-already-exists","errorCode":null,"errorMessage":"Access already exists","messagePattern":"Access already exists","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/sync-server/src/services/user-service.ts","lineNumber":199,"sourceCode":"}\n\nexport function addUserAccess(userId, fileId) {\n  if (!userId || !fileId) {\n    throw new Error('Invalid parameters');\n  }\n  try {\n    const userExists = getUserById(userId);\n    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(',');","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/services/user-service.ts#L181-L217","documentation":"addUserAccess inserts into user_access, which has a UNIQUE constraint on (user_id, file_id). When the INSERT violates it, SQLite raises a UNIQUE constraint error which this function detects and rethrows as 'Access already exists' — meaning the user already has access to the file. It is an idempotency signal, not corruption.","triggerScenarios":"Calling addUserAccess twice with the same (userId, fileId) pair — double-clicking a share button, retrying a request that actually succeeded, replaying a share operation, or concurrent requests sharing the same file with the same user simultaneously.","commonSituations":"Front-end share forms submitting twice without disabling the button; automation scripts re-running without checking current access; race conditions between two admins sharing the same file at once; re-running a migration/import script.","solutions":["Check existing access first (getUserAccess or countUserAccess) and skip the insert if it already returns a row","Treat this error as success/no-op in callers that only need the access to exist (idempotent handling)","Debounce/disable the share action in the UI after the first submission","Use INSERT OR IGNORE semantics at the call site pattern: check-then-insert or catch-and-continue"],"exampleFix":"// before\nawait addUserAccess(userId, fileId); // throws on retry\n// after\nconst existing = countUserAccess(fileId, userId);\nif (!existing) {\n  await addUserAccess(userId, fileId);\n}","handlingStrategy":"validation","validationCode":"import { countUserAccess } from './services/user-service';\nasync function grantAccessOnce(userId, fileId) {\n  if (countUserAccess(fileId, userId) > 0) return; // already shared\n  await addUserAccess(userId, fileId);\n}","typeGuard":"function hasAccess(userId, fileId) {\n  return countUserAccess(fileId, userId) > 0;\n}","tryCatchPattern":"try {\n  addUserAccess(userId, fileId);\n} catch (e) {\n  if (e.message.includes('Access already exists')) {\n    return; // idempotent success\n  }\n  throw e;\n}","preventionTips":["Check countUserAccess before inserting","Treat duplicates as success in idempotent callers","Debounce/disable share buttons after first click","Guard concurrent shares with a per-(user,file) lock or queue"],"tags":["sync-server","unique-constraint","duplicate","idempotency"],"backgroundTag":"unique-constraint-violation","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}