{"record":{"id":"765e27c4055e2963","repo":"toeverything/AFFiNE","slug":"already-in-space","errorCode":"already_in_space","errorMessage":"You have already joined in Space ${spaceId}.","messagePattern":"You have already joined in Space (.+?)\\.","errorType":"http","errorClass":"AlreadyInSpace","httpStatus":403,"severity":"warning","filePath":"packages/backend/server/src/core/workspaces/resolvers/member.ts","lineNumber":267,"sourceCode":"    const candidates: InviteCandidate[] = [];\n    const seen = new Set<string>();\n    for (const [index, email] of emails.entries()) {\n      try {\n        const normalizedEmail = email.trim().toLowerCase();\n        validators.assertValidEmail(normalizedEmail);\n        if (seen.has(normalizedEmail)) {\n          throw new ActionForbidden('Duplicate invite email.');\n        }\n        seen.add(normalizedEmail);\n\n        const target = await this.models.user.getUserByEmail(normalizedEmail);\n        if (target) {\n          const originRecord = await this.models.workspaceUser.get(\n            workspaceId,\n            target.id\n          );\n          if (originRecord) {\n            throw new AlreadyInSpace({ spaceId: workspaceId });\n          }\n        }\n\n        candidates.push({\n          index,\n          email,\n          normalizedEmail,\n          domain: emailDomain(normalizedEmail),\n          target: target ? { id: target.id } : undefined,\n        });\n      } catch (error) {\n        results[index] = {\n          email,\n          error: mapAnyError(error),\n        };\n      }\n    }\n","sourceCodeStart":249,"sourceCodeEnd":285,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/workspaces/resolvers/member.ts#L249-L285","documentation":"Thrown during the inviteMembers candidate-collection pass when a target user (matched by email) already has a workspaceUser record for that workspace. This pre-check runs before lock acquisition so already-invited users fail fast. Reported as already_in_space (action_forbidden, HTTP 403).","triggerScenarios":"Calling inviteMembers where one of the emails belongs to a user who is already a member (any status) of the workspace.","commonSituations":"Re-inviting someone who already accepted; inviting a pending member again; the member list UI was stale and the user appeared absent.","solutions":["Filter out emails of existing members before calling inviteMembers (query members first).","Read the per-entry InviteResult and skip/ignore already_in_space rows.","Refresh the member list before re-running an invite batch."],"exampleFix":"// before\ninviteMembers(workspaceId, emails);\n// after\nconst present = new Set((await members(...)).map(m => m.email));\ninviteMembers(workspaceId, emails.filter(e => !present.has(e.toLowerCase())));","handlingStrategy":"validation","validationCode":"// fetch existing member emails before inviting\nconst present = new Set((await members(ws, { take: 1000 })).map(m => m.email?.toLowerCase()).filter(Boolean));\nconst toInvite = emails.filter(e => !present.has(e.trim().toLowerCase()));\nif (toInvite.length) await inviteMembers(workspaceId, toInvite);","typeGuard":null,"tryCatchPattern":"const results = await inviteMembers(workspaceId, emails);\n// already_in_space rows are expected; filter them as 'success'\nconst actionable = results.filter(r => !r.error || r.error.code !== 'already_in_space');","preventionTips":["Query the current member list before constructing the invite batch.","Treat already_in_space as a non-error in the InviteResult UI.","Refresh the member list after each invite batch."],"tags":["graphql","invite","workspace","state"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}