toeverything/AFFiNE · error · CanNotBatchGrantDocOwnerPermissions

can_not_batch_grant_doc_owner_permissions

can_not_batch_grant_doc_owner_permissions

Error message

Can not batch grant doc owner permissions.

What it means

CanNotBatchGrantDocOwnerPermissions, thrown by PermissionWriteModel.batchSetUserRoles (permission-write.ts:577-588) - the permission write path enforces the same rule as the doc-user model: DocRole.Owner cannot be batch granted. Here the role guard runs before the empty-list early return, so even userIds = [] with role Owner throws. Other roles are converted with docRoleToNew(role) and upserted per user into docGrant.

Solutions

  1. Route ownership changes through the owner-transfer flow instead of batch grants
  2. Grant Admin/Write/Read in batch - only Owner is refused
  3. Validate the role at the API boundary so callers never reach this throw

Example fix

// before
await permissions.batchSetUserRoles(workspaceId, docId, userIds, role);

// after
if (role === DocRole.Owner) {
  throw new Error('Owner cannot be batch granted; use the owner-transfer flow');
}
await permissions.batchSetUserRoles(workspaceId, docId, userIds, role);
Defensive patterns

Strategy: validation

Validate before calling

if (role === DocRole.Owner) {
  throw new Error('Owner cannot be batch granted; use the owner-transfer flow');
}
await permissions.batchSetUserRoles(workspaceId, docId, userIds, role);

Type guard

const isBatchGrantableRole = (r: DocRole): boolean => r !== DocRole.Owner;

Try / catch

try {
  await permissions.batchSetUserRoles(workspaceId, docId, userIds, role);
} catch (e) {
  if (e instanceof CanNotBatchGrantDocOwnerPermissions) {
    // route to the owner-transfer flow instead
  }
  throw e;
}

Prevention

When it happens

Trigger: batchSetUserRoles(workspaceId, docId, userIds, DocRole.Owner) with any userIds value, including an empty array, since the Owner check precedes the length check in this model.

Common situations: Role dropdowns that include Owner; migrating permission data by mapping an 'owner' flag straight into a batch grant call.

Related errors


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

Appendix: source

Thrown at packages/backend/server/src/models/permission-write.ts:582

      create: {
        workspaceId,
        docId,
        principalType: 'user',
        principalId: userId,
        role: docRoleToNew(role),
      },
    });
  }

  @Transactional()
  async batchSetUserRoles(
    workspaceId: string,
    docId: string,
    userIds: string[],
    role: DocRole
  ) {
    if (role === DocRole.Owner) {
      throw new CanNotBatchGrantDocOwnerPermissions();
    }
    if (userIds.length === 0) {
      return 0;
    }

    const grantRole = docRoleToNew(role);
    for (const userId of userIds) {
      await this.db.docGrant.upsert({
        where: {
          workspaceId_docId_principalType_principalId: {
            workspaceId,
            docId,
            principalType: 'user',
            principalId: userId,
          },
        },
        update: {
          role: grantRole,

View on GitHub (pinned to 2af30773ae)