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

  1. Guard the call: skip or error client-side when docId equals workspaceId
  2. Fix the id source so docId always references a real page
  3. 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

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


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)