Repository: toeverything/AFFiNE
Pull request #12311: feat(server): filter docs by access role
Author: login: fengmk2
Labels: name: app:server
name: test
description: Related to test cases
Created: 2025-05-15T13:04:14Z | Merged: 2025-05-19T03:52:22Z

close CLOUD-208

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Enhanced document access control with batch permission checks, enabling efficient filtering of documents based on user roles and permissions.
  - Added detailed document-level role and permission management for workspace users.
- **Bug Fixes**
  - Improved accuracy in filtering search results to only display documents users have permission to read.
- **Tests**
  - Added comprehensive tests for document-level permission filtering and search result accuracy.
  - Introduced new mock utilities to support permission-related test scenarios.
- **Refactor**
  - Simplified and optimized permission logic for determining user roles and document access.
- **Documentation**
  - Updated type definitions for improved clarity in permission handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/packages/backend/server/src/__tests__/e2e/indexer/__snapshots__/search.spec.ts.md b/packages/backend/server/src/__tests__/e2e/indexer/__snapshots__/search.spec.ts.md
index bad36d4a8b965..e975fdabddd15 100644
--- a/packages/backend/server/src/__tests__/e2e/indexer/__snapshots__/search.spec.ts.md
+++ b/packages/backend/server/src/__tests__/e2e/indexer/__snapshots__/search.spec.ts.md
@@ -34,3 +34,107 @@ Generated by [AVA](https://avajs.dev).
         highlights: null,
       },
     ]
+
+## should filter no read permission docs on team workspace
+
+> Snapshot 1
+
+    [
+      {
+        fields: {
+          blockId: [
+            'block-0',
+          ],
+          docId: [
+            'doc-0',
+          ],
+        },
+        highlights: null,
+      },
+      {
+        fields: {
+          blockId: [
+            'block-2',
+          ],
+          docId: [
+            'doc-2',
+          ],
+          ref: [
+            '{"foo": "bar1"}',
+            '{"foo": "bar3"}',
+          ],
+          refDocId: [
+            'doc-0',
+            'doc-2',
+          ],
+        },
+        highlights: null,
+      },
+      {
+        fields: {
+          blockId: [
+            'block-1',
+          ],
+          docId: [
+            'doc-1',
+          ],
+          ref: [
+            '{"foo": "bar1"}',
+          ],
+          refDocId: [
+            'doc-0',
+          ],
+        },
+        highlights: null,
+      },
+    ]
+
+> Snapshot 2
+
+    [
+      {
+        fields: {
+          blockId: [
+            'block-0',
+          ],
+          docId: [
+            'doc-0',
+          ],
+        },
+        highlights: null,
+      },
+      {
+        fields: {
+          blockId: [
+            'block-1',
+          ],
+          docId: [
+            'doc-1',
+          ],
+          ref: [
+            '{"foo": "bar1"}',
+          ],
+          refDocId: [
+            'doc-0',
+          ],
+        },
+        highlights: null,
+      },
+    ]
+
+## should return empty results when search not match any docs
+
+> Snapshot 1
+
+    {
+      workspace: {
+        search: {
+          nodes: [],
+          pagination: {
+            count: 0,
+            hasMore: false,
+            nextCursor: null,
+          },
+        },
+      },
+    }
diff --git a/packages/backend/server/src/__tests__/e2e/indexer/__snapshots__/search.spec.ts.snap b/packages/backend/server/src/__tests__/e2e/indexer/__snapshots__/search.spec.ts.snap
index a1ceaa40cab38..ec5c80acce1bd 100644
Binary files a/packages/backend/server/src/__tests__/e2e/indexer/__snapshots__/search.spec.ts.snap and b/packages/backend/server/src/__tests__/e2e/indexer/__snapshots__/search.spec.ts.snap differ
diff --git a/packages/backend/server/src/__tests__/e2e/indexer/search.spec.ts b/packages/backend/server/src/__tests__/e2e/indexer/search.spec.ts
index 3a53a8c557248..ab136518a7d1a 100644
--- a/packages/backend/server/src/__tests__/e2e/indexer/search.spec.ts
+++ b/packages/backend/server/src/__tests__/e2e/indexer/search.spec.ts
@@ -5,6 +5,7 @@ import {
   SearchTable,
 } from '@affine/graphql';
 
+import { DocRole } from '../../../models';
 import { IndexerService } from '../../../plugins/indexer/service';
 import { Mockers } from '../../mocks';
 import { app, e2e } from '../test';
@@ -106,3 +107,172 @@ e2e('should search with query', async t => {
   t.is(result.workspace.search.nodes.length, 2);
   t.snapshot(result.workspace.search.nodes);
 });
+
+e2e('should filter no read permission docs on team workspace', async t => {
+  const owner = await app.signup();
+  const workspace = await app.create(Mockers.Workspace, {
+    owner,
+  });
+  await app.create(Mockers.TeamWorkspace, {
+    id: workspace.id,
+  });
+
+  const indexerService = app.get(IndexerService);
+  await indexerService.write(
+    SearchTable.block,
+    [
+      {
+        docId: 'doc-0',
+        workspaceId: workspace.id,
+        content: 'test1',
+        flavour: 'markdown',
+        blockId: 'block-0',
+        createdByUserId: owner.id,
+        updatedByUserId: owner.id,
+        createdAt: new Date('2025-04-22T00:00:00.000Z'),
+        updatedAt: new Date('2025-04-22T00:00:00.000Z'),
+      },
+      {
+        docId: 'doc-1',
+        workspaceId: workspace.id,
+        content: 'test2',
+        flavour: 'markdown',
+        blockId: 'block-1',
+        refDocId: ['doc-0'],
+        ref: ['{"foo": "bar1"}'],
+        createdByUserId: owner.id,
+        updatedByUserId: owner.id,
+        createdAt: new Date('2021-04-22T00:00:00.000Z'),
+        updatedAt: new Date('2021-04-22T00:00:00.000Z'),
+      },
+      {
+        docId: 'doc-2',
+        workspaceId: workspace.id,
+        content: 'test3',
+        flavour: 'markdown',
+        blockId: 'block-2',
+        refDocId: ['doc-0', 'doc-2'],
+        ref: ['{"foo": "bar1"}', '{"foo": "bar3"}'],
+        createdByUserId: owner.id,
+        updatedByUserId: owner.id,
+        createdAt: new Date('2025-03-22T00:00:00.000Z'),
+        updatedAt: new Date('2025-03-22T00:00:00.000Z'),
+      },
+    ],
+    {
+      refresh: true,
+    }
+  );
+  // set all docs to no access
+  await app.create(Mockers.DocMeta, {
+    workspaceId: workspace.id,
+    docId: 'doc-0',
+    defaultRole: DocRole.None,
+  });
+  await app.create(Mockers.DocMeta, {
+    workspaceId: workspace.id,
+    docId: 'doc-1',
+    defaultRole: DocRole.None,
+  });
+  await app.create(Mockers.DocMeta, {
+    workspaceId: workspace.id,
+    docId: 'doc-2',
+    defaultRole: DocRole.None,
+  });
+
+  // owner can read all docs
+  const result = await app.gql({
+    query: indexerSearchQuery,
+    variables: {
+      id: workspace.id,
+      input: {
+        table: SearchTable.block,
+        query: {
+          type: SearchQueryType.match,
+          field: 'workspaceId',
+          match: workspace.id,
+        },
+        options: {
+          fields: ['docId', 'blockId', 'refDocId', 'ref'],
+          pagination: {
+            limit: 100,
+          },
+        },
+      },
+    },
+  });
+
+  t.snapshot(result.workspace.search.nodes);
+
+  // other user can only read docs that they have read permission
+  const other = await app.signup();
+  await app.create(Mockers.WorkspaceUser, {
+    workspaceId: workspace.id,
+    userId: other.id,
+  });
+  await app.create(Mockers.DocUser, {
+    workspaceId: workspace.id,
+    docId: 'doc-0',
+    userId: other.id,
+    type: DocRole.Reader,
+  });
+  await app.create(Mockers.DocUser, {
+    workspaceId: workspace.id,
+    docId: 'doc-1',
+    userId: other.id,
+    type: DocRole.Manager,
+  });
+
+  const otherResult = await app.gql({
+    query: indexerSearchQuery,
+    variables: {
+      id: workspace.id,
+      input: {
+        table: SearchTable.block,
+        query: {
+          type: SearchQueryType.match,
+          field: 'workspaceId',
+          match: workspace.id,
+        },
+        options: {
+          fields: ['docId', 'blockId', 'refDocId', 'ref'],
+          pagination: {
+            limit: 100,
+          },
+        },
+      },
+    },
+  });
+
+  t.snapshot(otherResult.workspace.search.nodes);
+});
+
+e2e('should return empty results when search not match any docs', async t => {
+  const owner = await app.signup();
+  const workspace = await app.create(Mockers.Workspace, {
+    owner,
+  });
+
+  const result = await app.gql({
+    query: indexerSearchQuery,
+    variables: {
+      id: workspace.id,
+      input: {
+        table: SearchTable.block,
+        query: {
+          type: SearchQueryType.match,
+          field: 'workspaceId',
+          match: workspace.id,
+        },
+        options: {
+          fields: ['docId', 'blockId', 'refDocId', 'ref'],
+          pagination: {
+            limit: 100,
+          },
+        },
+      },
+    },
+  });
+
+  t.snapshot(result);
+});
diff --git a/packages/backend/server/src/__tests__/mocks/doc-user.mock.ts b/packages/backend/server/src/__tests__/mocks/doc-user.mock.ts
new file mode 100644
index 0000000000000..da6fa3623fa58
--- /dev/null
+++ b/packages/backend/server/src/__tests__/mocks/doc-user.mock.ts
@@ -0,0 +1,16 @@
+import type { WorkspaceDocUserRole } from '@prisma/client';
+import { Prisma } from '@prisma/client';
+
+import { Mocker } from './factory';
+
+export type MockDocUserInput = Prisma.WorkspaceDocUserRoleUncheckedCreateInput;
+
+export type MockedDocUser = WorkspaceDocUserRole;
+
+export class MockDocUser extends Mocker<MockDocUserInput, MockedDocUser> {
+  override async create(input: MockDocUserInput) {
+    return await this.db.workspaceDocUserRole.create({
+      data: input,
+    });
+  }
+}
diff --git a/packages/backend/server/src/__tests__/mocks/index.ts b/packages/backend/server/src/__tests__/mocks/index.ts
index b6abf141c8174..bc5b4d4241c7c 100644
--- a/packages/backend/server/src/__tests__/mocks/index.ts
+++ b/packages/backend/server/src/__tests__/mocks/index.ts
@@ -7,6 +7,7 @@ export * from './workspace-user.mock';
 import { MockCopilotProvider } from './copilot.mock';
 import { MockDocMeta } from './doc-meta.mock';
 import { MockDocSnapshot } from './doc-snapshot.mock';
+import { MockDocUser } from './doc-user.mock';
 import { MockEventBus } from './eventbus.mock';
 import { MockMailer } from './mailer.mock';
 import { MockJobQueue } from './queue.mock';
@@ -24,6 +25,7 @@ export const Mockers = {
   UserSettings: MockUserSettings,
   DocMeta: MockDocMeta,
   DocSnapshot: MockDocSnapshot,
+  DocUser: MockDocUser,
 };
 
 export { MockCopilotProvider, MockEventBus, MockJobQueue, MockMailer };
diff --git a/packages/backend/server/src/core/permission/__tests__/__snapshots__/docs.spec.ts.md b/packages/backend/server/src/core/permission/__tests__/__snapshots__/docs.spec.ts.md
new file mode 100644
index 0000000000000..70353c89fc583
--- /dev/null
+++ b/packages/backend/server/src/core/permission/__tests__/__snapshots__/docs.spec.ts.md
@@ -0,0 +1,62 @@
+# Snapshot report for `src/core/permission/__tests__/docs.spec.ts`
+
+The actual snapshot is saved in `docs.spec.ts.snap`.
+
+Generated by [AVA](https://avajs.dev).
+
+## should filter docs by Doc.Read
+
+> Snapshot 1
+
+    [
+      {
+        docId: 'doc1',
+      },
+      {
+        docId: 'doc2',
+      },
+      {
+        docId: 'doc3',
+      },
+    ]
+
+> Snapshot 2
+
+    [
+      {
+        docId: 'doc1',
+      },
+      {
+        docId: 'doc2',
+      },
+      {
+        docId: 'doc3',
+      },
+    ]
+
+## should filter docs by Doc.Publish
+
+> Snapshot 1
+
+    [
+      {
+        docId: 'doc1',
+      },
+      {
+        docId: 'doc2',
+      },
+      {
+        docId: 'doc3',
+      },
+    ]
+
+> Snapshot 2
+
+    [
+      {
+        docId: 'doc2',
+      },
+      {
+        docId: 'doc3',
+      },
+    ]
diff --git a/packages/backend/server/src/core/permission/__tests__/__snapshots__/docs.spec.ts.snap b/packages/backend/server/src/core/permission/__tests__/__snapshots__/docs.spec.ts.snap
new file mode 100644
index 0000000000000..739edc5d49b5f
Binary files /dev/null and b/packages/backend/server/src/core/permission/__tests__/__snapshots__/docs.spec.ts.snap differ
diff --git a/packages/backend/server/src/core/permission/__tests__/doc.spec.ts b/packages/backend/server/src/core/permission/__tests__/doc.spec.ts
index 88440e7eed276..b7c387f568a85 100644
--- a/packages/backend/server/src/core/permission/__tests__/doc.spec.ts
+++ b/packages/backend/server/src/core/permission/__tests__/doc.spec.ts
@@ -21,7 +21,7 @@ let ws: Workspace;
 test.before(async () => {
   module = await createTestingModule({ imports: [PermissionModule] });
   models = module.get<Models>(Models);
-  ac = new DocAccessController(models);
+  ac = new DocAccessController();
 });
 
 test.beforeEach(async () => {
diff --git a/packages/backend/server/src/core/permission/__tests__/docs.spec.ts b/packages/backend/server/src/core/permission/__tests__/docs.spec.ts
new file mode 100644
index 0000000000000..5ba1431ffde6b
--- /dev/null
+++ b/packages/backend/server/src/core/permission/__tests__/docs.spec.ts
@@ -0,0 +1,144 @@
+import test from 'ava';
+
+import { createModule } from '../../../__tests__/create-module';
+import { Mockers } from '../../../__tests__/mocks';
+import { DocRole, PermissionModule, WorkspaceRole } from '..';
+import { AccessControllerBuilder } from '../builder';
+
+const module = await createModule({
+  imports: [PermissionModule],
+});
+
+const builder = module.get(AccessControllerBuilder);
+
+test.after.always(async () => {
+  await module.close();
+});
+
+test('should filter docs by Doc.Read', async t => {
+  const owner = await module.create(Mockers.User);
+  const workspace = await module.create(Mockers.Workspace, {
+    owner,
+  });
+
+  const docs1 = await builder
+    .user(owner.id)
+    .workspace(workspace.id)
+    .docs(
+      [{ docId: 'doc1' }, { docId: 'doc2' }, { docId: 'doc3' }],
+      'Doc.Read'
+    );
+
+  t.is(docs1.length, 3);
+  t.snapshot(docs1);
+
+  // member should have access to the docs
+  const member = await module.create(Mockers.User);
+  await module.create(Mockers.WorkspaceUser, {
+    workspaceId: workspace.id,
+    userId: member.id,
+    type: WorkspaceRole.Collaborator,
+  });
+
+  await module.create(Mockers.DocUser, {
+    workspaceId: workspace.id,
+    docId: 'doc1',
+    userId: member.id,
+    type: DocRole.Reader,
+  });
+
+  await module.create(Mockers.DocUser, {
+    workspaceId: workspace.id,
+    docId: 'doc2',
+    userId: member.id,
+    type: DocRole.Manager,
+  });
+
+  const docs2 = await builder
+    .user(member.id)
+    .workspace(workspace.id)
+    .docs(
+      [{ docId: 'doc1' }, { docId: 'doc2' }, { docId: 'doc3' }],
+      'Doc.Read'
+    );
+
+  t.is(docs2.length, 3);
+  t.snapshot(docs2);
+
+  // other user should not have access to the docs
+  const other = await module.create(Mockers.User);
+
+  const docs3 = await builder
+    .user(other.id)
+    .workspace(workspace.id)
+    .docs(
+      [{ docId: 'doc1' }, { docId: 'doc2' }, { docId: 'doc3' }],
+      'Doc.Read'
+    );
+
+  t.is(docs3.length, 0);
+});
+
+test('should filter docs by Doc.Publish', async t => {
+  const owner = await module.create(Mockers.User);
+  const workspace = await module.create(Mockers.Workspace, {
+    owner,
+  });
+
+  const docs1 = await builder
+    .user(owner.id)
+    .workspace(workspace.id)
+    .docs(
+      [{ docId: 'doc1' }, { docId: 'doc2' }, { docId: 'doc3' }],
+      'Doc.Publish'
+    );
+
+  t.is(docs1.length, 3);
+  t.snapshot(docs1);
+
+  // member should have access to the docs
+  const member = await module.create(Mockers.User);
+  await module.create(Mockers.WorkspaceUser, {
+    workspaceId: workspace.id,
+    userId: member.id,
+    type: WorkspaceRole.Collaborator,
+  });
+
+  await module.create(Mockers.DocUser, {
+    workspaceId: workspace.id,
+    docId: 'doc1',
+    userId: member.id,
+    type: DocRole.Reader,
+  });
+
+  await module.create(Mockers.DocUser, {
+    workspaceId: workspace.id,
+    docId: 'doc2',
+    userId: member.id,
+    type: DocRole.Manager,
+  });
+
+  const docs2 = await builder
+    .user(member.id)
+    .workspace(workspace.id)
+    .docs(
+      [{ docId: 'doc1' }, { docId: 'doc2' }, { docId: 'doc3' }],
+      'Doc.Publish'
+    );
+
+  t.is(docs2.length, 2);
+  t.snapshot(docs2);
+
+  // other user should not have access to the docs
+  const other = await module.create(Mockers.User);
+
+  const docs3 = await builder
+    .user(other.id)
+    .workspace(workspace.id)
+    .docs(
+      [{ docId: 'doc1' }, { docId: 'doc2' }, { docId: 'doc3' }],
+      'Doc.Publish'
+    );
+
+  t.is(docs3.length, 0);
+});
diff --git a/packages/backend/server/src/core/permission/builder.ts b/packages/backend/server/src/core/permission/builder.ts
index ab391a07d2185..b7ff4a9cd2081 100644
--- a/packages/backend/server/src/core/permission/builder.ts
+++ b/packages/backend/server/src/core/permission/builder.ts
@@ -4,6 +4,7 @@ import { DocID } from '../utils/doc';
 import { getAccessController } from './controller';
 import { Resource } from './resource';
 import { DocAction, WorkspaceAction } from './types';
+import { WorkspaceAccessController } from './workspace';
 
 @Injectable()
 export class AccessControllerBuilder {
@@ -67,6 +68,28 @@ class WorkspaceAccessControllerBuilder {
     });
   }
 
+  /**
+   * Filter items by doc access permission
+   * @param items - items to filter
+   * @param action - action to check
+   * @returns filtered items
+   */
+  async docs<T extends { docId: string }>(
+    items: T[],
+    action: DocAction
+  ): Promise<T[]> {
+    const docIds = items.map(item => item.docId);
+    const checker = getAccessController('ws') as WorkspaceAccessController;
+    const docRoles = await checker.docRoles(this.data, docIds);
+    const docRolesMap = new Map(
+      docRoles.map((role, index) => [docIds[index], role])
+    );
+
+    return items.filter(item => {
+      return docRolesMap.get(item.docId)?.permissions[action];
+    });
+  }
+
   async assert(action: WorkspaceAction) {
     const checker = getAccessController('ws');
     await checker.assert(this.data, action);
diff --git a/packages/backend/server/src/core/permission/doc.ts b/packages/backend/server/src/core/permission/doc.ts
index 9f930baf9e154..47d0dfe8d9972 100644
--- a/packages/backend/server/src/core/permission/doc.ts
+++ b/packages/backend/server/src/core/permission/doc.ts
@@ -1,16 +1,13 @@
 import { Injectable } from '@nestjs/common';
 
 import { DocActionDenied } from '../../base';
-import { Models } from '../../models';
 import { AccessController, getAccessController } from './controller';
 import type { Resource } from './resource';
 import {
   DocAction,
   docActionRequiredRole,
   DocRole,
-  fixupDocRole,
   mapDocRoleToPermissions,
-  WorkspaceRole,
 } from './types';
 import { WorkspaceAccessController } from './workspace';
 
@@ -18,10 +15,6 @@ import { WorkspaceAccessController } from './workspace';
 export class DocAccessController extends AccessController<'doc'> {
   protected readonly type = 'doc';
 
-  constructor(private readonly models: Models) {
-    super();
-  }
-
   async role(resource: Resource<'doc'>) {
     const role = await this.getRole(resource);
 
@@ -63,55 +56,9 @@ export class DocAccessController extends AccessController<'doc'> {
     const workspaceController = getAccessController(
       'ws'
     ) as WorkspaceAccessController;
-    const workspaceRole = await workspaceController.getRole(payload);
-
-    const userRole = await this.models.docUser.get(
-      payload.workspaceId,
+    const docRoles = await workspaceController.getDocRoles(payload, [
       payload.docId,
-      payload.userId
-    );
-
-    let docRole = userRole?.type ?? (null as DocRole | null);
-
-    // fallback logic
-    if (docRole === null) {
-      const defaultDocRole = await this.defaultDocRole(
-        payload.workspaceId,
-        payload.docId
-      );
-
-      // if user is in workspace but doc role is not set, fallback to default doc role
-      if (workspaceRole !== null && workspaceRole !== WorkspaceRole.External) {
-        docRole =
-          defaultDocRole.external !== null
-            ? // edgecase: when doc role set to [None] for workspace member, but doc is public, we should fallback to external role
-              Math.max(defaultDocRole.workspace, defaultDocRole.external)
-            : defaultDocRole.workspace;
-      } else {
-        // else fallback to external doc role
-        docRole = defaultDocRole.external;
-      }
-    }
-
-    // we need to fixup doc role to make sure it's not miss set
-    // for example: workspace owner will have doc owner role
-    //              workspace external will not have role higher than editor
-    const role = fixupDocRole(workspaceRole, docRole);
-
-    // never return [None]
-    return role === DocRole.None ? null : role;
-  }
-
-  private async defaultDocRole(workspaceId: string, docId: string) {
-    const doc = await this.models.doc.getMeta(workspaceId, docId, {
-      select: {
-        public: true,
-        defaultRole: true,
-      },
-    });
-    return {
-      external: doc?.public ? DocRole.External : null,
-      workspace: doc?.defaultRole ?? DocRole.Manager,
-    };
+    ]);
+    return docRoles[0];
   }
 }
diff --git a/packages/backend/server/src/core/permission/types.ts b/packages/backend/server/src/core/permission/types.ts
index 8934ddb3746a9..f9a452d1d9d35 100644
--- a/packages/backend/server/src/core/permission/types.ts
+++ b/packages/backend/server/src/core/permission/types.ts
@@ -150,6 +150,8 @@ type ResourceActionName<T extends keyof typeof Actions> =
 export type WorkspaceAction = ResourceActionName<'Workspace'>;
 export type DocAction = ResourceActionName<'Doc'>;
 export type Action = WorkspaceAction | DocAction;
+export type WorkspaceActionPermissions = Record<WorkspaceAction, boolean>;
+export type DocActionPermissions = Record<DocAction, boolean>;
 
 const cache = new WeakMap<object, any>();
 const buildPathReader = (
@@ -194,13 +196,10 @@ export const DOC_ACTIONS = RoleActionsMap.DocRole[DocRole.Owner];
 export function mapWorkspaceRoleToPermissions(
   workspaceRole: WorkspaceRole | null
 ) {
-  const permissions = WORKSPACE_ACTIONS.reduce(
-    (map, action) => {
-      map[action] = false;
-      return map;
-    },
-    {} as Record<WorkspaceAction, boolean>
-  );
+  const permissions = WORKSPACE_ACTIONS.reduce((map, action) => {
+    map[action] = false;
+    return map;
+  }, {} as WorkspaceActionPermissions);
 
   if (workspaceRole === null) {
     return permissions;
@@ -214,13 +213,10 @@ export function mapWorkspaceRoleToPermissions(
 }
 
 export function mapDocRoleToPermissions(docRole: DocRole | null) {
-  const permissions = DOC_ACTIONS.reduce(
-    (map, action) => {
-      map[action] = false;
-      return map;
-    },
-    {} as Record<DocAction, boolean>
-  );
+  const permissions = DOC_ACTIONS.reduce((map, action) => {
+    map[action] = false;
+    return map;
+  }, {} as DocActionPermissions);
 
   if (docRole === null || docRole === DocRole.None) {
     return permissions;
diff --git a/packages/backend/server/src/core/permission/workspace.ts b/packages/backend/server/src/core/permission/workspace.ts
index 59b384df843c3..b492f39a09ef3 100644
--- a/packages/backend/server/src/core/permission/workspace.ts
+++ b/packages/backend/server/src/core/permission/workspace.ts
@@ -1,10 +1,12 @@
 import { Injectable } from '@nestjs/common';
 
 import { SpaceAccessDenied } from '../../base';
-import { Models } from '../../models';
+import { DocRole, Models } from '../../models';
 import { AccessController } from './controller';
 import type { Resource } from './resource';
 import {
+  fixupDocRole,
+  mapDocRoleToPermissions,
   mapWorkspaceRoleToPermissions,
   WorkspaceAction,
   workspaceActionRequiredRole,
@@ -74,6 +76,104 @@ export class WorkspaceAccessController extends AccessController<'ws'> {
     return role;
   }
 
+  async docRoles(payload: Resource<'ws'>, docIds: string[]) {
+    const docRoles = await this.getDocRoles(payload, docIds);
+    return docRoles.map(role => ({
+      role,
+      permissions: mapDocRoleToPermissions(role),
+    }));
+  }
+
+  async getDocRoles(payload: Resource<'ws'>, docIds: string[]) {
+    const docRoles: (DocRole | null)[] = [];
+
+    if (docIds.length === 0) {
+      return docRoles;
+    }
+
+    const workspaceRole = await this.getRole(payload);
+
+    const userRoles = await this.models.docUser.findMany(
+      payload.workspaceId,
+      docIds,
+      payload.userId
+    );
+    const userRolesMap = new Map(userRoles.map(role => [role.docId, role]));
+
+    const noUserRoleDocIds = docIds.filter(docId => {
+      const userRole = userRolesMap.get(docId);
+      return (userRole?.type ?? null) === null;
+    });
+    const defaultDocRoles =
+      noUserRoleDocIds.length > 0
+        ? await this.getDocDefaultRoles(
+            payload,
+            noUserRoleDocIds,
+            workspaceRole
+          )
+        : [];
+    const defaultDocRolesMap = new Map(
+      defaultDocRoles.map((role, index) => [noUserRoleDocIds[index], role])
+    );
+
+    for (const docId of docIds) {
+      const userRole = userRolesMap.get(docId);
+
+      let docRole: DocRole | null = userRole?.type ?? null;
+
+      // fallback logic
+      if (docRole === null) {
+        docRole = defaultDocRolesMap.get(docId) ?? null;
+      }
+
+      // we need to fixup doc role to make sure it's not miss set
+      // for example: workspace owner will have doc owner role
+      //              workspace external will not have role higher than editor
+      const role = fixupDocRole(workspaceRole, docRole);
+
+      // never return [None]
+      docRoles.push(role === DocRole.None ? null : role);
+    }
+
+    return docRoles;
+  }
+
+  private async getDocDefaultRoles(
+    payload: Resource<'ws'>,
+    docIds: string[],
+    workspaceRole: WorkspaceRole | null
+  ) {
+    const fallbackDocRoles: (DocRole | null)[] = [];
+
+    if (docIds.length === 0) {
+      return fallbackDocRoles;
+    }
+
+    const defaultDocRoles = await this.models.doc.findDefaultRoles(
+      payload.workspaceId,
+      docIds
+    );
+
+    for (const defaultDocRole of defaultDocRoles) {
+      let docRole: DocRole | null;
+      // if user is in workspace but doc role is not set, fallback to default doc role
+      if (workspaceRole !== null && workspaceRole !== WorkspaceRole.External) {
+        docRole =
+          defaultDocRole.external !== null
+            ? // edgecase: when doc role set to [None] for workspace member, but doc is public, we should fallback to external role
+              Math.max(defaultDocRole.workspace, defaultDocRole.external)
+            : defaultDocRole.workspace;
+      } else {
+        // else fallback to external doc role
+        docRole = defaultDocRole.external;
+      }
+
+      fallbackDocRoles.push(docRole);
+    }
+
+    return fallbackDocRoles;
+  }
+
   private async defaultWorkspaceRole(payload: Resource<'ws'>) {
     const ws = await this.models.workspace.get(payload.workspaceId);
 
diff --git a/packages/backend/server/src/models/doc-user.ts b/packages/backend/server/src/models/doc-user.ts
index 8695160a25c09..d8a71e3bda504 100644
--- a/packages/backend/server/src/models/doc-user.ts
+++ b/packages/backend/server/src/models/doc-user.ts
@@ -174,6 +174,18 @@ export class DocUserModel extends BaseModel {
     });
   }
 
+  async findMany(workspaceId: string, docIds: string[], userId: string) {
+    return await this.db.workspaceDocUserRole.findMany({
+      where: {
+        workspaceId,
+        docId: {
+          in: docIds,
+        },
+        userId,
+      },
+    });
+  }
+
   count(workspaceId: string, docId: string) {
     return this.db.workspaceDocUserRole.count({
       where: {
diff --git a/packages/backend/server/src/models/doc.ts b/packages/backend/server/src/models/doc.ts
index d013261bcd7ec..148a9e0d82b8d 100644
--- a/packages/backend/server/src/models/doc.ts
+++ b/packages/backend/server/src/models/doc.ts
@@ -378,6 +378,26 @@ export class DocModel extends BaseModel {
     });
   }
 
+  async findDefaultRoles(workspaceId: string, docIds: string[]) {
+    const docs = await this.findMetas(
+      docIds.map(docId => ({
+        workspaceId,
+        docId,
+      })),
+      {
+        select: {
+          defaultRole: true,
+          public: true,
+        },
+      }
+    );
+
+    return docs.map(doc => ({
+      external: doc?.public ? DocRole.External : null,
+      workspace: doc?.defaultRole ?? DocRole.Manager,
+    }));
+  }
+
   async findAuthors(ids: { workspaceId: string; docId: string }[]) {
     const rows = await this.db.snapshot.findMany({
       where: {
@@ -401,13 +421,29 @@ export class DocModel extends BaseModel {
     );
   }
 
-  async findMetas(ids: { workspaceId: string; docId: string }[]) {
-    const rows = await this.db.workspaceDoc.findMany({
+  async findMetas<Select extends Prisma.WorkspaceDocSelect>(
+    ids: { workspaceId: string; docId: string }[],
+    options?: { select?: Select }
+  ) {
+    let select = options?.select;
+    if (select) {
+      // add workspaceId and docId to the select
+      select = {
+        ...select,
+        workspaceId: true,
+        docId: true,
+      };
+    }
+    const rows = (await this.db.workspaceDoc.findMany({
       where: {
         workspaceId: { in: ids.map(id => id.workspaceId) },
         docId: { in: ids.map(id => id.docId) },
       },
-    });
+      select,
+    })) as (Prisma.WorkspaceDocGetPayload<{ select: Select }> & {
+      workspaceId: string;
+      docId: string;
+    })[];
     const resultMap = new Map(
       rows.map(row => [`${row.workspaceId}-${row.docId}`, row])
     );
diff --git a/packages/backend/server/src/plugins/indexer/resolver.ts b/packages/backend/server/src/plugins/indexer/resolver.ts
index 001c9301f55c5..e4e988f185fc8 100644
--- a/packages/backend/server/src/plugins/indexer/resolver.ts
+++ b/packages/backend/server/src/plugins/indexer/resolver.ts
@@ -113,6 +113,10 @@ export class IndexerResolver {
     user: UserType,
     nodes: SearchNodeWithMeta[]
   ) {
+    if (nodes.length === 0) {
+      return nodes;
+    }
+
     const isTeamWorkspace = await this.models.workspaceFeature.has(
       workspace.id,
       'team_plan_v1'
@@ -120,17 +124,17 @@ export class IndexerResolver {
     if (!isTeamWorkspace) {
       return nodes;
     }
-    const needs: SearchNodeWithMeta[] = [];
-    // TODO(@fengmk2): CLOUD-208 support batch check
-    for (const node of nodes) {
-      const canRead = await this.ac
-        .user(user.id)
-        .doc(node._source.workspaceId, node._source.docId)
-        .can('Doc.Read');
-      if (canRead) {
-        needs.push(node);
-      }
-    }
-    return needs;
+
+    const needs = await this.ac
+      .user(user.id)
+      .workspace(workspace.id)
+      .docs(
+        nodes.map(node => ({
+          node,
+          docId: node._source.docId,
+        })),
+        'Doc.Read'
+      );
+    return needs.map(node => node.node);
   }
 }


Reviews:
user: login: fengmk2
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-15T13:06:24Z
user: login: coderabbitai[bot]
body: > [!CAUTION]
> Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.  If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

**Actionable comments posted: 4**

<details>
<summary>🔭 Outside diff range comments (2)</summary><blockquote>

<details>
<summary>packages/backend/server/src/core/permission/doc.ts (1)</summary><blockquote>

`55-63`: _🛠️ Refactor suggestion_

_⚠️ Potential issue_

**Return type can be `undefined` – breaks the declared contract**

`getRole` is declared to return `DocRole | null`, but `docRoles[0]` may be `undefined` when no role is found.  `undefined` is not assignable to the declared return type and can surface as a runtime bug once `strictNullChecks` is enabled or when calling code assumes the value is either a valid enum or `null`.

In addition, resolving the workspace controller via `getAccessController('ws')` hides a hard dependency and makes the method harder to unit-test.  Constructor injection keeps dependencies explicit and leverages Nest’s DI lifecycle.

```diff
@@
-    const docRoles = await workspaceController.getDocRoles(payload, [
-      payload.docId,
-    ]);
-    return docRoles[0];
+    const [role] = await workspaceController.getDocRoles(payload, [
+      payload.docId,
+    ]);
+    // Normalise the “no-role” case to `null` to respect the method signature
+    return role ?? null;
```

Consider injecting `WorkspaceAccessController` via the constructor to avoid the service locator pattern:

```ts
@Injectable()
export class DocAccessController extends AccessController<'doc'> {
  constructor(private readonly workspaceController: WorkspaceAccessController) {
    super();
  }
  …
}
```

</blockquote></details>
<details>
<summary>packages/backend/server/src/models/doc.ts (1)</summary><blockquote>

`437-465`: _⚠️ Potential issue_

**Guard against empty input to avoid `IN ()` SQL and save a DB hop**

Calling `findMetas([])` generates `WHERE id IN ()`, which some SQL back-ends reject.  
Add an early return:

```diff
   async findMetas<Select extends Prisma.WorkspaceDocSelect>(
     ids: { workspaceId: string; docId: string }[],
     options?: { select?: Select }
   ) {
+    if (ids.length === 0) {
+      return [];
+    }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>packages/backend/server/src/__tests__/e2e/indexer/search.spec.ts (1)</summary><blockquote>

`226-247`: _🛠️ Refactor suggestion_

**Same flakiness concern for the second user’s snapshot**

Apply the deterministic sort here as well to avoid order-dependent failures.

</blockquote></details>
<details>
<summary>packages/backend/server/src/models/doc.ts (1)</summary><blockquote>

`394-412`: _🛠️ Refactor suggestion_

**Same escalation issue in `findDefaultRoles()`**

For documents that lack a meta row you again default to `Manager`, propagating the privilege-escalation noted above. Please adopt the safer fallback.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>packages/backend/server/src/__tests__/e2e/indexer/search.spec.ts (1)</summary><blockquote>

`111-165`: **Sub-test duplicates index-seeding boilerplate**

The block-writing logic (`indexerService.write(…)` plus the three nearly identical objects) is duplicated between the two tests.  Extracting a helper (e.g. `seedThreeDocs(workspace, user)`) keeps the test focused on behaviour instead of setup noise and reduces maintenance cost when document structure changes.

</blockquote></details>
<details>
<summary>packages/backend/server/src/core/permission/__tests__/builder.spec.ts (1)</summary><blockquote>

`57-118`: **Tests assert length but still rely on hit order for snapshots**

`docs1`, `docs2`, and `docs3` are snapshot-tested after a hard length assertion.  
If `WorkspaceAccessController.docs` changes its internal ordering (e.g. switches from `docIds` input order to DB order), the snapshot will fail while the semantic guarantee (“contains exactly these docs”) still holds.

Either:
1. Compare sets instead of arrays (`t.deepEqual(new Set(docs2.map(d => d.docId)), new Set(['doc1','doc2','doc3']))`), or  
2. Sort by `docId` before snapshotting as suggested for the e2e tests.

Doing so will make the test intention explicit and robust to harmless internal refactors.

</blockquote></details>
<details>
<summary>packages/backend/server/src/__tests__/mocks/doc-user.mock.ts (1)</summary><blockquote>

`10-16`: **Unchecked create bypasses constraints – flag accidental production use**

Using `Prisma.WorkspaceDocUserRoleUncheckedCreateInput` is fine for isolated tests, but it silently skips FK checks.  Add a short comment to discourage copy-pasting this mock into production code and to remind future maintainers why the unchecked variant is acceptable here.

```ts
// NOTE: Unchecked input is intentional – the mock bypasses FK checks for speed
```

</blockquote></details>
<details>
<summary>packages/backend/server/src/core/permission/workspace.ts (1)</summary><blockquote>

`146-154`: **`Math.max` relies on enum integer ordering — brittle**

Using `Math.max(defaultDocRole.workspace, defaultDocRole.external)` assumes the enum’s numeric values line up with permission strength. If the enum changes (sparse or reordered values) the comparison breaks.

Introduce an explicit ranking map or comparator instead of `Math.max`.

</blockquote></details>

</blockquote></details>

<details>
<summary>🛑 Comments failed to post (4)</summary><blockquote>

<details>
<summary>packages/backend/server/src/__tests__/e2e/indexer/search.spec.ts (1)</summary><blockquote>

183-205: _🛠️ Refactor suggestion_

**Snapshot may be flaky – result order is undefined**

The ES/Meili/PG-FTS backend (whichever your `IndexerService` targets) does **not** guarantee the order of hits when no `sort` clause is supplied.  Relying on raw order in snapshots can lead to non-deterministic CI failures.

Add a deterministic sort in the query options (e.g. `sort: [{ field: 'docId', order: 'asc' }]`) or sort in-memory before asserting / snapshotting:

```diff
-  t.snapshot(result.workspace.search.nodes);
+  t.snapshot(
+    [...result.workspace.search.nodes].sort((a, b) => a.docId.localeCompare(b.docId))
+  );
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
  // owner can read all docs
  const result = await app.gql({
    query: indexerSearchQuery,
    variables: {
      id: workspace.id,
      input: {
        table: SearchTable.block,
        query: {
          type: SearchQueryType.match,
          field: 'workspaceId',
          match: workspace.id,
        },
        options: {
          fields: ['docId', 'blockId', 'refDocId', 'ref'],
          pagination: {
            limit: 100,
          },
        },
      },
    },
  });

  t.snapshot(
    [...result.workspace.search.nodes].sort((a, b) => a.docId.localeCompare(b.docId))
  );
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/backend/server/src/__tests__/e2e/indexer/search.spec.ts around lines
183 to 205, the test relies on the order of search results which is not
guaranteed by the backend, causing flaky snapshots. To fix this, add a
deterministic sort clause in the query options, for example by including a sort
array with a field like 'docId' in ascending order, or alternatively sort the
result nodes in-memory before calling t.snapshot to ensure consistent ordering.
```

</details>

</blockquote></details>
<details>
<summary>packages/backend/server/src/models/doc.ts (1)</summary><blockquote>

381-392: _⚠️ Potential issue_

**Defaulting missing doc to `Manager` grants unintended write access**

When `doc === null`, `workspace` falls back to `DocRole.Manager`, handing *edit-level* permissions to anyone who reaches this code path.  
Return `null` (or a strictly read-only role) instead and let `fixupDocRole()` compute the effective value:

```diff
-  workspace: doc?.defaultRole ?? DocRole.Manager,
+  workspace: doc?.defaultRole ?? null,
```




<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
  async getDefaultRole(workspaceId: string, docId: string) {
    const doc = await this.getMeta(workspaceId, docId, {
      select: {
        public: true,
        defaultRole: true,
      },
    });
    return {
      external: doc?.public ? DocRole.External : null,
      workspace: doc?.defaultRole ?? null,
    };
  }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/backend/server/src/models/doc.ts around lines 381 to 392, the
current code defaults the workspace role to DocRole.Manager when the doc is
null, which grants unintended write access. Modify the return statement so that
if doc is null, the workspace role is set to null or a strictly read-only role
instead of DocRole.Manager. This change prevents granting edit-level permissions
by default and allows fixupDocRole() to compute the effective role properly.
```

</details>

</blockquote></details>
<details>
<summary>packages/backend/server/src/core/permission/workspace.ts (2)</summary><blockquote>

150-158: _⚠️ Potential issue_

**Public-doc logic can upgrade anonymous users to editor/manager**

The `Math.max` branch can elevate an external visitor from `External` to whatever the workspace default role is (e.g. `Manager`) for public documents, enabling unintended edits.

Confirm the desired behaviour and cap external users’ role if necessary.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/backend/server/src/core/permission/workspace.ts around lines 150 to
158, the current logic uses Math.max to assign docRole, which can
unintentionally elevate an external user's role to a higher workspace default
role for public documents. To fix this, revise the logic to cap the external
user's role at the intended maximum (likely the external role) and prevent
upgrading to workspace roles like Manager or Editor when the user is anonymous
or external. Adjust the conditional checks to enforce this role capping
explicitly.
```

</details>

---

97-107: _🛠️ Refactor suggestion_

**Skip default-role lookup when every doc already has a user role**

`noUserRoleDocIds` can be empty, yet `getDocDefaultRoles()` is still invoked, leading to an unnecessary—and potentially invalid—query.

```diff
-    const defaultDocRoles = await this.getDocDefaultRoles(
-      payload,
-      noUserRoleDocIds,
-      workspaceRole
-    );
+    const defaultDocRoles =
+      noUserRoleDocIds.length === 0
+        ? []
+        : await this.getDocDefaultRoles(
+            payload,
+            noUserRoleDocIds,
+            workspaceRole,
+          );
```




<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    const noUserRoleDocIds = docIds.filter(docId => {
      const userRole = userRolesMap.get(docId);
      return (userRole?.type ?? null) === null;
    });
    const defaultDocRoles =
      noUserRoleDocIds.length === 0
        ? []
        : await this.getDocDefaultRoles(
            payload,
            noUserRoleDocIds,
            workspaceRole,
          );
    const defaultDocRolesMap = new Map(
      defaultDocRoles.map((role, index) => [noUserRoleDocIds[index], role])
    );
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/backend/server/src/core/permission/workspace.ts around lines 97 to
107, the code calls getDocDefaultRoles even when noUserRoleDocIds is empty,
causing unnecessary and possibly invalid queries. Add a conditional check before
calling getDocDefaultRoles to skip the call entirely if noUserRoleDocIds is
empty, ensuring the function is only invoked when there are documents without
user roles.
```

</details>

</blockquote></details>

</blockquote></details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T02:45:51Z
user: login: forehalo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T03:33:09Z
user: login: forehalo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T03:33:39Z
user: login: forehalo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T03:34:00Z
user: login: forehalo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T03:34:22Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T04:45:54Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T04:53:48Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>packages/backend/server/src/core/permission/workspace.ts (1)</summary><blockquote>

`103-106`: **Consider simplifying the null check with nullish coalescing.**

The current implementation is correct but could be slightly simplified for readability.

```diff
-    const noUserRoleDocIds = docIds.filter(docId => {
-      const userRole = userRolesMap.get(docId);
-      return (userRole?.type ?? null) === null;
-    });
+    const noUserRoleDocIds = docIds.filter(docId => 
+      userRolesMap.get(docId)?.type ?? null === null
+    );
```

</blockquote></details>
<details>
<summary>packages/backend/server/src/core/permission/__tests__/docs.spec.ts (2)</summary><blockquote>

`24-33`: **Consider adding a test with an empty document array.**

While the implementation handles empty arrays, there's no explicit test for this case. Consider adding a test that passes an empty document array to verify it behaves as expected.

```diff
+  const emptyDocs = await builder
+    .user(owner.id)
+    .workspace(workspace.id)
+    .docs(
+      [],
+      'Doc.Read'
+    );
+
+  t.is(emptyDocs.length, 0);
```

---

`68-80`: **Consider testing external users with public documents.**

The current tests verify workspace members and non-members, but don't test external users accessing public documents. Consider adding tests for this scenario to verify that public documents are properly accessible to external users.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0edbb755ab4fb084f2dd0aeb4f64642303b6c9a8 and 6f19fcf1091fff3ac620ec1adc6009869d110fef.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `packages/backend/server/src/core/permission/__tests__/__snapshots__/docs.spec.ts.snap` is excluded by `!**/*.snap`

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `packages/backend/server/src/core/permission/__tests__/__snapshots__/docs.spec.ts.md` (1 hunks)
* `packages/backend/server/src/core/permission/__tests__/docs.spec.ts` (1 hunks)
* `packages/backend/server/src/core/permission/workspace.ts` (2 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* packages/backend/server/src/core/permission/__tests__/__snapshots__/docs.spec.ts.md

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>packages/backend/server/src/core/permission/workspace.ts (4)</summary>

`79-85`: **Looks good: Well-structured batch permission mapping.**

The `docRoles` method provides a clean interface for retrieving document roles with their mapped permissions in a batch operation, which is crucial for efficient permission filtering.

---

`87-139`: **Well-implemented batch role resolution with proper fallback handling.**

The implementation correctly:
1. Handles empty document arrays efficiently
2. Retrieves workspace roles and explicit document roles in batch
3. Applies fallback logic for documents without explicit roles
4. Fixes up roles based on workspace role hierarchy
5. Normalizes `None` roles to `null`

---

`141-175`: **Good implementation of default role resolution logic.**

The method properly handles role fallback rules based on workspace membership and document visibility. The empty array check at lines 148-150 is crucial for preventing unnecessary database operations.

---

`159-169`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify behavior with different role combinations.**

The conditional logic here determines document access based on whether a user is a workspace member or external. Let's verify this logic works as expected with edge cases.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check implementation of fixupDocRole to understand how role fixup works
rg -A 5 'export function fixupDocRole' --type ts
```

Length of output: 520

---

🏁 Script executed:

```shell
#!/bin/bash
# Show full implementation of fixupDocRole for verification
rg -C 20 'export function fixupDocRole' --type ts packages/backend/server/src/core/permission/types.ts
```

Length of output: 1383

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate where defaultDocRole is defined to verify its values
rg -n 'defaultDocRole' -C 5 packages/backend/server/src/core/permission/workspace.ts
```

Length of output: 2181

---



</details>

**Add comprehensive tests for `getDocDefaultRoles` fallback logic**

The current branch at  
`packages/backend/server/src/core/permission/workspace.ts:159–169`  
determines the “default” document role based on whether a user is in the workspace (and not External) or truly external. To ensure this handles every edge case, please add unit tests covering at least these scenarios:

- workspaceRole = null  
  • defaultDocRole.external = null → expect `null`  
  • defaultDocRole.external = Viewer → expect `Viewer`  
- workspaceRole = WorkspaceRole.External  
  • defaultDocRole.external = null → expect `null`  
  • defaultDocRole.external = Editor → expect `Editor`  
- workspaceRole = non-External (e.g. Member, Admin, Owner)  
  • defaultDocRole.external = null → expect `defaultDocRole.workspace`  
  • defaultDocRole.external = X and defaultDocRole.workspace = Y  
    – if X > Y → expect X  
    – if X ≤ Y → expect Y  

And verify integration with `fixupDocRole` (e.g. Owner always upgrades to DocRole.Owner, External trimmed to Editor, etc.).

</details>
<details>
<summary>packages/backend/server/src/core/permission/__tests__/docs.spec.ts (3)</summary>

`8-16`: **Appropriate test setup with cleanup.**

The test module is properly initialized with the necessary dependencies and includes the important cleanup step to avoid resource leaks.

---

`18-80`: **Comprehensive test coverage for read permissions across different roles.**

The test thoroughly validates document read access for:
1. Workspace owner (full access)
2. Workspace member with varied document roles (Reader, Manager, implicit)
3. Non-workspace user (no access)

Good use of snapshot testing to capture expected outputs.

---

`82-144`: **Well-structured test for publish permissions with different role combinations.**

The test correctly verifies that:
1. Workspace owners can publish all documents
2. Collaborators can publish only documents where they have Manager role (2 out of 3)
3. Non-workspace users have no publish access

The expected document count assertions (lines 96, 129, 143) verify the filtering logic works correctly.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T05:00:03Z
user: login: darkskygit
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-17T15:27:51Z

Review comments:
diff_hunk: @@ -74,6 +77,79 @@ export class WorkspaceAccessController extends AccessController<'ws'> {
     return role;
   }
 
+  async getDocRoles(payload: Resource<'ws'>, docIds: string[]) {
+    const workspaceRole = await this.getRole(payload);
+
+    const userRoles = await this.models.docUser.list(
+      payload.workspaceId,
+      docIds,
+      payload.userId
+    );
+    const userRolesMap = new Map(userRoles.map(role => [role.docId, role]));
+    const docRolesMap = new Map<string, DocRole>();
+
+    for (const docId of docIds) {
+      const userRole = userRolesMap.get(docId);
+
+      let docRole = userRole?.type ?? (null as DocRole | null);
+
+      // fallback logic
+      if (docRole === null) {
+        const defaultDocRole = await this.models.doc.getDefaultRole(
path: packages/backend/server/src/core/permission/workspace.ts
user: login: fengmk2
body: batch query
created_at: 2025-05-15T13:06:23Z
author_association: COLLABORATOR
diff_hunk: @@ -46,3 +53,131 @@ test('should build correct doc resource', t => {
     workspaceId: 'ws1',
   });
 });
+
+test('should filter docs by Doc.Read', async t => {
path: packages/backend/server/src/core/permission/__tests__/builder.spec.ts
user: login: forehalo
body: should be in docs.spec.ts

created_at: 2025-05-16T03:33:08Z
author_association: COLLABORATOR
diff_hunk: @@ -74,6 +76,93 @@ export class WorkspaceAccessController extends AccessController<'ws'> {
     return role;
   }
 
+  async docRoles(payload: Resource<'ws'>, docIds: string[]) {
+    const docRoles = await this.getDocRoles(payload, docIds);
+    return docRoles.map(role => ({
+      role,
+      permissions: mapDocRoleToPermissions(role),
+    }));
+  }
+
+  async getDocRoles(payload: Resource<'ws'>, docIds: string[]) {
+    const workspaceRole = await this.getRole(payload);
path: packages/backend/server/src/core/permission/workspace.ts
user: login: forehalo
body: lack of empty array test
created_at: 2025-05-16T03:33:38Z
author_association: COLLABORATOR
diff_hunk: @@ -74,6 +76,93 @@ export class WorkspaceAccessController extends AccessController<'ws'> {
     return role;
   }
 
+  async docRoles(payload: Resource<'ws'>, docIds: string[]) {
+    const docRoles = await this.getDocRoles(payload, docIds);
+    return docRoles.map(role => ({
+      role,
+      permissions: mapDocRoleToPermissions(role),
+    }));
+  }
+
+  async getDocRoles(payload: Resource<'ws'>, docIds: string[]) {
+    const workspaceRole = await this.getRole(payload);
+
+    const userRoles = await this.models.docUser.findMany(
+      payload.workspaceId,
+      docIds,
+      payload.userId
+    );
+    const userRolesMap = new Map(userRoles.map(role => [role.docId, role]));
+
+    const noUserRoleDocIds = docIds.filter(docId => {
+      const userRole = userRolesMap.get(docId);
+      return (userRole?.type ?? null) === null;
+    });
+    const defaultDocRoles = await this.getDocDefaultRoles(
path: packages/backend/server/src/core/permission/workspace.ts
user: login: forehalo
body: lack of empty array test




created_at: 2025-05-16T03:33:59Z
author_association: COLLABORATOR
diff_hunk: @@ -74,6 +76,93 @@ export class WorkspaceAccessController extends AccessController<'ws'> {
     return role;
   }
 
+  async docRoles(payload: Resource<'ws'>, docIds: string[]) {
+    const docRoles = await this.getDocRoles(payload, docIds);
+    return docRoles.map(role => ({
+      role,
+      permissions: mapDocRoleToPermissions(role),
+    }));
+  }
+
+  async getDocRoles(payload: Resource<'ws'>, docIds: string[]) {
+    const workspaceRole = await this.getRole(payload);
+
+    const userRoles = await this.models.docUser.findMany(
+      payload.workspaceId,
+      docIds,
+      payload.userId
+    );
+    const userRolesMap = new Map(userRoles.map(role => [role.docId, role]));
+
+    const noUserRoleDocIds = docIds.filter(docId => {
+      const userRole = userRolesMap.get(docId);
+      return (userRole?.type ?? null) === null;
+    });
+    const defaultDocRoles = await this.getDocDefaultRoles(
+      payload,
+      noUserRoleDocIds,
+      workspaceRole
+    );
+    const defaultDocRolesMap = new Map(
+      defaultDocRoles.map((role, index) => [noUserRoleDocIds[index], role])
+    );
+
+    const docRoles: (DocRole | null)[] = [];
+
+    for (const docId of docIds) {
+      const userRole = userRolesMap.get(docId);
+
+      let docRole: DocRole | null = userRole?.type ?? null;
+
+      // fallback logic
+      if (docRole === null) {
+        docRole = defaultDocRolesMap.get(docId) ?? null;
+      }
+
+      // we need to fixup doc role to make sure it's not miss set
+      // for example: workspace owner will have doc owner role
+      //              workspace external will not have role higher than editor
+      const role = fixupDocRole(workspaceRole, docRole);
+
+      // never return [None]
+      docRoles.push(role === DocRole.None ? null : role);
+    }
+
+    return docRoles;
+  }
+
+  private async getDocDefaultRoles(
+    payload: Resource<'ws'>,
+    docIds: string[],
+    workspaceRole: WorkspaceRole | null
+  ) {
+    const defaultDocRoles = await this.models.doc.findDefaultRoles(
path: packages/backend/server/src/core/permission/workspace.ts
user: login: forehalo
body: lack of empty array test




created_at: 2025-05-16T03:34:21Z
author_association: COLLABORATOR
diff_hunk: @@ -74,6 +76,93 @@ export class WorkspaceAccessController extends AccessController<'ws'> {
     return role;
   }
 
+  async docRoles(payload: Resource<'ws'>, docIds: string[]) {
+    const docRoles = await this.getDocRoles(payload, docIds);
+    return docRoles.map(role => ({
+      role,
+      permissions: mapDocRoleToPermissions(role),
+    }));
+  }
+
+  async getDocRoles(payload: Resource<'ws'>, docIds: string[]) {
+    const workspaceRole = await this.getRole(payload);
+
+    const userRoles = await this.models.docUser.findMany(
+      payload.workspaceId,
+      docIds,
+      payload.userId
+    );
+    const userRolesMap = new Map(userRoles.map(role => [role.docId, role]));
+
+    const noUserRoleDocIds = docIds.filter(docId => {
+      const userRole = userRolesMap.get(docId);
+      return (userRole?.type ?? null) === null;
+    });
+    const defaultDocRoles = await this.getDocDefaultRoles(
+      payload,
+      noUserRoleDocIds,
+      workspaceRole
+    );
+    const defaultDocRolesMap = new Map(
+      defaultDocRoles.map((role, index) => [noUserRoleDocIds[index], role])
+    );
+
+    const docRoles: (DocRole | null)[] = [];
+
+    for (const docId of docIds) {
+      const userRole = userRolesMap.get(docId);
+
+      let docRole: DocRole | null = userRole?.type ?? null;
+
+      // fallback logic
+      if (docRole === null) {
+        docRole = defaultDocRolesMap.get(docId) ?? null;
+      }
+
+      // we need to fixup doc role to make sure it's not miss set
+      // for example: workspace owner will have doc owner role
+      //              workspace external will not have role higher than editor
+      const role = fixupDocRole(workspaceRole, docRole);
+
+      // never return [None]
+      docRoles.push(role === DocRole.None ? null : role);
+    }
+
+    return docRoles;
+  }
+
+  private async getDocDefaultRoles(
+    payload: Resource<'ws'>,
+    docIds: string[],
+    workspaceRole: WorkspaceRole | null
+  ) {
+    const defaultDocRoles = await this.models.doc.findDefaultRoles(
path: packages/backend/server/src/core/permission/workspace.ts
user: login: graphite-app[bot]
body: @fengmk2 applied this fix suggested by [Graphite](https://graphite.dev?ref=github-ai-suggested-fix):

```suggestion
    if (docIds.length === 0) {
      return [];
    }
    
    const defaultDocRoles = await this.models.doc.findDefaultRoles(
```
created_at: 2025-05-16T04:45:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,6 +76,97 @@ export class WorkspaceAccessController extends AccessController<'ws'> {
     return role;
   }
 
+  async docRoles(payload: Resource<'ws'>, docIds: string[]) {
+    const docRoles = await this.getDocRoles(payload, docIds);
+    return docRoles.map(role => ({
+      role,
+      permissions: mapDocRoleToPermissions(role),
+    }));
+  }
+
+  async getDocRoles(payload: Resource<'ws'>, docIds: string[]) {
+    const workspaceRole = await this.getRole(payload);
+
+    const userRoles = await this.models.docUser.findMany(
+      payload.workspaceId,
+      docIds,
+      payload.userId
+    );
+    const userRolesMap = new Map(userRoles.map(role => [role.docId, role]));
+
+    const noUserRoleDocIds = docIds.filter(docId => {
+      const userRole = userRolesMap.get(docId);
+      return (userRole?.type ?? null) === null;
+    });
+    const defaultDocRoles = await this.getDocDefaultRoles(
+      payload,
+      noUserRoleDocIds,
+      workspaceRole
+    );
+    const defaultDocRolesMap = new Map(
+      defaultDocRoles.map((role, index) => [noUserRoleDocIds[index], role])
+    );
+
+    const docRoles: (DocRole | null)[] = [];
+
+    for (const docId of docIds) {
+      const userRole = userRolesMap.get(docId);
+
+      let docRole: DocRole | null = userRole?.type ?? null;
+
+      // fallback logic
+      if (docRole === null) {
+        docRole = defaultDocRolesMap.get(docId) ?? null;
+      }
+
+      // we need to fixup doc role to make sure it's not miss set
+      // for example: workspace owner will have doc owner role
+      //              workspace external will not have role higher than editor
+      const role = fixupDocRole(workspaceRole, docRole);
+
+      // never return [None]
+      docRoles.push(role === DocRole.None ? null : role);
+    }
+
+    return docRoles;
+  }
+
+  private async getDocDefaultRoles(
+    payload: Resource<'ws'>,
+    docIds: string[],
+    workspaceRole: WorkspaceRole | null
+  ) {
+    if (docIds.length === 0) {
+      return [];
+    }
+    
+    const defaultDocRoles = await this.models.doc.findDefaultRoles(
path: packages/backend/server/src/core/permission/workspace.ts
user: login: graphite-app[bot]
body: This line has trailing whitespace which violates the code style rules. Remove the trailing whitespace to fix the issue.

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=toeverything&ref=ai-review-comment) (based on CI logs)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-16T04:53:47Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-15T13:04:23Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

"""
## Walkthrough

The changes introduce batch document-level permission filtering and querying capabilities in the backend, including new methods for batch role resolution, updates to permission logic, and expanded test coverage. The search indexer and permission modules are refactored to leverage these batch operations, and new tests and mocks are added to validate document access control and permission filtering.

## Changes

| File(s)                                                                                          | Change Summary |
|-------------------------------------------------------------------------------------------------|----------------|
| `.../src/core/permission/builder.ts`, `.../src/core/permission/workspace.ts`, `.../src/core/permission/doc.ts`, `.../src/core/permission/types.ts` | Added batch document permission filtering methods, document role retrieval logic, and refined type aliases for permission mapping. Refactored `DocAccessController` to use new batch role logic. |
| `.../src/models/doc.ts`, `.../src/models/doc-user.ts`                                           | Added batch document default role and user role query methods; enhanced meta field selection for documents. |
| `.../src/plugins/indexer/resolver.ts`                                                          | Refactored to use batch document permission filtering in search result filtering. |
| `.../src/__tests__/e2e/indexer/search.spec.ts`, `.../src/__tests__/e2e/indexer/__snapshots__/search.spec.ts.md` | Added end-to-end tests and snapshots for search result filtering by document permissions. |
| `.../src/core/permission/__tests__/docs.spec.ts`, `.../src/core/permission/__tests__/__snapshots__/docs.spec.ts.md` | Added new test suite and snapshots for batch document permission filtering logic. |
| `.../src/__tests__/mocks/doc-user.mock.ts`, `.../src/__tests__/mocks/index.ts`                  | Introduced mock class and exports for document user role entities for testing. |
| `.../src/core/permission/__tests__/doc.spec.ts`                                                 | Updated test setup to match constructor signature changes in `DocAccessController`. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant AccessController
    participant WorkspaceAccessController
    participant DocModel
    participant DocUserModel

    User->>AccessController: Request docs with action (e.g., Doc.Read)
    AccessController->>WorkspaceAccessController: docs(items, action)
    WorkspaceAccessController->>WorkspaceAccessController: getDocRoles(payload, docIds)
    WorkspaceAccessController->>DocUserModel: findMany(workspaceId, docIds, userId)
    WorkspaceAccessController->>DocModel: findDefaultRoles(workspaceId, docIds)
    WorkspaceAccessController->>WorkspaceAccessController: fixupDocRole/merge roles
    WorkspaceAccessController-->>AccessController: Filtered items with granted permission
    AccessController-->>User: Return accessible docs
```

## Assessment against linked issues

| Objective                                                                    | Addressed | Explanation |
|-------------------------------------------------------------------------------|-----------|-------------|
| Batch querying of user document read permissions (CLOUD-208)                  | ✅        |             |

## Poem

> In fields of code, permissions bloom,  
> Batch checks now clear away the gloom.  
> Docs filtered fast, with roles in hand,  
> Tests and mocks at our command.  
> The search is sharp, the logic neat—  
> This bunny hops with nimble feet! 🐇✨
"""

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6abe9b758a1f6b9a963be8b6738f20d807e7d803 and 1e7774929cc27b210cf61e5345a864441795d699.

</details>

<details>
<summary>⛔ Files ignored due to path filters (2)</summary>

* `packages/backend/server/src/__tests__/e2e/indexer/__snapshots__/search.spec.ts.snap` is excluded by `!**/*.snap`
* `packages/backend/server/src/core/permission/__tests__/__snapshots__/docs.spec.ts.snap` is excluded by `!**/*.snap`

</details>

<details>
<summary>📒 Files selected for processing (14)</summary>

* `packages/backend/server/src/__tests__/e2e/indexer/__snapshots__/search.spec.ts.md` (1 hunks)
* `packages/backend/server/src/__tests__/e2e/indexer/search.spec.ts` (2 hunks)
* `packages/backend/server/src/__tests__/mocks/doc-user.mock.ts` (1 hunks)
* `packages/backend/server/src/__tests__/mocks/index.ts` (2 hunks)
* `packages/backend/server/src/core/permission/__tests__/__snapshots__/docs.spec.ts.md` (1 hunks)
* `packages/backend/server/src/core/permission/__tests__/doc.spec.ts` (1 hunks)
* `packages/backend/server/src/core/permission/__tests__/docs.spec.ts` (1 hunks)
* `packages/backend/server/src/core/permission/builder.ts` (2 hunks)
* `packages/backend/server/src/core/permission/doc.ts` (1 hunks)
* `packages/backend/server/src/core/permission/types.ts` (3 hunks)
* `packages/backend/server/src/core/permission/workspace.ts` (2 hunks)
* `packages/backend/server/src/models/doc-user.ts` (1 hunks)
* `packages/backend/server/src/models/doc.ts` (2 hunks)
* `packages/backend/server/src/plugins/indexer/resolver.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (14)</summary>

* packages/backend/server/src/core/permission/__tests__/doc.spec.ts
* packages/backend/server/src/models/doc-user.ts
* packages/backend/server/src/__tests__/mocks/index.ts
* packages/backend/server/src/core/permission/types.ts
* packages/backend/server/src/__tests__/e2e/indexer/__snapshots__/search.spec.ts.md
* packages/backend/server/src/core/permission/builder.ts
* packages/backend/server/src/plugins/indexer/resolver.ts
* packages/backend/server/src/core/permission/__tests__/__snapshots__/docs.spec.ts.md
* packages/backend/server/src/__tests__/e2e/indexer/search.spec.ts
* packages/backend/server/src/__tests__/mocks/doc-user.mock.ts
* packages/backend/server/src/core/permission/doc.ts
* packages/backend/server/src/core/permission/__tests__/docs.spec.ts
* packages/backend/server/src/core/permission/workspace.ts
* packages/backend/server/src/models/doc.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (772)</summary>

* GitHub Check: Frontend Copilot E2E Test (6, 8)
* GitHub Check: Cloud E2E Test 4/6
* GitHub Check: Server Test (6, 8)
* GitHub Check: Server Test (2, 8)
* GitHub Check: Unit Test (3)
* GitHub Check: Unit Test (2)
* GitHub Check: Unit Test (4)
* GitHub Check: Unit Test (1)
* GitHub Check: Unit Test (5)
* GitHub Check: Native Unit Test
* GitHub Check: test-build-mobile-app / build-ios-web
* GitHub Check: test-build-mobile-app / build-android-web
* GitHub Check: y-octo binding test on aarch64-pc-windows-msvc
* GitHub Check: E2E Test (10)
* GitHub Check: E2E Test (9)
* GitHub Check: E2E Test (3)
* GitHub Check: E2E BlockSuite Test (7)
* GitHub Check: E2E Test (6)
* GitHub Check: E2E Test (7)
* GitHub Check: E2E Test (5)
* GitHub Check: E2E Test (8)
* GitHub Check: Build AFFiNE native (aarch64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-pc-windows-msvc
* GitHub Check: E2E Test (4)
* GitHub Check: E2E Test (1)
* GitHub Check: E2E Test (2)
* GitHub Check: E2E Mobile Test (5)
* GitHub Check: Build AFFiNE native (x86_64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-apple-darwin
* GitHub Check: E2E Mobile Test (3)
* GitHub Check: E2E Mobile Test (4)
* GitHub Check: E2E Mobile Test (1)
* GitHub Check: E2E Mobile Test (2)
* GitHub Check: E2E BlockSuite Test (8)
* GitHub Check: E2E BlockSuite Test (9)
* GitHub Check: E2E BlockSuite Test (2)
* GitHub Check: E2E BlockSuite Test (10)
* GitHub Check: E2E BlockSuite Test (5)
* GitHub Check: E2E BlockSuite Test (6)
* GitHub Check: E2E BlockSuite Test (1)
* GitHub Check: E2E BlockSuite Test (3)
* GitHub Check: E2E BlockSuite Test (4)
* GitHub Check: fuzzing
* GitHub Check: E2E BlockSuite Cross Browser Test (1, firefox)
* GitHub Check: Run native tests
* GitHub Check: E2E BlockSuite Cross Browser Test (2, chromium)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, firefox)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, chromium)
* GitHub Check: Build @affine/electron renderer
* GitHub Check: loom thread test
* GitHub Check: Analyze (typescript, blocksuite)
* GitHub Check: Analyze (javascript, affine)
* GitHub Check: Analyze (typescript, affine)
* GitHub Check: Analyze (javascript, blocksuite)
* GitHub Check: Typecheck
* GitHub Check: Lint
* GitHub Check: Cloud Desktop E2E Test
* GitHub Check: Cloud E2E Test 6/6
* GitHub Check: Frontend Copilot E2E Test (6, 8)
* GitHub Check: Cloud E2E Test 4/6
* GitHub Check: Frontend Copilot E2E Test (3, 8)
* GitHub Check: Cloud E2E Test 3/6
* GitHub Check: Cloud E2E Test 1/6
* GitHub Check: Frontend Copilot E2E Test (2, 8)
* GitHub Check: Server Test (7, 8)
* GitHub Check: Server Test (6, 8)
* GitHub Check: Server Test (4, 8)
* GitHub Check: Server Test (3, 8)
* GitHub Check: Server Test (2, 8)
* GitHub Check: Server Test (5, 8)
* GitHub Check: Server Copilot Api Test
* GitHub Check: Unit Test (3)
* GitHub Check: Unit Test (2)
* GitHub Check: Unit Test (4)
* GitHub Check: Unit Test (1)
* GitHub Check: Unit Test (5)
* GitHub Check: Native Unit Test
* GitHub Check: test-build-mobile-app / build-ios-web
* GitHub Check: test-build-mobile-app / build-android-web
* GitHub Check: y-octo binding test on aarch64-pc-windows-msvc
* GitHub Check: E2E Test (10)
* GitHub Check: E2E Test (9)
* GitHub Check: E2E Test (3)
* GitHub Check: E2E BlockSuite Test (7)
* GitHub Check: E2E Test (6)
* GitHub Check: E2E Test (7)
* GitHub Check: E2E Test (5)
* GitHub Check: E2E Test (8)
* GitHub Check: Build AFFiNE native (aarch64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-pc-windows-msvc
* GitHub Check: E2E Test (4)
* GitHub Check: E2E Test (1)
* GitHub Check: E2E Test (2)
* GitHub Check: E2E Mobile Test (5)
* GitHub Check: Build AFFiNE native (x86_64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-apple-darwin
* GitHub Check: E2E Mobile Test (3)
* GitHub Check: E2E Mobile Test (4)
* GitHub Check: E2E Mobile Test (1)
* GitHub Check: E2E Mobile Test (2)
* GitHub Check: E2E BlockSuite Test (8)
* GitHub Check: E2E BlockSuite Test (9)
* GitHub Check: E2E BlockSuite Test (2)
* GitHub Check: E2E BlockSuite Test (10)
* GitHub Check: E2E BlockSuite Test (5)
* GitHub Check: E2E BlockSuite Test (6)
* GitHub Check: E2E BlockSuite Test (1)
* GitHub Check: E2E BlockSuite Test (3)
* GitHub Check: E2E BlockSuite Test (4)
* GitHub Check: fuzzing
* GitHub Check: E2E BlockSuite Cross Browser Test (1, firefox)
* GitHub Check: Run native tests
* GitHub Check: E2E BlockSuite Cross Browser Test (2, chromium)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, firefox)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, chromium)
* GitHub Check: Build @affine/electron renderer
* GitHub Check: loom thread test
* GitHub Check: Analyze (typescript, blocksuite)
* GitHub Check: Analyze (javascript, affine)
* GitHub Check: Analyze (typescript, affine)
* GitHub Check: Analyze (javascript, blocksuite)
* GitHub Check: Typecheck
* GitHub Check: Lint
* GitHub Check: Cloud Desktop E2E Test
* GitHub Check: Cloud E2E Test 6/6
* GitHub Check: Frontend Copilot E2E Test (1, 8)
* GitHub Check: Cloud E2E Test 5/6
* GitHub Check: Frontend Copilot E2E Test (5, 8)
* GitHub Check: Frontend Copilot E2E Test (6, 8)
* GitHub Check: Cloud E2E Test 4/6
* GitHub Check: Frontend Copilot E2E Test (3, 8)
* GitHub Check: Cloud E2E Test 3/6
* GitHub Check: Frontend Copilot E2E Test (8, 8)
* GitHub Check: Frontend Copilot E2E Test (4, 8)
* GitHub Check: Cloud E2E Test 1/6
* GitHub Check: Frontend Copilot E2E Test (2, 8)
* GitHub Check: Server Test (7, 8)
* GitHub Check: Server Test (6, 8)
* GitHub Check: Server Test (4, 8)
* GitHub Check: Server Test (3, 8)
* GitHub Check: Server E2E Test
* GitHub Check: Server Test (2, 8)
* GitHub Check: Server Test (1, 8)
* GitHub Check: Server Test (0, 8)
* GitHub Check: Server Test (5, 8)
* GitHub Check: Server Copilot Api Test
* GitHub Check: Check Git Status
* GitHub Check: Unit Test (3)
* GitHub Check: Unit Test (2)
* GitHub Check: Unit Test (4)
* GitHub Check: Unit Test (1)
* GitHub Check: Unit Test (5)
* GitHub Check: Native Unit Test
* GitHub Check: test-build-mobile-app / build-ios-web
* GitHub Check: test-build-mobile-app / build-android-web
* GitHub Check: y-octo binding test on aarch64-pc-windows-msvc
* GitHub Check: E2E Test (10)
* GitHub Check: E2E Test (9)
* GitHub Check: E2E Test (3)
* GitHub Check: E2E BlockSuite Test (7)
* GitHub Check: E2E Test (6)
* GitHub Check: E2E Test (7)
* GitHub Check: E2E Test (5)
* GitHub Check: E2E Test (8)
* GitHub Check: Build AFFiNE native (aarch64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-pc-windows-msvc
* GitHub Check: E2E Test (4)
* GitHub Check: E2E Test (1)
* GitHub Check: E2E Test (2)
* GitHub Check: E2E Mobile Test (5)
* GitHub Check: Build AFFiNE native (x86_64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-apple-darwin
* GitHub Check: E2E Mobile Test (3)
* GitHub Check: E2E Mobile Test (4)
* GitHub Check: E2E Mobile Test (1)
* GitHub Check: E2E Mobile Test (2)
* GitHub Check: E2E BlockSuite Test (8)
* GitHub Check: E2E BlockSuite Test (9)
* GitHub Check: E2E BlockSuite Test (2)
* GitHub Check: E2E BlockSuite Test (10)
* GitHub Check: E2E BlockSuite Test (5)
* GitHub Check: E2E BlockSuite Test (6)
* GitHub Check: E2E BlockSuite Test (1)
* GitHub Check: E2E BlockSuite Test (3)
* GitHub Check: E2E BlockSuite Test (4)
* GitHub Check: fuzzing
* GitHub Check: E2E BlockSuite Cross Browser Test (1, firefox)
* GitHub Check: Run native tests
* GitHub Check: E2E BlockSuite Cross Browser Test (2, chromium)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, firefox)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, chromium)
* GitHub Check: Build @affine/electron renderer
* GitHub Check: loom thread test
* GitHub Check: Analyze (typescript, blocksuite)
* GitHub Check: Analyze (javascript, affine)
* GitHub Check: Analyze (typescript, affine)
* GitHub Check: Analyze (javascript, blocksuite)
* GitHub Check: Typecheck
* GitHub Check: Lint
* GitHub Check: Frontend Copilot E2E Test (7, 8)
* GitHub Check: Cloud Desktop E2E Test
* GitHub Check: Cloud E2E Test 6/6
* GitHub Check: Frontend Copilot E2E Test (1, 8)
* GitHub Check: Cloud E2E Test 5/6
* GitHub Check: Frontend Copilot E2E Test (5, 8)
* GitHub Check: Frontend Copilot E2E Test (6, 8)
* GitHub Check: Cloud E2E Test 4/6
* GitHub Check: Frontend Copilot E2E Test (3, 8)
* GitHub Check: Cloud E2E Test 2/6
* GitHub Check: Cloud E2E Test 3/6
* GitHub Check: Frontend Copilot E2E Test (8, 8)
* GitHub Check: Frontend Copilot E2E Test (4, 8)
* GitHub Check: Cloud E2E Test 1/6
* GitHub Check: Frontend Copilot E2E Test (2, 8)
* GitHub Check: Server Test (7, 8)
* GitHub Check: Server Test (6, 8)
* GitHub Check: Server Test (4, 8)
* GitHub Check: Server Test with Elasticsearch
* GitHub Check: Server Test (3, 8)
* GitHub Check: Server E2E Test
* GitHub Check: Server Test (2, 8)
* GitHub Check: Server Test (1, 8)
* GitHub Check: Server Test (0, 8)
* GitHub Check: Server Test (5, 8)
* GitHub Check: Server Copilot Api Test
* GitHub Check: Check Git Status
* GitHub Check: Unit Test (3)
* GitHub Check: Unit Test (2)
* GitHub Check: Unit Test (4)
* GitHub Check: Unit Test (1)
* GitHub Check: Unit Test (5)
* GitHub Check: Native Unit Test
* GitHub Check: test-build-mobile-app / build-ios-web
* GitHub Check: test-build-mobile-app / build-android-web
* GitHub Check: y-octo binding test on aarch64-pc-windows-msvc
* GitHub Check: E2E Test (10)
* GitHub Check: E2E Test (9)
* GitHub Check: E2E Test (3)
* GitHub Check: E2E BlockSuite Test (7)
* GitHub Check: E2E Test (6)
* GitHub Check: E2E Test (7)
* GitHub Check: E2E Test (5)
* GitHub Check: E2E Test (8)
* GitHub Check: Build AFFiNE native (aarch64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-pc-windows-msvc
* GitHub Check: E2E Test (4)
* GitHub Check: E2E Test (1)
* GitHub Check: E2E Test (2)
* GitHub Check: E2E Mobile Test (5)
* GitHub Check: Build AFFiNE native (x86_64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-apple-darwin
* GitHub Check: E2E Mobile Test (3)
* GitHub Check: E2E Mobile Test (4)
* GitHub Check: E2E Mobile Test (1)
* GitHub Check: E2E Mobile Test (2)
* GitHub Check: E2E BlockSuite Test (8)
* GitHub Check: E2E BlockSuite Test (9)
* GitHub Check: E2E BlockSuite Test (2)
* GitHub Check: E2E BlockSuite Test (10)
* GitHub Check: E2E BlockSuite Test (5)
* GitHub Check: E2E BlockSuite Test (6)
* GitHub Check: E2E BlockSuite Test (1)
* GitHub Check: E2E BlockSuite Test (3)
* GitHub Check: E2E BlockSuite Test (4)
* GitHub Check: fuzzing
* GitHub Check: E2E BlockSuite Cross Browser Test (1, firefox)
* GitHub Check: Run native tests
* GitHub Check: E2E BlockSuite Cross Browser Test (2, chromium)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, firefox)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, chromium)
* GitHub Check: Build @affine/electron renderer
* GitHub Check: loom thread test
* GitHub Check: Analyze (typescript, blocksuite)
* GitHub Check: Analyze (javascript, affine)
* GitHub Check: Analyze (typescript, affine)
* GitHub Check: Analyze (javascript, blocksuite)
* GitHub Check: Typecheck
* GitHub Check: Lint
* GitHub Check: Frontend Copilot E2E Test (7, 8)
* GitHub Check: Cloud Desktop E2E Test
* GitHub Check: Cloud E2E Test 6/6
* GitHub Check: Frontend Copilot E2E Test (1, 8)
* GitHub Check: Cloud E2E Test 5/6
* GitHub Check: Frontend Copilot E2E Test (5, 8)
* GitHub Check: Frontend Copilot E2E Test (6, 8)
* GitHub Check: Cloud E2E Test 4/6
* GitHub Check: Frontend Copilot E2E Test (3, 8)
* GitHub Check: Cloud E2E Test 2/6
* GitHub Check: Cloud E2E Test 3/6
* GitHub Check: Frontend Copilot E2E Test (8, 8)
* GitHub Check: Frontend Copilot E2E Test (4, 8)
* GitHub Check: Cloud E2E Test 1/6
* GitHub Check: Frontend Copilot E2E Test (2, 8)
* GitHub Check: Server Test (7, 8)
* GitHub Check: Server Test (6, 8)
* GitHub Check: Server Test (4, 8)
* GitHub Check: Server Test with Elasticsearch
* GitHub Check: Server Test (3, 8)
* GitHub Check: Server E2E Test
* GitHub Check: Server Test (2, 8)
* GitHub Check: Server Test (1, 8)
* GitHub Check: Server Test (0, 8)
* GitHub Check: Server Test (5, 8)
* GitHub Check: Server Copilot Api Test
* GitHub Check: Check Git Status
* GitHub Check: Unit Test (3)
* GitHub Check: Unit Test (2)
* GitHub Check: Unit Test (4)
* GitHub Check: Unit Test (1)
* GitHub Check: Unit Test (5)
* GitHub Check: Native Unit Test
* GitHub Check: test-build-mobile-app / build-ios-web
* GitHub Check: test-build-mobile-app / build-android-web
* GitHub Check: y-octo binding test on aarch64-pc-windows-msvc
* GitHub Check: E2E Test (10)
* GitHub Check: E2E Test (9)
* GitHub Check: E2E Test (3)
* GitHub Check: E2E BlockSuite Test (7)
* GitHub Check: E2E Test (6)
* GitHub Check: E2E Test (7)
* GitHub Check: E2E Test (5)
* GitHub Check: E2E Test (8)
* GitHub Check: Build AFFiNE native (aarch64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-pc-windows-msvc
* GitHub Check: E2E Test (4)
* GitHub Check: E2E Test (1)
* GitHub Check: E2E Test (2)
* GitHub Check: E2E Mobile Test (5)
* GitHub Check: Build AFFiNE native (x86_64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-apple-darwin
* GitHub Check: E2E Mobile Test (3)
* GitHub Check: E2E Mobile Test (4)
* GitHub Check: E2E Mobile Test (1)
* GitHub Check: E2E Mobile Test (2)
* GitHub Check: E2E BlockSuite Test (8)
* GitHub Check: E2E BlockSuite Test (9)
* GitHub Check: E2E BlockSuite Test (2)
* GitHub Check: E2E BlockSuite Test (10)
* GitHub Check: E2E BlockSuite Test (5)
* GitHub Check: E2E BlockSuite Test (6)
* GitHub Check: E2E BlockSuite Test (1)
* GitHub Check: E2E BlockSuite Test (3)
* GitHub Check: E2E BlockSuite Test (4)
* GitHub Check: fuzzing
* GitHub Check: E2E BlockSuite Cross Browser Test (1, firefox)
* GitHub Check: Run native tests
* GitHub Check: E2E BlockSuite Cross Browser Test (2, chromium)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, firefox)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, chromium)
* GitHub Check: Build @affine/electron renderer
* GitHub Check: loom thread test
* GitHub Check: Analyze (typescript, blocksuite)
* GitHub Check: Analyze (javascript, affine)
* GitHub Check: Analyze (typescript, affine)
* GitHub Check: Analyze (javascript, blocksuite)
* GitHub Check: Typecheck
* GitHub Check: Lint
* GitHub Check: Frontend Copilot E2E Test (7, 8)
* GitHub Check: Cloud Desktop E2E Test
* GitHub Check: Cloud E2E Test 6/6
* GitHub Check: Frontend Copilot E2E Test (1, 8)
* GitHub Check: Cloud E2E Test 5/6
* GitHub Check: Frontend Copilot E2E Test (5, 8)
* GitHub Check: Frontend Copilot E2E Test (6, 8)
* GitHub Check: Cloud E2E Test 4/6
* GitHub Check: Frontend Copilot E2E Test (3, 8)
* GitHub Check: Cloud E2E Test 2/6
* GitHub Check: Cloud E2E Test 3/6
* GitHub Check: Frontend Copilot E2E Test (8, 8)
* GitHub Check: Frontend Copilot E2E Test (4, 8)
* GitHub Check: Cloud E2E Test 1/6
* GitHub Check: Frontend Copilot E2E Test (2, 8)
* GitHub Check: Server Test (7, 8)
* GitHub Check: Server Test (6, 8)
* GitHub Check: Server Test (4, 8)
* GitHub Check: Server Test with Elasticsearch
* GitHub Check: Server Test (3, 8)
* GitHub Check: Server E2E Test
* GitHub Check: Server Test (2, 8)
* GitHub Check: Server Test (1, 8)
* GitHub Check: Server Test (0, 8)
* GitHub Check: Server Test (5, 8)
* GitHub Check: Server Copilot Api Test
* GitHub Check: Check Git Status
* GitHub Check: Unit Test (3)
* GitHub Check: Unit Test (2)
* GitHub Check: Unit Test (4)
* GitHub Check: Unit Test (1)
* GitHub Check: Unit Test (5)
* GitHub Check: Native Unit Test
* GitHub Check: test-build-mobile-app / build-ios-web
* GitHub Check: test-build-mobile-app / build-android-web
* GitHub Check: y-octo binding test on aarch64-pc-windows-msvc
* GitHub Check: E2E Test (10)
* GitHub Check: E2E Test (9)
* GitHub Check: E2E Test (3)
* GitHub Check: E2E BlockSuite Test (7)
* GitHub Check: E2E Test (6)
* GitHub Check: E2E Test (7)
* GitHub Check: E2E Test (5)
* GitHub Check: E2E Test (8)
* GitHub Check: Build AFFiNE native (aarch64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-pc-windows-msvc
* GitHub Check: E2E Test (4)
* GitHub Check: E2E Test (1)
* GitHub Check: E2E Test (2)
* GitHub Check: E2E Mobile Test (5)
* GitHub Check: Build AFFiNE native (x86_64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-apple-darwin
* GitHub Check: E2E Mobile Test (3)
* GitHub Check: E2E Mobile Test (4)
* GitHub Check: E2E Mobile Test (1)
* GitHub Check: E2E Mobile Test (2)
* GitHub Check: E2E BlockSuite Test (8)
* GitHub Check: E2E BlockSuite Test (9)
* GitHub Check: E2E BlockSuite Test (2)
* GitHub Check: E2E BlockSuite Test (10)
* GitHub Check: E2E BlockSuite Test (5)
* GitHub Check: E2E BlockSuite Test (6)
* GitHub Check: E2E BlockSuite Test (1)
* GitHub Check: E2E BlockSuite Test (3)
* GitHub Check: E2E BlockSuite Test (4)
* GitHub Check: fuzzing
* GitHub Check: E2E BlockSuite Cross Browser Test (1, firefox)
* GitHub Check: Run native tests
* GitHub Check: E2E BlockSuite Cross Browser Test (2, chromium)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, firefox)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, chromium)
* GitHub Check: Build @affine/electron renderer
* GitHub Check: loom thread test
* GitHub Check: Analyze (typescript, blocksuite)
* GitHub Check: Analyze (javascript, affine)
* GitHub Check: Analyze (typescript, affine)
* GitHub Check: Analyze (javascript, blocksuite)
* GitHub Check: Typecheck
* GitHub Check: Lint
* GitHub Check: Frontend Copilot E2E Test (7, 8)
* GitHub Check: Cloud Desktop E2E Test
* GitHub Check: Cloud E2E Test 6/6
* GitHub Check: Frontend Copilot E2E Test (1, 8)
* GitHub Check: Cloud E2E Test 5/6
* GitHub Check: Frontend Copilot E2E Test (5, 8)
* GitHub Check: Frontend Copilot E2E Test (6, 8)
* GitHub Check: Cloud E2E Test 4/6
* GitHub Check: Frontend Copilot E2E Test (3, 8)
* GitHub Check: Cloud E2E Test 2/6
* GitHub Check: Cloud E2E Test 3/6
* GitHub Check: Frontend Copilot E2E Test (8, 8)
* GitHub Check: Frontend Copilot E2E Test (4, 8)
* GitHub Check: Cloud E2E Test 1/6
* GitHub Check: Frontend Copilot E2E Test (2, 8)
* GitHub Check: Server Test (7, 8)
* GitHub Check: Server Test (6, 8)
* GitHub Check: Server Test (4, 8)
* GitHub Check: Server Test with Elasticsearch
* GitHub Check: Server Test (3, 8)
* GitHub Check: Server E2E Test
* GitHub Check: Server Test (2, 8)
* GitHub Check: Server Test (1, 8)
* GitHub Check: Server Test (0, 8)
* GitHub Check: Server Test (5, 8)
* GitHub Check: Server Copilot Api Test
* GitHub Check: Check Git Status
* GitHub Check: Unit Test (3)
* GitHub Check: Unit Test (2)
* GitHub Check: Unit Test (4)
* GitHub Check: Unit Test (1)
* GitHub Check: Unit Test (5)
* GitHub Check: Native Unit Test
* GitHub Check: test-build-mobile-app / build-ios-web
* GitHub Check: test-build-mobile-app / build-android-web
* GitHub Check: y-octo binding test on aarch64-pc-windows-msvc
* GitHub Check: E2E Test (10)
* GitHub Check: E2E Test (9)
* GitHub Check: E2E Test (3)
* GitHub Check: E2E BlockSuite Test (7)
* GitHub Check: E2E Test (6)
* GitHub Check: E2E Test (7)
* GitHub Check: E2E Test (5)
* GitHub Check: E2E Test (8)
* GitHub Check: Build AFFiNE native (aarch64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-pc-windows-msvc
* GitHub Check: E2E Test (4)
* GitHub Check: E2E Test (1)
* GitHub Check: E2E Test (2)
* GitHub Check: E2E Mobile Test (5)
* GitHub Check: Build AFFiNE native (x86_64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-apple-darwin
* GitHub Check: E2E Mobile Test (3)
* GitHub Check: E2E Mobile Test (4)
* GitHub Check: E2E Mobile Test (1)
* GitHub Check: E2E Mobile Test (2)
* GitHub Check: E2E BlockSuite Test (8)
* GitHub Check: E2E BlockSuite Test (9)
* GitHub Check: E2E BlockSuite Test (2)
* GitHub Check: E2E BlockSuite Test (10)
* GitHub Check: E2E BlockSuite Test (5)
* GitHub Check: E2E BlockSuite Test (6)
* GitHub Check: E2E BlockSuite Test (1)
* GitHub Check: E2E BlockSuite Test (3)
* GitHub Check: E2E BlockSuite Test (4)
* GitHub Check: fuzzing
* GitHub Check: E2E BlockSuite Cross Browser Test (1, firefox)
* GitHub Check: Run native tests
* GitHub Check: E2E BlockSuite Cross Browser Test (2, chromium)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, firefox)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, chromium)
* GitHub Check: Build @affine/electron renderer
* GitHub Check: loom thread test
* GitHub Check: Analyze (typescript, blocksuite)
* GitHub Check: Analyze (javascript, affine)
* GitHub Check: Analyze (typescript, affine)
* GitHub Check: Analyze (javascript, blocksuite)
* GitHub Check: Typecheck
* GitHub Check: Lint
* GitHub Check: Frontend Copilot E2E Test (7, 8)
* GitHub Check: Cloud Desktop E2E Test
* GitHub Check: Cloud E2E Test 6/6
* GitHub Check: Frontend Copilot E2E Test (1, 8)
* GitHub Check: Cloud E2E Test 5/6
* GitHub Check: Frontend Copilot E2E Test (5, 8)
* GitHub Check: Frontend Copilot E2E Test (6, 8)
* GitHub Check: Cloud E2E Test 4/6
* GitHub Check: Frontend Copilot E2E Test (3, 8)
* GitHub Check: Cloud E2E Test 2/6
* GitHub Check: Cloud E2E Test 3/6
* GitHub Check: Frontend Copilot E2E Test (8, 8)
* GitHub Check: Frontend Copilot E2E Test (4, 8)
* GitHub Check: Cloud E2E Test 1/6
* GitHub Check: Frontend Copilot E2E Test (2, 8)
* GitHub Check: Server Test (7, 8)
* GitHub Check: Server Test (6, 8)
* GitHub Check: Server Test (4, 8)
* GitHub Check: Server Test with Elasticsearch
* GitHub Check: Server Test (3, 8)
* GitHub Check: Server E2E Test
* GitHub Check: Server Test (2, 8)
* GitHub Check: Server Test (1, 8)
* GitHub Check: Server Test (0, 8)
* GitHub Check: Server Test (5, 8)
* GitHub Check: Server Copilot Api Test
* GitHub Check: Check Git Status
* GitHub Check: Unit Test (3)
* GitHub Check: Unit Test (2)
* GitHub Check: Unit Test (4)
* GitHub Check: Unit Test (1)
* GitHub Check: Unit Test (5)
* GitHub Check: Native Unit Test
* GitHub Check: test-build-mobile-app / build-ios-web
* GitHub Check: test-build-mobile-app / build-android-web
* GitHub Check: y-octo binding test on aarch64-pc-windows-msvc
* GitHub Check: E2E Test (10)
* GitHub Check: E2E Test (9)
* GitHub Check: E2E Test (3)
* GitHub Check: E2E BlockSuite Test (7)
* GitHub Check: E2E Test (6)
* GitHub Check: E2E Test (7)
* GitHub Check: E2E Test (5)
* GitHub Check: E2E Test (8)
* GitHub Check: Build AFFiNE native (aarch64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-pc-windows-msvc
* GitHub Check: E2E Test (4)
* GitHub Check: E2E Test (1)
* GitHub Check: E2E Test (2)
* GitHub Check: E2E Mobile Test (5)
* GitHub Check: Build AFFiNE native (x86_64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-apple-darwin
* GitHub Check: E2E Mobile Test (3)
* GitHub Check: E2E Mobile Test (4)
* GitHub Check: E2E Mobile Test (1)
* GitHub Check: E2E Mobile Test (2)
* GitHub Check: E2E BlockSuite Test (8)
* GitHub Check: E2E BlockSuite Test (9)
* GitHub Check: E2E BlockSuite Test (2)
* GitHub Check: E2E BlockSuite Test (10)
* GitHub Check: E2E BlockSuite Test (5)
* GitHub Check: E2E BlockSuite Test (6)
* GitHub Check: E2E BlockSuite Test (1)
* GitHub Check: E2E BlockSuite Test (3)
* GitHub Check: E2E BlockSuite Test (4)
* GitHub Check: fuzzing
* GitHub Check: E2E BlockSuite Cross Browser Test (1, firefox)
* GitHub Check: Run native tests
* GitHub Check: E2E BlockSuite Cross Browser Test (2, chromium)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, firefox)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, chromium)
* GitHub Check: Build @affine/electron renderer
* GitHub Check: loom thread test
* GitHub Check: Analyze (typescript, blocksuite)
* GitHub Check: Analyze (javascript, affine)
* GitHub Check: Analyze (typescript, affine)
* GitHub Check: Analyze (javascript, blocksuite)
* GitHub Check: Typecheck
* GitHub Check: Lint
* GitHub Check: Frontend Copilot E2E Test (7, 8)
* GitHub Check: Cloud Desktop E2E Test
* GitHub Check: Cloud E2E Test 6/6
* GitHub Check: Frontend Copilot E2E Test (1, 8)
* GitHub Check: Cloud E2E Test 5/6
* GitHub Check: Frontend Copilot E2E Test (5, 8)
* GitHub Check: Frontend Copilot E2E Test (6, 8)
* GitHub Check: Cloud E2E Test 4/6
* GitHub Check: Frontend Copilot E2E Test (3, 8)
* GitHub Check: Cloud E2E Test 2/6
* GitHub Check: Cloud E2E Test 3/6
* GitHub Check: Frontend Copilot E2E Test (8, 8)
* GitHub Check: Frontend Copilot E2E Test (4, 8)
* GitHub Check: Cloud E2E Test 1/6
* GitHub Check: Frontend Copilot E2E Test (2, 8)
* GitHub Check: Server Test (7, 8)
* GitHub Check: Server Test (6, 8)
* GitHub Check: Server Test (4, 8)
* GitHub Check: Server Test with Elasticsearch
* GitHub Check: Server Test (3, 8)
* GitHub Check: Server E2E Test
* GitHub Check: Server Test (2, 8)
* GitHub Check: Server Test (1, 8)
* GitHub Check: Server Test (0, 8)
* GitHub Check: Server Test (5, 8)
* GitHub Check: Server Copilot Api Test
* GitHub Check: Check Git Status
* GitHub Check: Unit Test (3)
* GitHub Check: Unit Test (2)
* GitHub Check: Unit Test (4)
* GitHub Check: Unit Test (1)
* GitHub Check: Unit Test (5)
* GitHub Check: Native Unit Test
* GitHub Check: test-build-mobile-app / build-ios-web
* GitHub Check: test-build-mobile-app / build-android-web
* GitHub Check: y-octo binding test on aarch64-pc-windows-msvc
* GitHub Check: E2E Test (10)
* GitHub Check: E2E Test (9)
* GitHub Check: E2E Test (3)
* GitHub Check: E2E BlockSuite Test (7)
* GitHub Check: E2E Test (6)
* GitHub Check: E2E Test (7)
* GitHub Check: E2E Test (5)
* GitHub Check: E2E Test (8)
* GitHub Check: Build AFFiNE native (aarch64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-pc-windows-msvc
* GitHub Check: E2E Test (4)
* GitHub Check: E2E Test (1)
* GitHub Check: E2E Test (2)
* GitHub Check: E2E Mobile Test (5)
* GitHub Check: Build AFFiNE native (x86_64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-apple-darwin
* GitHub Check: E2E Mobile Test (3)
* GitHub Check: E2E Mobile Test (4)
* GitHub Check: E2E Mobile Test (1)
* GitHub Check: E2E Mobile Test (2)
* GitHub Check: E2E BlockSuite Test (8)
* GitHub Check: E2E BlockSuite Test (9)
* GitHub Check: E2E BlockSuite Test (2)
* GitHub Check: E2E BlockSuite Test (10)
* GitHub Check: E2E BlockSuite Test (5)
* GitHub Check: E2E BlockSuite Test (6)
* GitHub Check: E2E BlockSuite Test (1)
* GitHub Check: E2E BlockSuite Test (3)
* GitHub Check: E2E BlockSuite Test (4)
* GitHub Check: fuzzing
* GitHub Check: E2E BlockSuite Cross Browser Test (1, firefox)
* GitHub Check: Run native tests
* GitHub Check: E2E BlockSuite Cross Browser Test (2, chromium)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, firefox)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, chromium)
* GitHub Check: Build @affine/electron renderer
* GitHub Check: loom thread test
* GitHub Check: Analyze (typescript, blocksuite)
* GitHub Check: Analyze (javascript, affine)
* GitHub Check: Analyze (typescript, affine)
* GitHub Check: Analyze (javascript, blocksuite)
* GitHub Check: Typecheck
* GitHub Check: Lint
* GitHub Check: Frontend Copilot E2E Test (7, 8)
* GitHub Check: Cloud Desktop E2E Test
* GitHub Check: Cloud E2E Test 6/6
* GitHub Check: Frontend Copilot E2E Test (1, 8)
* GitHub Check: Cloud E2E Test 5/6
* GitHub Check: Frontend Copilot E2E Test (5, 8)
* GitHub Check: Frontend Copilot E2E Test (6, 8)
* GitHub Check: Cloud E2E Test 4/6
* GitHub Check: Frontend Copilot E2E Test (3, 8)
* GitHub Check: Cloud E2E Test 2/6
* GitHub Check: Cloud E2E Test 3/6
* GitHub Check: Frontend Copilot E2E Test (8, 8)
* GitHub Check: Frontend Copilot E2E Test (4, 8)
* GitHub Check: Cloud E2E Test 1/6
* GitHub Check: Frontend Copilot E2E Test (2, 8)
* GitHub Check: Server Test (7, 8)
* GitHub Check: Server Test (6, 8)
* GitHub Check: Server Test (4, 8)
* GitHub Check: Server Test with Elasticsearch
* GitHub Check: Server Test (3, 8)
* GitHub Check: Server E2E Test
* GitHub Check: Server Test (2, 8)
* GitHub Check: Server Test (1, 8)
* GitHub Check: Server Test (0, 8)
* GitHub Check: Server Test (5, 8)
* GitHub Check: Server Copilot Api Test
* GitHub Check: Check Git Status
* GitHub Check: Unit Test (3)
* GitHub Check: Unit Test (2)
* GitHub Check: Unit Test (4)
* GitHub Check: Unit Test (1)
* GitHub Check: Unit Test (5)
* GitHub Check: Native Unit Test
* GitHub Check: test-build-mobile-app / build-ios-web
* GitHub Check: test-build-mobile-app / build-android-web
* GitHub Check: y-octo binding test on aarch64-pc-windows-msvc
* GitHub Check: E2E Test (10)
* GitHub Check: E2E Test (9)
* GitHub Check: E2E Test (3)
* GitHub Check: E2E BlockSuite Test (7)
* GitHub Check: E2E Test (6)
* GitHub Check: E2E Test (7)
* GitHub Check: E2E Test (5)
* GitHub Check: E2E Test (8)
* GitHub Check: Build AFFiNE native (aarch64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-pc-windows-msvc
* GitHub Check: E2E Test (4)
* GitHub Check: E2E Test (1)
* GitHub Check: E2E Test (2)
* GitHub Check: E2E Mobile Test (5)
* GitHub Check: Build AFFiNE native (x86_64-pc-windows-msvc)
* GitHub Check: y-octo binding test on x86_64-apple-darwin
* GitHub Check: E2E Mobile Test (3)
* GitHub Check: E2E Mobile Test (4)
* GitHub Check: E2E Mobile Test (1)
* GitHub Check: E2E Mobile Test (2)
* GitHub Check: E2E BlockSuite Test (8)
* GitHub Check: E2E BlockSuite Test (9)
* GitHub Check: E2E BlockSuite Test (2)
* GitHub Check: E2E BlockSuite Test (10)
* GitHub Check: E2E BlockSuite Test (5)
* GitHub Check: E2E BlockSuite Test (6)
* GitHub Check: E2E BlockSuite Test (1)
* GitHub Check: E2E BlockSuite Test (3)
* GitHub Check: E2E BlockSuite Test (4)
* GitHub Check: fuzzing
* GitHub Check: E2E BlockSuite Cross Browser Test (1, firefox)
* GitHub Check: Run native tests
* GitHub Check: E2E BlockSuite Cross Browser Test (2, chromium)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, firefox)
* GitHub Check: E2E BlockSuite Cross Browser Test (2, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, webkit)
* GitHub Check: E2E BlockSuite Cross Browser Test (1, chromium)
* GitHub Check: Build @affine/electron renderer
* GitHub Check: loom thread test
* GitHub Check: Analyze (typescript, blocksuite)
* GitHub Check: Analyze (javascript, affine)
* GitHub Check: Analyze (typescript, affine)
* GitHub Check: Analyze (javascript, blocksuite)
* GitHub Check: Typecheck
* GitHub Check: Lint

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=toeverything/AFFiNE&utm_content=12311):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: fengmk2
created_at: 2025-05-15T13:04:43Z
body: * **#12311** <a href="https://app.graphite.dev/github/pr/toeverything/AFFiNE/12311?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/toeverything/AFFiNE/12311?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* `canary`
<hr />

## How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:
- <kbd>merge</kbd> - adds this PR to the back of the merge queue
- <kbd>hotfix</kbd> - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/toeverything?ref=merge-queue-instructions-in-stack-comment&prId=12311).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>

<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: COLLABORATOR
user: login: codecov[bot]
created_at: 2025-05-15T13:09:51Z
body: ## [Codecov](https://app.codecov.io/gh/toeverything/AFFiNE/pull/12311?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) Report
Attention: Patch coverage is `98.19820%` with `4 lines` in your changes missing coverage. Please review.
> Project coverage is 55.18%. Comparing base [(`85bb728`)](https://app.codecov.io/gh/toeverything/AFFiNE/commit/85bb728ca8ccbcac4d4f27921dcef57d82da457d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) to head [(`1e77749`)](https://app.codecov.io/gh/toeverything/AFFiNE/commit/1e7774929cc27b210cf61e5345a864441795d699?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything).
> Report is 1 commits behind head on canary.

| [Files with missing lines](https://app.codecov.io/gh/toeverything/AFFiNE/pull/12311?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) | Patch % | Lines |
|---|---|---|
| [...es/backend/server/src/core/permission/workspace.ts](https://app.codecov.io/gh/toeverything/AFFiNE/pull/12311?src=pr&el=tree&filepath=packages%2Fbackend%2Fserver%2Fsrc%2Fcore%2Fpermission%2Fworkspace.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything#diff-cGFja2FnZXMvYmFja2VuZC9zZXJ2ZXIvc3JjL2NvcmUvcGVybWlzc2lvbi93b3Jrc3BhY2UudHM=) | 96.03% | [4 Missing :warning: ](https://app.codecov.io/gh/toeverything/AFFiNE/pull/12311?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           canary   #12311      +/-   ##
==========================================
+ Coverage   55.09%   55.18%   +0.08%     
==========================================
  Files        2619     2620       +1     
  Lines      121809   121945     +136     
  Branches    19430    19454      +24     
==========================================
+ Hits        67108    67291     +183     
+ Misses      53176    52679     -497     
- Partials     1525     1975     +450     
```

| [Flag](https://app.codecov.io/gh/toeverything/AFFiNE/pull/12311/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) | Coverage Δ | |
|---|---|---|
| [server-test](https://app.codecov.io/gh/toeverything/AFFiNE/pull/12311/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) | `78.49% <97.29%> (+0.07%)` | :arrow_up: |
| [unittest](https://app.codecov.io/gh/toeverything/AFFiNE/pull/12311/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) | `31.31% <ø> (+0.06%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/toeverything/AFFiNE/pull/12311?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: graphite-app[bot]
created_at: 2025-05-19T03:24:03Z
body: ### Merge activity

* **May 18, 11:24 PM EDT**: `fengmk2` added this pull request to the [Graphite merge queue](https://app.graphite.dev/merges?org=toeverything&repo=AFFiNE).
* **May 19, 3:42 AM UTC**: The [Graphite merge queue](https://app.graphite.dev/merges?org=toeverything&repo=AFFiNE) couldn't merge this PR because **it was not satisfying all requirements** (Failed CI: '3, 2, 1 Launch', 'Unit Test (4)').
* **May 18, 11:51 PM EDT**: `fengmk2` added this pull request to the [Graphite merge queue](https://app.graphite.dev/merges?org=toeverything&repo=AFFiNE).
* **May 18, 11:52 PM EDT**: Merged by the [Graphite merge queue](https://app.graphite.dev/merges?org=toeverything&repo=AFFiNE).

author_association: CONTRIBUTOR