From bbef398cd3d2a8a1e16e9c7f950fe434e204392a Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Mon, 31 Mar 2025 09:37:52 +0200 Subject: [PATCH] feat(workspaces): repository functions for invitable collaborators --- .../modules/workspaces/repositories/users.ts | 94 +++++- .../integration/repositories/users.spec.ts | 287 ++++++++++++++++++ 2 files changed, 380 insertions(+), 1 deletion(-) create mode 100644 packages/server/modules/workspaces/tests/integration/repositories/users.spec.ts diff --git a/packages/server/modules/workspaces/repositories/users.ts b/packages/server/modules/workspaces/repositories/users.ts index d2dbe869d..00a7c6d38 100644 --- a/packages/server/modules/workspaces/repositories/users.ts +++ b/packages/server/modules/workspaces/repositories/users.ts @@ -1,8 +1,16 @@ -import { Users } from '@/modules/core/dbSchema' +import { StreamAcl, Streams, UserEmails, Users } from '@/modules/core/dbSchema' +import { User } from '@/modules/core/domain/users/types' import { metaHelpers } from '@/modules/core/helpers/meta' +import { StreamAclRecord, UserRecord } from '@/modules/core/helpers/types' import { SetUserActiveWorkspace } from '@/modules/workspaces/domain/operations' +import { WorkspaceAcl } from '@/modules/workspacesCore/helpers/db' import { Knex } from 'knex' +const tables = { + users: (db: Knex) => db(Users.name), + streamAcl: (db: Knex) => db(StreamAcl.name) +} + export const setUserActiveWorkspaceFactory = (deps: { db: Knex }): SetUserActiveWorkspace => async ({ userId, workspaceSlug, isProjectsActive = false }) => { @@ -12,3 +20,87 @@ export const setUserActiveWorkspaceFactory = meta.set(userId, 'isProjectsActive', isProjectsActive) ]) } + +export const getInvitableCollaboratorsByProjectIdFactory = + ({ db }: { db: Knex }) => + async ({ + filter, + cursor, + limit + }: { + filter: { + workspaceId: string + projectId: string + search?: string + } + cursor?: string + limit: number + }): Promise[]> => { + const { workspaceId, projectId, search } = filter + const query = tables + .users(db) + .join(WorkspaceAcl.name, WorkspaceAcl.col.userId, Users.col.id) + .join(UserEmails.name, UserEmails.col.userId, Users.col.id) + .join(Streams.name, Streams.col.workspaceId, WorkspaceAcl.col.workspaceId) + .where(WorkspaceAcl.col.workspaceId, workspaceId) + .whereNotIn( + Users.col.id, + await tables + .streamAcl(db) + .select(StreamAcl.col.userId) + .where(StreamAcl.col.resourceId, projectId) + ) + if (search) { + query.andWhere((w) => + w + .whereLike(Users.col.name, `%${search}%`) + .orWhereLike(UserEmails.col.email, `%${search}%`) + ) + } + if (cursor) { + query.andWhere(Users.col.createdAt, '<', cursor) + } + return await query + .orderBy(Users.col.createdAt, 'desc') + .limit(limit) + .select([Users.col.id, Users.col.name]) + .groupBy(Users.col.id) + } + +export const countInvitableCollaboratorsByProjectIdFactory = + ({ db }: { db: Knex }) => + async ({ + filter + }: { + filter: { + workspaceId: string + projectId: string + search?: string + } + }) => { + const { workspaceId, projectId, search } = filter + const query = tables + .users(db) + .join(WorkspaceAcl.name, WorkspaceAcl.col.userId, Users.col.id) + .join(UserEmails.name, UserEmails.col.userId, Users.col.id) + .join(Streams.name, Streams.col.workspaceId, WorkspaceAcl.col.workspaceId) + .where(WorkspaceAcl.col.workspaceId, workspaceId) + .whereNotIn( + Users.col.id, + await tables + .streamAcl(db) + .select(StreamAcl.col.userId) + .where(StreamAcl.col.resourceId, projectId) + ) + .select([Users.col.id, Users.col.name]) + .groupBy(Users.col.id) + if (search) { + query.andWhere((w) => + w + .whereLike(Users.col.name, `%${search}%`) + .orWhereLike(UserEmails.col.email, `%${search}%`) + ) + } + const [res] = await query.count() + return parseInt(res.count.toString()) + } diff --git a/packages/server/modules/workspaces/tests/integration/repositories/users.spec.ts b/packages/server/modules/workspaces/tests/integration/repositories/users.spec.ts new file mode 100644 index 000000000..ffa484e56 --- /dev/null +++ b/packages/server/modules/workspaces/tests/integration/repositories/users.spec.ts @@ -0,0 +1,287 @@ +import { db } from '@/db/knex' +import { + createRandomEmail, + createRandomString +} from '@/modules/core/helpers/testHelpers' +import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' +import { getInvitableCollaboratorsByProjectIdFactory } from '@/modules/workspaces/repositories/users' +import { + assignToWorkspace, + createTestWorkspace +} from '@/modules/workspaces/tests/helpers/creation' +import { createTestUser } from '@/test/authHelper' +import { createTestStream } from '@/test/speckle-helpers/streamHelper' +import { Roles } from '@speckle/shared' +import { expect } from 'chai' + +const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() + +;(FF_WORKSPACES_MODULE_ENABLED ? describe : describe.skip)( + 'Workspace repositories', + () => { + describe('users repository', () => { + describe('getInvitableCollaboratorsByProjectIdFactory returns a function that ', () => { + const getInvitableCollaboratorsByProjectId = + getInvitableCollaboratorsByProjectIdFactory({ db }) + + it('should return all workspace collaborators not members of the project', async () => { + const admin = await createTestUser({ + name: createRandomString(), + email: createRandomEmail(), + role: Roles.Server.User, + verified: true + }) + const workspace = { + id: createRandomString(), + name: createRandomString(), + slug: createRandomString(), + ownerId: admin.id + } + await createTestWorkspace(workspace, admin) + + const member = await createTestUser({ + name: createRandomString(), + email: createRandomEmail(), + role: Roles.Server.User, + verified: true + }) + await assignToWorkspace(workspace, member, Roles.Workspace.Member) + + // Non workspace member + await createTestUser({ + name: createRandomString(), + email: createRandomEmail(), + role: Roles.Server.User, + verified: true + }) + + const projectMember = await createTestUser({ + name: createRandomString(), + email: createRandomEmail(), + role: Roles.Server.User, + verified: true + }) + + const project = { + id: createRandomString(), + workspaceId: workspace.id + } + await createTestStream(project, projectMember) + + // User in another project should still be invitable + const otherProject = { + id: createRandomString(), + workspaceId: workspace.id + } + await createTestStream(otherProject, admin) + + const invitable = await getInvitableCollaboratorsByProjectId({ + filter: { + workspaceId: workspace.id, + projectId: project.id + }, + limit: 10 + }) + expect(invitable).to.have.length(2) + expect(invitable).to.deep.equalInAnyOrder([ + { id: admin.id, name: admin.name }, + { id: member.id, name: member.name } + ]) + }) + it('should should filter by user name', async () => { + const admin = await createTestUser({ + name: createRandomString() + 'fixed' + createRandomString(), + email: createRandomEmail(), + role: Roles.Server.User, + verified: true + }) + const workspace = { + id: createRandomString(), + name: createRandomString(), + slug: createRandomString(), + ownerId: admin.id + } + await createTestWorkspace(workspace, admin) + + const member = await createTestUser({ + name: createRandomString(), + email: createRandomEmail(), + role: Roles.Server.User, + verified: true + }) + await assignToWorkspace(workspace, member, Roles.Workspace.Member) + + // Non workspace member + await createTestUser({ + name: createRandomString(), + email: createRandomEmail(), + role: Roles.Server.User, + verified: true + }) + + const projectMember = await createTestUser({ + name: createRandomString(), + email: createRandomEmail(), + role: Roles.Server.User, + verified: true + }) + + const project = { + id: createRandomString(), + workspaceId: workspace.id + } + await createTestStream(project, projectMember) + + // User in another project should still be invitable + const otherProject = { + id: createRandomString(), + workspaceId: workspace.id + } + await createTestStream(otherProject, admin) + + const invitable = await getInvitableCollaboratorsByProjectId({ + filter: { + workspaceId: workspace.id, + projectId: project.id, + search: 'fixed' + }, + limit: 10 + }) + expect(invitable).to.have.length(1) + expect(invitable).to.deep.equalInAnyOrder([ + { id: admin.id, name: admin.name } + ]) + }) + it('should should filter by user email', async () => { + const admin = await createTestUser({ + name: createRandomString(), + email: createRandomString() + 'fixed' + createRandomString(), + role: Roles.Server.User, + verified: true + }) + const workspace = { + id: createRandomString(), + name: createRandomString(), + slug: createRandomString(), + ownerId: admin.id + } + await createTestWorkspace(workspace, admin) + + const member = await createTestUser({ + name: createRandomString(), + email: createRandomEmail(), + role: Roles.Server.User, + verified: true + }) + await assignToWorkspace(workspace, member, Roles.Workspace.Member) + + // Non workspace member + await createTestUser({ + name: createRandomString(), + email: createRandomEmail(), + role: Roles.Server.User, + verified: true + }) + + const projectMember = await createTestUser({ + name: createRandomString(), + email: createRandomEmail(), + role: Roles.Server.User, + verified: true + }) + + const project = { + id: createRandomString(), + workspaceId: workspace.id + } + await createTestStream(project, projectMember) + + // User in another project should still be invitable + const otherProject = { + id: createRandomString(), + workspaceId: workspace.id + } + await createTestStream(otherProject, admin) + + const invitable = await getInvitableCollaboratorsByProjectId({ + filter: { + workspaceId: workspace.id, + projectId: project.id, + search: 'fixed' + }, + limit: 10 + }) + expect(invitable).to.have.length(1) + expect(invitable).to.deep.equalInAnyOrder([ + { id: admin.id, name: admin.name } + ]) + }) + it('should should filter by user name and email', async () => { + const admin = await createTestUser({ + name: createRandomString(), + email: createRandomString() + 'fixed' + createRandomString(), + role: Roles.Server.User, + verified: true + }) + const workspace = { + id: createRandomString(), + name: createRandomString(), + slug: createRandomString(), + ownerId: admin.id + } + await createTestWorkspace(workspace, admin) + + const member = await createTestUser({ + name: createRandomString() + 'fixed' + createRandomString(), + email: createRandomEmail(), + role: Roles.Server.User, + verified: true + }) + await assignToWorkspace(workspace, member, Roles.Workspace.Member) + + // Non workspace member + await createTestUser({ + name: createRandomString(), + email: createRandomEmail(), + role: Roles.Server.User, + verified: true + }) + + const projectMember = await createTestUser({ + name: createRandomString(), + email: createRandomEmail(), + role: Roles.Server.User, + verified: true + }) + + const project = { + id: createRandomString(), + workspaceId: workspace.id + } + await createTestStream(project, projectMember) + + // User in another project should still be invitable + const otherProject = { + id: createRandomString(), + workspaceId: workspace.id + } + await createTestStream(otherProject, admin) + + const invitable = await getInvitableCollaboratorsByProjectId({ + filter: { + workspaceId: workspace.id, + projectId: project.id, + search: 'fixed' + }, + limit: 10 + }) + expect(invitable).to.have.length(2) + expect(invitable).to.deep.equalInAnyOrder([ + { id: admin.id, name: admin.name }, + { id: member.id, name: member.name } + ]) + }) + }) + }) + } +)