{"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":810,"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    if (input.role === DocRole.Owner) {\n      await this.ac.user(user.id).doc(input).assert('Doc.TransferOwner');\n      await this.models.docUser.setOwner(\n        input.workspaceId,\n        input.docId,\n        input.userId\n      );","sourceCodeStart":792,"sourceCodeEnd":828,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/workspaces/resolvers/doc.ts#L792-L828","documentation":"Thrown by the updateDocUserRole mutation when input.docId === input.workspaceId. Same root-doc invariant as revokeDocUserRole: the workspace root doc is addressed by workspace-level role APIs, not the doc-grant API. Rejecting early (invalid_input, HTTP 400) prevents a no-op or inconsistent state where doc-level owner/grant logic would run against the workspace root.","triggerScenarios":"Calling mutation updateDocUserRole with UpdateDocUserRoleInput whose docId equals workspaceId (the workspace root doc id passed as docId).","commonSituations":"A shared permission-management component binds both fields to the same workspace id when the user opens the workspace landing page; a script that iterates docs and accidentally includes the workspace root id; stale cached doc id equated to the workspace id.","solutions":["Supply the actual nested doc id as docId; keep workspaceId as the workspace id.","To change a workspace member's role, call grantMember(workspaceId, userId, role) instead.","Guard the caller: skip/redirect when workspaceId === docId."],"exampleFix":"// before\nupdateDocUserRole({ workspaceId: ws.id, docId: ws.id, userId, role });\n// after\nupdateDocUserRole({ workspaceId: ws.id, docId: page.id, userId, role });","handlingStrategy":"validation","validationCode":"function assertUpdateDocUserRoleArgs(input: { workspaceId: string; docId: string }) {\n  if (input.workspaceId === input.docId) {\n    throw new Error('Cannot update doc role on the workspace root; use grantMember instead');\n  }\n}\nassertUpdateDocUserRoleArgs(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":["Bind docId and workspaceId to distinct values from the route.","Route workspace-root permission changes through grantMember/revokeMember.","Add a client-side guard that disables the action when the ids match."],"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"}