{"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":771,"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    await this.ac.user(user.id).doc(input).assert('Doc.Users.Manage');\n\n    await this.models.docUser.delete(\n      input.workspaceId,\n      input.docId,\n      input.userId\n    );\n    this.event.emit('doc.grants.changed', {\n      workspaceId: input.workspaceId,\n      docId: input.docId,\n    });\n\n    const info = {\n      ...pairs,","sourceCodeStart":753,"sourceCodeEnd":789,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/workspaces/resolvers/doc.ts#L753-L789","documentation":"Thrown by the revokeDocUserRole GraphQL mutation when input.docId === input.workspaceId. In this codebase a workspace's root doc shares the workspace's id, so the per-doc grant APIs do not apply to it; access to the root is governed entirely by workspace membership. The guard exists because calling revokeDocUserRole on the workspace root would silently no-op or corrupt workspace-level permissions, so it is rejected up front as invalid_input (HTTP 400).","triggerScenarios":"Calling mutation revokeDocUserRole with RevokeDocUserRoleInput where docId and workspaceId are the same string value (e.g. passing the workspace id as docId).","commonSituations":"A frontend 'manage doc access' screen is reused for the workspace root page and fills both fields from the same workspace id; an older client that never split the two ids; copying a workspace id from the URL into both args of a manual GraphQL request.","solutions":["Pass the nested doc's id as docId and keep the workspace id in workspaceId.","If you intend to remove a workspace member, call revokeMember(workspaceId, userId) instead.","Add a client-side precondition: if (workspaceId === docId) route to the workspace-member flow rather than the doc-grant flow."],"exampleFix":"// before\nrevokeDocUserRole({ workspaceId: ws.id, docId: ws.id, userId });\n// after\nrevokeDocUserRole({ workspaceId: ws.id, docId: page.id, userId });","handlingStrategy":"validation","validationCode":"function assertRevokeDocArgs(input: { workspaceId: string; docId: string }) {\n  if (!input.workspaceId || !input.docId) throw new Error('workspaceId and docId are required');\n  if (input.workspaceId === input.docId) {\n    throw new Error('Cannot revoke doc roles on the workspace root; use revokeMember instead');\n  }\n}\n// call before revokeDocUserRole\nassertRevokeDocArgs(input);","typeGuard":"function isNestedDocInput(input: { workspaceId: string; docId: string }): boolean {\n  return Boolean(input.workspaceId) && Boolean(input.docId) && input.workspaceId !== input.docId;\n}","tryCatchPattern":null,"preventionTips":["Never pass the workspace id as docId; the workspace root doc shares the workspace id.","Differentiate the workspace-member flow from the doc-grant flow in the UI.","Unit-test the precondition workspaceId !== docId for all doc-grant calls."],"tags":["graphql","permissions","workspace","validation","doc-role"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}