{"record":{"id":"686479db8dec8735","repo":"toeverything/AFFiNE","slug":"expect-to-grant-doc-user-roles","errorCode":"expect_to_grant_doc_user_roles","errorMessage":"Expect doc not to be workspace","messagePattern":"Expect doc not to be workspace","errorType":"exception","errorClass":"ExpectToGrantDocUserRoles","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/core/workspaces/resolvers/doc.ts","lineNumber":729,"sourceCode":"    );\n  }\n\n  @Mutation(() => Boolean)\n  async grantDocUserRoles(\n    @CurrentUser() user: CurrentUser,\n    @Args('input') input: GrantDocUserRolesInput\n  ): Promise<boolean> {\n    const pairs = {\n      spaceId: input.workspaceId,\n      docId: input.docId,\n    };\n\n    if (input.workspaceId === input.docId) {\n      this.logger.error(\n        'Expect to grant doc user roles, but it is a workspace',\n        pairs\n      );\n      throw new ExpectToGrantDocUserRoles(\n        pairs,\n        'Expect doc not to be workspace'\n      );\n    }\n\n    await this.ac.user(user.id).doc(input).assert('Doc.Users.Manage');\n\n    await this.models.docUser.batchSetUserRoles(\n      input.workspaceId,\n      input.docId,\n      input.userIds,\n      input.role\n    );\n    this.event.emit('doc.grants.changed', {\n      workspaceId: input.workspaceId,\n      docId: input.docId,\n    });\n","sourceCodeStart":711,"sourceCodeEnd":747,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/workspaces/resolvers/doc.ts#L711-L747","documentation":"Thrown by grantDocUserRoles when input.workspaceId === input.docId. Granting per-doc user roles on the workspace root is invalid (the root has no separate doc ACL), so the server rejects it with ExpectToGrantDocUserRoles (invalid_input), attaching { spaceId, docId } to extensions and logging at error level.","triggerScenarios":"Calling grantDocUserRoles with a GrantDocUserRolesInput whose docId equals workspaceId - the client is trying to grant doc-level roles on the workspace root.","commonSituations":"UI passes the workspace id as the docId for the root node; role-management dialog opened on the workspace root; client shares a docId variable across workspace and doc scopes.","solutions":["Pass a real child page id as input.docId, distinct from input.workspaceId.","Disable doc-level role grant in the UI when the selected node is the workspace root.","Add a client-side precondition: input.workspaceId !== input.docId before invoking the mutation."],"exampleFix":"// before\nawait gql.grantDocUserRoles({\n  input: { workspaceId, docId: workspaceId, userIds, role },\n});\n\n// after\nif (docId === workspaceId) {\n  throw new Error('Use workspace member roles for the root, not doc roles');\n}\nawait gql.grantDocUserRoles({\n  input: { workspaceId, docId, userIds, role },\n});","handlingStrategy":"validation","validationCode":"// Reject the input before calling the mutation\nif (input.workspaceId === input.docId) {\n  throw new Error('Use workspace member roles for the root, not doc roles');\n}\nawait gql.grantDocUserRoles({ input });","typeGuard":"function isExpectToGrantDocUserRoles(e: unknown): boolean {\n  return (\n    typeof e === 'object' &&\n    e !== null &&\n    (e as any).extensions?.code === 'expect_to_grant_doc_user_roles'\n  );\n}","tryCatchPattern":"try {\n  await gql.grantDocUserRoles({ input });\n} catch (e) {\n  if (isExpectToGrantDocUserRoles(e)) {\n    // selection bug; do not retry, fix the docId source\n    reportBug('grantDocUserRoles called with workspaceId as docId');\n    return;\n  }\n  throw e;\n}","preventionTips":["Disable the 'manage doc roles' action on the workspace root in the UI.","Validate input.workspaceId !== input.docId in the client SDK before sending.","Keep role-grant scopes typed (workspace roles vs doc roles) to prevent misuse."],"tags":["graphql","doc","permissions","validation"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}