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

grantDocUserRoles rejects input where workspaceId === docId: the workspace root is a Space, and doc-level user roles only apply to real pages. The check runs before Doc.Users.Manage is asserted, so this fires even for callers who would otherwise be authorized.

Solutions

  1. Validate input client-side: workspaceId and docId must differ before calling the mutation
  2. Source docId from a page/doc object, never from the workspace
  3. For workspace-wide roles use the workspace member APIs instead of doc roles

Example fix

// before
await grantDocUserRoles({ workspaceId: ws.id, docId: ws.id, userIds, role }); // root -> EXPECT_TO_GRANT_DOC_USER_ROLES

// after
const page = getSelectedPage(); // real page id
if (!page || page.id === ws.id) throw new Error('select a page first');
await grantDocUserRoles({ workspaceId: ws.id, docId: page.id, userIds, role });
Defensive patterns

Strategy: validation

Validate before calling

// Validate the grant input before calling the mutation
function validDocRoleInput(workspaceId: string, docId: string | undefined): boolean {
  return !!docId && docId !== workspaceId;
}
if (!validDocRoleInput(input.workspaceId, input.docId)) {
  throw new Error('docId must reference a page, not the workspace');
}
await grantDocUserRoles(input);

Type guard

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

Try / catch

try {
  await grantDocUserRoles(input);
} catch (e) {
  if (isExpectToGrantDocUserRoles(e)) {
    resetShareDialogToPageSelection(); // user targeted the root; ask for a page
  } else throw e;
}

Prevention

When it happens

Trigger: Calling grantDocUserRoles with input.docId equal to input.workspaceId — client building the input from a workspace/root object or defaulting docId to the workspace id.

Common situations: Sharing flows that treat the workspace root as a page; forms where docId was never populated and fell back to the space id.

Related errors


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

Appendix: source

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

    );
  }

  @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'
      );
    }

    const role = toDomainDocRole(input.role);
    if (!role || role === 'owner') {
      throw new ExpectToGrantDocUserRoles(pairs, 'Invalid grant role');
    }
    try {
      await this.runtime.executeDomainCommandV1({
        command: 'grant_doc_roles',
        actorUserId: user.id,
        workspaceId: input.workspaceId,
        docId: input.docId,
        targetUserIds: input.userIds,
        newRole: role,
      });

View on GitHub (pinned to 2af30773ae)