{"record":{"id":"57ce89b5f9cb667b","repo":"toeverything/AFFiNE","slug":"expect-to-revoke-doc-user-roles","errorCode":"expect_to_revoke_doc_user_roles","errorMessage":"Expect doc not to be workspace","messagePattern":"Expect doc not to be workspace","errorType":"exception","errorClass":"ExpectToRevokeDocUserRoles","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/core/workspaces/resolvers/doc.ts","lineNumber":804,"sourceCode":"    this.logger.log(`Grant doc user roles (${JSON.stringify(info)})`);\n    return true;\n  }\n\n  @Mutation(() => Boolean)\n  async revokeDocUserRoles(\n    @CurrentUser() user: CurrentUser,\n    @Args('input') input: RevokeDocUserRoleInput\n  ): Promise<boolean> {\n    const pairs = {\n      spaceId: input.workspaceId,\n      docId: input.docId,\n    };\n    if (input.workspaceId === input.docId) {\n      this.logger.error(\n        'Expect to revoke doc user roles, but it is a workspace',\n        pairs\n      );\n      throw new ExpectToRevokeDocUserRoles(\n        pairs,\n        'Expect doc not to be workspace'\n      );\n    }\n    try {\n      await this.runtime.executeDomainCommandV1({\n        command: 'transition_doc_role',\n        actorUserId: user.id,\n        workspaceId: input.workspaceId,\n        docId: input.docId,\n        targetUserId: input.userId,\n      });\n    } catch (error) {\n      if (backendRuntimeErrorCode(error) === 'domain_permission_denied') {\n        throw new DocActionDenied({\n          action: 'Doc.Users.Manage',\n          ...pairs,\n        });","sourceCodeStart":786,"sourceCodeEnd":822,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/2af30773aecd567f09b346e7b72fc69143144057/packages/backend/server/src/core/workspaces/resolvers/doc.ts#L786-L822","documentation":"revokeDocUserRole rejects input where workspaceId === docId, the same root-is-a-Space guard as the other doc-role mutations. Revoking a user's role 'on the workspace root' is meaningless at doc level, so the call is refused with ExpectToRevokeDocUserRoles before any permission or model work.","triggerScenarios":"Calling revokeDocUserRole with input.docId === input.workspaceId — usually a stale id or an unpopulated form field defaulting to the space id.","commonSituations":"Role-management UIs operating on a synthetic root node; cleanup scripts iterating ids that include the workspace id.","solutions":["Guard the call: skip or error client-side when docId equals workspaceId","Fix the id source so docId always references a real page","To remove workspace access entirely, use workspace member removal, not doc role revocation"],"exampleFix":"// before\nawait revokeDocUserRole({ workspaceId: ws.id, docId: targetId /* sometimes ws.id */, userId });\n\n// after\nif (targetId === ws.id) {\n  await removeWorkspaceMember(ws.id, userId); // workspace-level removal\n} else {\n  await revokeDocUserRole({ workspaceId: ws.id, docId: targetId, userId });\n}","handlingStrategy":"validation","validationCode":"// Route root-level revocations to the workspace API instead\nif (input.docId === input.workspaceId) {\n  await removeWorkspaceMember(input.workspaceId, input.userId);\n} else {\n  await revokeDocUserRole(input);\n}","typeGuard":"function isExpectToRevokeDocUserRoles(e: unknown): boolean {\n  return (\n    typeof e === 'object' && e !== null &&\n    (e as { extensions?: { code?: string } }).extensions?.code === 'expect_to_revoke_doc_user_roles'\n  );\n}","tryCatchPattern":"try {\n  await revokeDocUserRole(input);\n} catch (e) {\n  if (isExpectToRevokeDocUserRoles(e)) {\n    // input was built for the root; correct the flow instead of retrying\n    await removeWorkspaceMember(input.workspaceId, input.userId);\n  } else throw e;\n}","preventionTips":["Filter the workspace id out of any id list feeding doc-role operations","Make docId a required, explicitly selected field in role-management forms","Distinguish workspace-level removal from doc-level revocation in the UI model"],"tags":["doc","permissions","validation","affine"],"backgroundTag":"invalid-identifier","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"}