feat(workspaces): added GQL fitlering capabilities to activeUser workspaces

*  added filtering mechanism for getWorkspaces completed or not completed workspaces
* added filtering mechanism to filter workspaces of active user by string hitting on slug or name
This commit is contained in:
Daniel Gak Anagrov
2025-05-19 16:30:56 +02:00
committed by GitHub
parent 3d4c4395f4
commit fa5f2eb1f5
14 changed files with 264 additions and 71 deletions
@@ -65,6 +65,8 @@ export type GetWorkspaceBySlugOrId = (args: {
export type GetWorkspaces = (args: {
workspaceIds?: string[]
userId?: string
search?: string
completed?: boolean
}) => Promise<WorkspaceWithOptionalRole[]>
export type GetAllWorkspaces = (args: {
@@ -59,7 +59,6 @@ import {
getWorkspaceCollaboratorsFactory,
getWorkspaceFactory,
getWorkspaceRolesFactory,
getWorkspaceRolesForUserFactory,
upsertWorkspaceFactory,
upsertWorkspaceRoleFactory,
getWorkspaceCollaboratorsTotalCountFactory,
@@ -76,7 +75,9 @@ import {
queryWorkspacesFactory,
countWorkspacesFactory,
countWorkspaceRoleWithOptionalProjectRoleFactory,
getPaginatedWorkspaceProjectsFactory
getPaginatedWorkspaceProjectsFactory,
getWorkspaceRolesForUserFactory,
getWorkspacesFactory
} from '@/modules/workspaces/repositories/workspaces'
import {
buildWorkspaceInviteEmailContentsFactory,
@@ -1846,18 +1847,20 @@ export = FF_WORKSPACES_MODULE_ENABLED
return await listExpiredSsoSessions({ userId: context.userId })
},
workspaces: async (_parent, _args, context) => {
workspaces: async (_parent, args, context) => {
if (!context.userId) {
throw new WorkspacesNotAuthorizedError()
}
const getWorkspaces = getWorkspacesForUserFactory({
getWorkspace: getWorkspaceFactory({ db }),
getWorkspaces: getWorkspacesFactory({ db }),
getWorkspaceRolesForUser: getWorkspaceRolesForUserFactory({ db })
})
const workspaces = await getWorkspaces({
userId: context.userId
userId: context.userId,
search: args.filter?.search ?? undefined,
completed: args.filter?.completed ?? undefined
})
// TODO: Pagination
@@ -52,6 +52,7 @@ import {
import { WorkspaceInvalidRoleError } from '@/modules/workspaces/errors/workspace'
import {
WorkspaceAcl as DbWorkspaceAcl,
WorkspaceCreationState as DbWorkspaceCreationState,
WorkspaceDomains,
Workspaces,
WorkspaceSeats
@@ -149,9 +150,30 @@ const workspaceWithRoleBaseQuery = ({
export const getWorkspacesFactory =
({ db }: { db: Knex }): GetWorkspaces =>
async ({ workspaceIds, userId }) => {
async ({ workspaceIds, userId, search, completed }) => {
const q = workspaceWithRoleBaseQuery({ db, userId })
if (workspaceIds !== undefined) q.whereIn(Workspaces.col.id, workspaceIds)
if (search) {
q.andWhere((builder) => {
builder
.where('name', 'ILIKE', `%${search}%`)
.orWhere('slug', 'ILIKE', `%${search}%`)
})
}
if (completed !== undefined) {
q.leftJoin(
DbWorkspaceCreationState.name,
Workspaces.col.id,
DbWorkspaceCreationState.col.workspaceId
).andWhere((builder) => {
builder
.where({ [DbWorkspaceCreationState.col.completed]: completed })
.orWhere({ [DbWorkspaceCreationState.col.completed]: null })
})
}
const results = await q
return results
}
@@ -1,11 +1,10 @@
import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations'
import {
GetUserDiscoverableWorkspaces,
GetWorkspace,
GetWorkspaceRolesForUser
GetWorkspaceRolesForUser,
GetWorkspaces
} from '@/modules/workspaces/domain/operations'
import { Workspace } from '@/modules/workspacesCore/domain/types'
import { chunk, isNull } from 'lodash'
type GetDiscoverableWorkspaceForUserArgs = {
userId: string
@@ -38,32 +37,29 @@ export const getDiscoverableWorkspacesForUserFactory =
type GetWorkspacesForUserArgs = {
userId: string
completed?: boolean
search?: string
}
export const getWorkspacesForUserFactory =
({
getWorkspace,
getWorkspaces,
getWorkspaceRolesForUser
}: {
getWorkspace: GetWorkspace
getWorkspaces: GetWorkspaces
getWorkspaceRolesForUser: GetWorkspaceRolesForUser
}) =>
async ({ userId }: GetWorkspacesForUserArgs): Promise<Workspace[]> => {
async ({
userId,
completed,
search
}: GetWorkspacesForUserArgs): Promise<Workspace[]> => {
const workspaceRoles = await getWorkspaceRolesForUser({ userId })
const workspaces: Workspace[] = []
for (const workspaceRoleBatch of chunk(workspaceRoles, 20)) {
// TODO: Use `getWorkspaces`, which I saw Fabians already wrote in another PR
const workspacesBatch = await Promise.all(
workspaceRoleBatch.map(({ workspaceId }) => getWorkspace({ workspaceId }))
)
workspaces.push(
...workspacesBatch.filter(
(workspace): workspace is Workspace => !isNull(workspace)
)
)
}
const workspaceIds = workspaceRoles.map((workspace) => {
return workspace.workspaceId
})
const workspaces = await getWorkspaces({ workspaceIds, completed, search })
return workspaces
}
@@ -26,7 +26,8 @@ import {
getWorkspaceDomainsFactory,
storeWorkspaceDomainFactory,
getWorkspaceBySlugFactory,
getWorkspaceRoleForUserFactory
getWorkspaceRoleForUserFactory,
upsertWorkspaceCreationStateFactory
} from '@/modules/workspaces/repositories/workspaces'
import {
buildWorkspaceInviteEmailContentsFactory,
@@ -107,7 +108,7 @@ import {
getWorkspaceSeatTypeToProjectRoleMappingFactory,
validateWorkspaceMemberProjectRoleFactory
} from '@/modules/workspaces/services/projects'
import { isBoolean, isString } from 'lodash'
import { assign, isBoolean, isString } from 'lodash'
import { captureCreatedInvite } from '@/test/speckle-helpers/inviteHelper'
import {
finalizeInvitedServerRegistrationFactory,
@@ -127,6 +128,8 @@ import {
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import { createRandomString } from '@/modules/core/helpers/testHelpers'
import { WorkspaceCreationState } from '@/modules/workspaces/domain/types'
const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
@@ -159,9 +162,16 @@ export const createTestWorkspace = async (
addPlan?: Partial<Pick<WorkspacePlan, 'name' | 'status'>> | boolean | WorkspacePlans
addSubscription?: boolean
regionKey?: string
addCreationState?: Pick<WorkspaceCreationState, 'completed' | 'state'>
}
) => {
const { domain, addPlan = true, regionKey, addSubscription } = options || {}
const {
domain,
addPlan = true,
regionKey,
addSubscription,
addCreationState
} = options || {}
const useRegion = isMultiRegionTestMode() && regionKey
if (!FF_WORKSPACES_MODULE_ENABLED) {
@@ -295,6 +305,17 @@ export const createTestWorkspace = async (
})
}
if (addCreationState) {
const upsertWorkspaceState = upsertWorkspaceCreationStateFactory({ db })
await upsertWorkspaceState({
workspaceCreationState: {
workspaceId: newWorkspace.id,
state: addCreationState.state,
completed: addCreationState.completed
}
})
}
const updateWorkspace = updateWorkspaceFactory({
validateSlug: validateSlugFactory({
getWorkspaceBySlug: getWorkspaceBySlugFactory({ db })
@@ -325,6 +346,19 @@ export const createTestWorkspace = async (
}
}
export const buildBasicTestWorkspace = (
overrides?: Partial<BasicTestWorkspace>
): BasicTestWorkspace =>
assign(
{
id: createRandomString(),
name: createRandomString(),
slug: createRandomString(),
ownerId: ''
},
overrides
)
export const assignToWorkspace = async (
workspace: BasicTestWorkspace,
user: BasicTestUser,
@@ -12,17 +12,24 @@ import {
getWorkspaceWithDomainsFactory,
countWorkspaceRoleWithOptionalProjectRoleFactory,
getWorkspaceCollaboratorsFactory,
getWorkspaceBySlugFactory
getWorkspaceBySlugFactory,
getWorkspacesFactory
} from '@/modules/workspaces/repositories/workspaces'
import db from '@/db/knex'
import cryptoRandomString from 'crypto-random-string'
import { expect } from 'chai'
import { Workspace, WorkspaceAcl } from '@/modules/workspacesCore/domain/types'
import { expectToThrow } from '@/test/assertionHelper'
import { BasicTestUser, createTestUser, createTestUsers } from '@/test/authHelper'
import {
BasicTestUser,
buildBasicTestUser,
createTestUser,
createTestUsers
} from '@/test/authHelper'
import {
BasicTestWorkspace,
assignToWorkspace,
buildBasicTestWorkspace,
createTestWorkspace
} from '@/modules/workspaces/tests/helpers/creation'
import {
@@ -46,6 +53,7 @@ import { createAndStoreTestWorkspaceFactory } from '@/test/speckle-helpers/works
import { WorkspaceJoinRequests } from '@/modules/workspacesCore/helpers/db'
const getWorkspace = getWorkspaceFactory({ db })
const getWorkspaces = getWorkspacesFactory({ db })
const getWorkspaceBySlug = getWorkspaceBySlugFactory({ db })
const getWorkspaceCollaborators = getWorkspaceCollaboratorsFactory({ db })
const deleteWorkspace = deleteWorkspaceFactory({ db })
@@ -84,6 +92,23 @@ const createAndStoreTestUser = async (): Promise<BasicTestUser> => {
describe('Workspace repositories', () => {
describe('getWorkspaceFactory creates a function, that', () => {
const testUserA = buildBasicTestUser()
const workspaceA1 = buildBasicTestWorkspace({ name: 'My House Workspace' })
const workspaceA2 = buildBasicTestWorkspace({ name: 'My Garage Workspace' })
before(async () => {
const testUserB = buildBasicTestUser()
await createTestUsers([testUserA, testUserB])
const workspaceB = buildBasicTestWorkspace()
await createTestWorkspace(workspaceB, testUserB)
await createTestWorkspace(workspaceA1, testUserA, {
addCreationState: { completed: true, state: {} }
})
await createTestWorkspace(workspaceA2, testUserA)
})
it('returns null if the workspace is not found', async () => {
const workspace = await getWorkspace({
workspaceId: cryptoRandomString({ length: 10 })
@@ -91,6 +116,61 @@ describe('Workspace repositories', () => {
expect(workspace).to.be.null
})
// not testing get here, we're going to use that for testing upsert
describe('getWorkspaces filters', () => {
it('is able to select them by name', async () => {
const workspaces = await getWorkspaces({
userId: testUserA.id,
workspaceIds: [workspaceA1.id],
search: 'house'
})
expect(workspaces).to.have.lengthOf(1)
expect(workspaces[0].id).to.eq(workspaceA1.id)
})
it('is able to filter them out by name', async () => {
const workspaces = await getWorkspaces({
userId: testUserA.id,
workspaceIds: [workspaceA1.id],
search: 'park'
})
expect(workspaces).to.have.lengthOf(0)
})
it('is able to filer them by completed status', async () => {
const workspaces = await getWorkspaces({
userId: testUserA.id,
workspaceIds: [workspaceA1.id],
completed: true
})
expect(workspaces).to.have.lengthOf(1)
expect(workspaces[0].id).to.eq(workspaceA1.id)
})
it('is able to filer them out by completed status', async () => {
const workspaces = await getWorkspaces({
userId: testUserA.id,
workspaceIds: [workspaceA1.id],
completed: false
})
expect(workspaces).to.have.lengthOf(0)
})
it('does not filter when there is no workspace_completed entry as safety mechanism', async () => {
const workspaces = await getWorkspaces({
userId: testUserA.id,
workspaceIds: [workspaceA2.id],
completed: false
})
expect(workspaces).to.have.lengthOf(1)
expect(workspaces[0].id).to.eq(workspaceA2.id)
})
})
})
describe('getWorkspaceBySlugFactory creates a function, that', () => {
@@ -7,6 +7,7 @@ import {
} from '@/test/graphqlHelper'
import {
BasicTestUser,
buildBasicTestUser,
createAuthTokenForUser,
createTestUser,
createTestUsers,
@@ -35,11 +36,12 @@ import {
WorkspaceEmbedOptionsDocument,
ProjectEmbedOptionsDocument
} from '@/test/graphql/generated/graphql'
import { beforeEachContext } from '@/test/hooks'
import { beforeEachContext, truncateTables } from '@/test/hooks'
import { AllScopes } from '@/modules/core/helpers/mainConstants'
import {
assignToWorkspace,
BasicTestWorkspace,
buildBasicTestWorkspace,
createTestWorkspace,
createWorkspaceInviteDirectly
} from '@/modules/workspaces/tests/helpers/creation'
@@ -76,6 +78,7 @@ import { itEach } from '@/test/assertionHelper'
import { assignWorkspaceSeatFactory } from '@/modules/workspaces/services/workspaceSeat'
import { createWorkspaceSeatFactory } from '@/modules/gatekeeper/repositories/workspaceSeat'
import { WorkspaceSeatType } from '@/modules/gatekeeper/domain/billing'
import { Workspaces } from '@/modules/workspaces/helpers/db'
const grantStreamPermissions = grantStreamPermissionsFactory({ db })
const validateAndCreateUserEmail = validateAndCreateUserEmailFactory({
@@ -700,16 +703,26 @@ describe('Workspaces GQL CRUD', () => {
})
describe('query activeUser.workspaces', () => {
it('should return all workspaces for a user', async () => {
const testUser: BasicTestUser = {
id: '',
name: 'John Speckle',
email: 'foobar@example.org',
role: Roles.Server.Admin,
verified: true
}
const testUser = buildBasicTestUser({ role: Roles.Server.Admin })
before(async () => {
await truncateTables([Workspaces.name])
await createTestUser(testUser)
await createTestWorkspace(buildBasicTestWorkspace(), testUser)
await createTestWorkspace(
buildBasicTestWorkspace({
name: 'A loooooooooong name'
}),
testUser
)
await createTestWorkspace(buildBasicTestWorkspace(), testUser, {
addCreationState: { completed: false, state: {} }
})
})
it('should return all workspaces for a user', async () => {
const testApollo: TestApolloServer = await testApolloServer({
context: await createTestContext({
auth: true,
@@ -720,36 +733,53 @@ describe('Workspaces GQL CRUD', () => {
})
})
const workspace1: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Workspace A',
slug: cryptoRandomString({ length: 10 })
}
const workspace2: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Workspace A',
slug: cryptoRandomString({ length: 10 })
}
const workspace3: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Workspace A',
slug: cryptoRandomString({ length: 10 })
}
await createTestWorkspace(workspace1, testUser)
await createTestWorkspace(workspace2, testUser)
await createTestWorkspace(workspace3, testUser)
const res = await testApollo.execute(GetActiveUserWorkspacesDocument, {})
expect(res).to.not.haveGraphQLErrors()
// TODO: this test depends on the previous tests
expect(res.data?.activeUser?.workspaces?.items?.length).to.equal(3)
})
it('omits non complete workspaces on request', async () => {
const testApollo: TestApolloServer = await testApolloServer({
context: await createTestContext({
auth: true,
userId: testUser.id,
token: '',
role: testUser.role,
scopes: AllScopes
})
})
const res = await testApollo.execute(GetActiveUserWorkspacesDocument, {
filter: {
completed: true
}
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.activeUser?.workspaces?.items?.length).to.equal(2)
})
it('filters by name workspaces on request', async () => {
const testApollo: TestApolloServer = await testApolloServer({
context: await createTestContext({
auth: true,
userId: testUser.id,
token: '',
role: testUser.role,
scopes: AllScopes
})
})
const res = await testApollo.execute(GetActiveUserWorkspacesDocument, {
filter: {
search: 'loooooooooong'
}
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.activeUser?.workspaces?.items?.length).to.equal(1)
})
})
describe('query workspace.projects', () => {