{"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":"exception","errorClass":"AlreadyInSpace","httpStatus":403,"severity":"warning","filePath":"packages/backend/server/src/core/workspaces/resolvers/member.ts","lineNumber":208,"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":190,"sourceCodeEnd":226,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/2af30773aecd567f09b346e7b72fc69143144057/packages/backend/server/src/core/workspaces/resolvers/member.ts#L190-L226","documentation":"Produced during inviteByEmail's pre-check loop when the target email belongs to an existing user that already has a workspaceUser record in this workspace (models.workspaceUser.get returns a row). The invite is skipped and the error is returned inside that email's InviteResult entry (results[index].error), not thrown to GraphQL. Code already_in_space, data.spaceId = workspaceId.","triggerScenarios":"Inviting an email whose user is already a member (any status — Pending, Accepted, AllocatingSeat, UnderReview counts, since the row exists). E.g. re-inviting someone whose invite is still pending, or inviting an existing member under a different email alias that resolves to the same user.","commonSituations":"Re-sending invites because the first email 'looked lost' but is still Pending; inviting a user who already joined via link; stale member lists in the client UI leading to duplicate invites.","solutions":["Refresh the member list before inviting and filter out emails of current members (including pending ones).","Treat results[i].error.name === 'already_in_space' as informational — surface 'already a member' per row rather than a hard failure.","If the pending invite should be resent, revoke/recreate that member entry instead of re-inviting the same email."],"exampleFix":"// before\nawait inviteByEmail(ws.id, ['bob@x.com']); // bob already member -> per-item error\n\n// after\nconst members = await getMembers(ws.id); // emails of all statuses\nconst memberEmails = new Set(members.map(m => m.email.toLowerCase()));\nawait inviteByEmail(ws.id, emails.filter(e => !memberEmails.has(e.toLowerCase())));","handlingStrategy":"validation","validationCode":"// filter out existing members (any status) before inviting\nconst memberEmails = new Set(\n  (await membersQuery({ workspaceId })).map(m => m.email.toLowerCase())\n);\nconst toInvite = emails.filter(e => !memberEmails.has(e.trim().toLowerCase()));","typeGuard":"function isAlreadyInSpaceResult(result: { error?: unknown }): boolean {\n  return String((result.error as { name?: string } | undefined)?.name ?? '').toLowerCase() === 'already_in_space';\n}","tryCatchPattern":"// per-item result — handle it where you render results\nconst results = await inviteByEmail(workspaceId, toInvite);\nfor (const r of results) {\n  if (isAlreadyInSpaceResult(r)) markRow(r.email, 'Already a member');\n}","preventionTips":["Refresh the member list (including Pending) right before a bulk invite.","Treat already_in_space as success for UI purposes — the desired state exists."],"tags":["affine","graphql","invite","membership","duplicate"],"backgroundTag":"already-a-member","analyzedSha":"2af30773aecd567f09b346e7b72fc69143144057","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}