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 bad36d4a8b..e975fdabdd 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 a1ceaa40ca..ec5c80acce 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 3a53a8c557..ab136518a7 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 0000000000..da6fa3623f --- /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 { + 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 b6abf141c8..bc5b4d4241 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 0000000000..70353c89fc --- /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 0000000000..739edc5d49 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 88440e7eed..b7c387f568 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); - 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 0000000000..5ba1431ffd --- /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 ab391a07d2..b7ff4a9cd2 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( + items: T[], + action: DocAction + ): Promise { + 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 9f930baf9e..47d0dfe8d9 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 8934ddb374..f9a452d1d9 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 = export type WorkspaceAction = ResourceActionName<'Workspace'>; export type DocAction = ResourceActionName<'Doc'>; export type Action = WorkspaceAction | DocAction; +export type WorkspaceActionPermissions = Record; +export type DocActionPermissions = Record; const cache = new WeakMap(); 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 - ); + 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 - ); + 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 59b384df84..b492f39a09 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 8695160a25..d8a71e3bda 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 d013261bcd..148a9e0d82 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