toeverything/AFFiNE · error · ExpectToRevokeDocUserRoles
expect_to_revoke_doc_user_roles
expect_to_revoke_doc_user_roles
Error message
Expect doc not to be workspace
What it means
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.
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
Example fix
// before
await revokeDocUserRole({ workspaceId: ws.id, docId: targetId /* sometimes ws.id */, userId });
// after
if (targetId === ws.id) {
await removeWorkspaceMember(ws.id, userId); // workspace-level removal
} else {
await revokeDocUserRole({ workspaceId: ws.id, docId: targetId, userId });
} Defensive patterns
Strategy: validation
Validate before calling
// Route root-level revocations to the workspace API instead
if (input.docId === input.workspaceId) {
await removeWorkspaceMember(input.workspaceId, input.userId);
} else {
await revokeDocUserRole(input);
} Type guard
function isExpectToRevokeDocUserRoles(e: unknown): boolean {
return (
typeof e === 'object' && e !== null &&
(e as { extensions?: { code?: string } }).extensions?.code === 'expect_to_revoke_doc_user_roles'
);
} Try / catch
try {
await revokeDocUserRole(input);
} catch (e) {
if (isExpectToRevokeDocUserRoles(e)) {
// input was built for the root; correct the flow instead of retrying
await removeWorkspaceMember(input.workspaceId, input.userId);
} else throw e;
} Prevention
- 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
When it happens
Trigger: Calling revokeDocUserRole with input.docId === input.workspaceId — usually a stale id or an unpopulated form field defaulting to the space id.
Common situations: Role-management UIs operating on a synthetic root node; cleanup scripts iterating ids that include the workspace id.
Related errors
- doc_default_role_can_not_be_owner
- expect_to_grant_doc_user_roles
- expect_to_update_doc_user_role
- expect_to_publish_doc
- expect_to_revoke_public_doc
AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-08-18).
Data as JSON: /api/errors/57ce89b5f9cb667b.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/core/workspaces/resolvers/doc.ts:804
this.logger.log(`Grant doc user roles (${JSON.stringify(info)})`);
return true;
}
@Mutation(() => Boolean)
async revokeDocUserRoles(
@CurrentUser() user: CurrentUser,
@Args('input') input: RevokeDocUserRoleInput
): Promise<boolean> {
const pairs = {
spaceId: input.workspaceId,
docId: input.docId,
};
if (input.workspaceId === input.docId) {
this.logger.error(
'Expect to revoke doc user roles, but it is a workspace',
pairs
);
throw new ExpectToRevokeDocUserRoles(
pairs,
'Expect doc not to be workspace'
);
}
try {
await this.runtime.executeDomainCommandV1({
command: 'transition_doc_role',
actorUserId: user.id,
workspaceId: input.workspaceId,
docId: input.docId,
targetUserId: input.userId,
});
} catch (error) {
if (backendRuntimeErrorCode(error) === 'domain_permission_denied') {
throw new DocActionDenied({
action: 'Doc.Users.Manage',
...pairs,
});View on GitHub (pinned to 2af30773ae)