{"record":{"id":"49ea4352a4569018","repo":"toeverything/AFFiNE","slug":"member-not-found-in-space","errorCode":"member_not_found_in_space","errorMessage":"Member not found in Space ${spaceId}.","messagePattern":"Member not found in Space (.+?)\\.","errorType":"exception","errorClass":"MemberNotFoundInSpace","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/core/workspaces/resolvers/member.ts","lineNumber":438,"sourceCode":"        } catch (error) {\n          if (backendRuntimeErrorCode(error) === 'seat_limit') {\n            throw new NoMoreSeat({ spaceId: workspaceId });\n          }\n          throw error;\n        }\n\n        this.event.emit('workspace.members.updated', {\n          workspaceId,\n        });\n\n        await this.workspaceService.sendReviewApprovedNotification(\n          role.id,\n          me.id\n        );\n      }\n      return true;\n    } else {\n      throw new MemberNotFoundInSpace({ spaceId: workspaceId });\n    }\n  }\n\n  @Mutation(() => Boolean)\n  async grantMember(\n    @CurrentUser() user: CurrentUser,\n    @Args('workspaceId') workspaceId: string,\n    @Args('userId') userId: string,\n    @Args('permission', { type: () => WorkspaceRole }) newRole: WorkspaceRole\n  ) {\n    let role: 'member' | 'admin' | 'owner';\n    switch (newRole) {\n      case WorkspaceRole.External:\n        return this.revokeMember(user, workspaceId, userId);\n      case WorkspaceRole.Collaborator:\n        role = 'member';\n        break;\n      case WorkspaceRole.Admin:","sourceCodeStart":420,"sourceCodeEnd":456,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/2af30773aecd567f09b346e7b72fc69143144057/packages/backend/server/src/core/workspaces/resolvers/member.ts#L420-L456","documentation":"Thrown by the approveMember mutation when models.workspaceUser.get(workspaceId, userId) returns no row — the user being approved has no membership record in the workspace. It fires when the role lookup fails entirely, as opposed to a member who exists but is not UnderReview (which returns true silently). Code member_not_found_in_space, type action_forbidden, HTTP 403, data.spaceId = workspaceId.","triggerScenarios":"Calling approveMember with a userId that was never a member, was already removed/revoked, or whose join request was withdrawn — e.g. two admins acting on the same request and one already handled or rejected it.","commonSituations":"Stale 'pending review' list in the admin UI after another admin processed the request; approving a user removed by an owner concurrently; passing a wrong userId (typo or from another workspace).","solutions":["Refresh the pending-members list and confirm the user still has a membership row in this workspace before approving.","If the row is gone, treat the request as already handled/cancelled — re-sync UI state instead of retrying.","Double-check that userId belongs to the same workspaceId (copy-paste errors across workspaces are common)."],"exampleFix":"// before\nawait approveMember(ws.id, userId); // row already removed -> member_not_found_in_space\n\n// after\nconst member = await getWorkspaceMember(ws.id, userId);\nif (!member || member.status !== 'UnderReview') {\n  await refetchPendingList(); // someone else handled it\n  return;\n}\nawait approveMember(ws.id, userId);","handlingStrategy":"try-catch","validationCode":"// confirm the membership row is still pending before approving\nconst member = await getWorkspaceMember(workspaceId, userId);\nif (!member || member.status !== 'UnderReview') {\n  await refetchPendingList(); // handled elsewhere or withdrawn\n}","typeGuard":"function isMemberNotFoundInSpace(e: unknown): boolean {\n  const ext = (e as { extensions?: Record<string, unknown> })?.extensions;\n  return String(ext?.name ?? '').toLowerCase() === 'member_not_found_in_space';\n}","tryCatchPattern":"try {\n  await approveMember(workspaceId, userId);\n} catch (e) {\n  if (isMemberNotFoundInSpace(e)) {\n    await refetchPendingList(); // request already handled — sync UI, no retry\n  } else throw e;\n}","preventionTips":["Refetch the pending-review list when the view gains focus.","Coordinate approvals across admins (or lock rows) to avoid double-processing."],"tags":["affine","graphql","member-approval","not-found","stale-state"],"backgroundTag":"member-not-found","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"}