toeverything/AFFiNE · error · ExpectToGrantDocUserRoles

expect_to_grant_doc_user_roles

expect_to_grant_doc_user_roles

Error message

Expect doc not to be workspace

What it means

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.

Source

Thrown at packages/backend/server/src/core/workspaces/resolvers/doc.ts:729

    );
  }

  @Mutation(() => Boolean)
  async grantDocUserRoles(
    @CurrentUser() user: CurrentUser,
    @Args('input') input: GrantDocUserRolesInput
  ): Promise<boolean> {
    const pairs = {
      spaceId: input.workspaceId,
      docId: input.docId,
    };

    if (input.workspaceId === input.docId) {
      this.logger.error(
        'Expect to grant doc user roles, but it is a workspace',
        pairs
      );
      throw new ExpectToGrantDocUserRoles(
        pairs,
        'Expect doc not to be workspace'
      );
    }

    await this.ac.user(user.id).doc(input).assert('Doc.Users.Manage');

    await this.models.docUser.batchSetUserRoles(
      input.workspaceId,
      input.docId,
      input.userIds,
      input.role
    );
    this.event.emit('doc.grants.changed', {
      workspaceId: input.workspaceId,
      docId: input.docId,
    });

View on GitHub (pinned to 26c515e050)

Solutions

  1. Pass a real child page id as input.docId, distinct from input.workspaceId.
  2. Disable doc-level role grant in the UI when the selected node is the workspace root.
  3. Add a client-side precondition: input.workspaceId !== input.docId before invoking the mutation.

Example fix

// before
await gql.grantDocUserRoles({
  input: { workspaceId, docId: workspaceId, userIds, role },
});

// after
if (docId === workspaceId) {
  throw new Error('Use workspace member roles for the root, not doc roles');
}
await gql.grantDocUserRoles({
  input: { workspaceId, docId, userIds, role },
});
Defensive patterns

Strategy: validation

Validate before calling

// Reject the input before calling the mutation
if (input.workspaceId === input.docId) {
  throw new Error('Use workspace member roles for the root, not doc roles');
}
await gql.grantDocUserRoles({ input });

Type guard

function isExpectToGrantDocUserRoles(e: unknown): boolean {
  return (
    typeof e === 'object' &&
    e !== null &&
    (e as any).extensions?.code === 'expect_to_grant_doc_user_roles'
  );
}

Try / catch

try {
  await gql.grantDocUserRoles({ input });
} catch (e) {
  if (isExpectToGrantDocUserRoles(e)) {
    // selection bug; do not retry, fix the docId source
    reportBug('grantDocUserRoles called with workspaceId as docId');
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling grantDocUserRoles with a GrantDocUserRolesInput whose docId equals workspaceId - the client is trying to grant doc-level roles on the workspace root.

Common situations: 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.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/686479db8dec8735. Report an issue: GitHub.