{"record":{"id":"398e56a22968c9d5","repo":"toeverything/AFFiNE","slug":"expect-to-update-doc-user-role","errorCode":"expect_to_update_doc_user_role","errorMessage":"Expect doc not to be workspace","messagePattern":"Expect doc not to be workspace","errorType":"exception","errorClass":"ExpectToUpdateDocUserRole","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/core/workspaces/resolvers/doc.ts","lineNumber":853,"sourceCode":"    this.logger.log(`Revoke doc user roles (${JSON.stringify(info)})`);\n    return true;\n  }\n\n  @Mutation(() => Boolean)\n  async updateDocUserRole(\n    @CurrentUser() user: CurrentUser,\n    @Args('input') input: UpdateDocUserRoleInput\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 update doc user role, but it is a workspace',\n        pairs\n      );\n      throw new ExpectToUpdateDocUserRole(\n        pairs,\n        'Expect doc not to be workspace'\n      );\n    }\n\n    const info = {\n      ...pairs,\n      userId: input.userId,\n      role: input.role,\n    };\n\n    const role = toDomainDocRole(input.role);\n    if (!role) {\n      throw new ExpectToUpdateDocUserRole(pairs, 'Invalid doc role');\n    }\n    try {\n      await this.runtime.executeDomainCommandV1({\n        command: 'transition_doc_role',","sourceCodeStart":835,"sourceCodeEnd":871,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/2af30773aecd567f09b346e7b72fc69143144057/packages/backend/server/src/core/workspaces/resolvers/doc.ts#L835-L871","documentation":"updateDocUserRole rejects input where workspaceId === docId, continuing the root-is-a-Space guard family. Changing a user's role 'on the workspace root' is not a doc-level operation, so the mutation throws ExpectToUpdateDocUserRole before the Doc.Users.Manage check.","triggerScenarios":"Calling updateDocUserRole with input.docId === input.workspaceId — id taken from the wrong object, or a UI whose selected node is the workspace root.","commonSituations":"Role pickers rendered for the root node of a doc tree; refactors that swapped the argument order or default values.","solutions":["Disable the role-change action for the root node and validate docId !== workspaceId before calling","Ensure the selected-node id in the UI comes from a page node","Use workspace member role APIs for workspace-wide changes"],"exampleFix":"// before\nonRoleChange(node.id /* root node === ws.id */, userId, role);\n\n// after\nif (node.id === ws.id) return disableRoleChangeForRoot(node);\nawait updateDocUserRole({ workspaceId: ws.id, docId: node.id, userId, role });","handlingStrategy":"validation","validationCode":"// Guard the update input before calling\nif (input.workspaceId === input.docId) {\n  throw new Error('cannot update a doc role on the workspace root');\n}\nawait updateDocUserRole(input);","typeGuard":"function isExpectToUpdateDocUserRole(e: unknown): boolean {\n  return (\n    typeof e === 'object' && e !== null &&\n    (e as { extensions?: { code?: string } }).extensions?.code === 'expect_to_update_doc_user_role'\n  );\n}","tryCatchPattern":"try {\n  await updateDocUserRole(input);\n} catch (e) {\n  if (isExpectToUpdateDocUserRole(e)) {\n    showSelectAPageError(); // role changes do not apply to the root\n  } else throw e;\n}","preventionTips":["Disable role-change controls when the selected tree node is the workspace root","Take docId from the selected page node, never from route defaults","Share a single isRootGuard(workspaceId, docId) helper across all doc mutation call sites"],"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"}