feat(workspaces): repository functions for invitable collaborators

This commit is contained in:
Alessandro Magionami
2025-03-31 09:37:52 +02:00
parent 29d36d0d0c
commit bbef398cd3
2 changed files with 380 additions and 1 deletions
@@ -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<UserRecord>(Users.name),
streamAcl: (db: Knex) => db<StreamAclRecord>(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<Pick<User, 'id' | 'name'>[]> => {
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())
}
@@ -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 }
])
})
})
})
}
)