Alessandro/web 1659 workspace limits (#2733)

* chore(workspaces): billing version limit graphql schema

* chore(workspaces): billing member role required

* chore(core): test helper for random string

* chore(core): test helpers

* chore(workspaces): workspaces billing version resolver

* chore(workspaces): rename version to versionsCount
This commit is contained in:
Alessandro Magionami
2024-08-26 17:53:34 +02:00
committed by GitHub
parent 131918b428
commit 0ac36af93e
12 changed files with 256 additions and 3 deletions
@@ -146,3 +146,9 @@ export type EmitWorkspaceEvent = <TEvent extends WorkspaceEvents>(args: {
eventName: TEvent
payload: EventBusPayloads[TEvent]
}) => Promise<unknown[]>
export type CountProjectsVersionsByWorkspaceId = ({
workspaceId
}: {
workspaceId: string
}) => Promise<number>
@@ -61,7 +61,8 @@ import {
deleteWorkspaceDomainFactory,
getWorkspaceDomainsFactory,
getUserDiscoverableWorkspacesFactory,
getWorkspaceWithDomainsFactory
getWorkspaceWithDomainsFactory,
countProjectsVersionsByWorkspaceIdFactory
} from '@/modules/workspaces/repositories/workspaces'
import {
buildWorkspaceInviteEmailContentsFactory,
@@ -102,6 +103,8 @@ import {
import { joinWorkspaceFactory } from '@/modules/workspaces/services/join'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { requestNewEmailVerification } from '@/modules/emails/services/verification/request'
import { Workspace } from '@/modules/workspacesCore/domain/types'
import { WORKSPACE_MAX_PROJECTS_VERSIONS } from '@/modules/gatekeeper/domain/constants'
const buildCreateAndSendServerOrProjectInvite = () =>
createAndSendInviteFactory({
@@ -611,6 +614,17 @@ export = FF_WORKSPACES_MODULE_ENABLED
},
domains: async (parent) => {
return await getWorkspaceDomainsFactory({ db })({ workspaceIds: [parent.id] })
},
billing: (parent) => ({ parent })
},
WorkspaceBilling: {
versionsCount: async (parent) => {
return {
current: await countProjectsVersionsByWorkspaceIdFactory({ db })({
workspaceId: (parent as { parent: Workspace }).parent.id
}),
max: WORKSPACE_MAX_PROJECTS_VERSIONS
}
}
},
WorkspaceCollaborator: {
@@ -5,6 +5,7 @@ import {
WorkspaceWithOptionalRole
} from '@/modules/workspacesCore/domain/types'
import {
CountProjectsVersionsByWorkspaceId,
DeleteWorkspace,
DeleteWorkspaceDomain,
DeleteWorkspaceRole,
@@ -30,7 +31,14 @@ import {
WorkspaceDomains,
Workspaces
} from '@/modules/workspaces/helpers/db'
import { knex, ServerAcl, ServerInvites, Users } from '@/modules/core/dbSchema'
import {
knex,
ServerAcl,
ServerInvites,
StreamCommits,
Streams,
Users
} from '@/modules/core/dbSchema'
import { UserWithRole } from '@/modules/core/repositories/users'
import { removePrivateFields } from '@/modules/core/helpers/userHelper'
import {
@@ -302,3 +310,15 @@ export const getWorkspaceWithDomainsFactory =
)
} as Workspace & { domains: WorkspaceDomain[] }
}
export const countProjectsVersionsByWorkspaceIdFactory =
({ db }: { db: Knex }): CountProjectsVersionsByWorkspaceId =>
async ({ workspaceId }) => {
const [res] = await tables
.streams(db)
.join(StreamCommits.name, StreamCommits.col.streamId, Streams.col.id)
.where({ workspaceId })
.count(StreamCommits.col.commitId)
return parseInt(res.count.toString())
}
@@ -30,6 +30,17 @@ export const basicPendingWorkspaceCollaboratorFragment = gql`
}
`
export const workspaceBillingFragment = gql`
fragment WorkspaceBilling on Workspace {
billing {
versionsCount {
current
max
}
}
}
`
export const createWorkspaceInviteQuery = gql`
mutation CreateWorkspaceInvite(
$workspaceId: String!
@@ -86,6 +97,18 @@ export const getWorkspaceWithTeamQuery = gql`
${basicPendingWorkspaceCollaboratorFragment}
`
export const getWorkspaceWithBillingQuery = gql`
query GetWorkspaceWithBilling($workspaceId: String!) {
workspace(id: $workspaceId) {
...BasicWorkspace
...WorkspaceBilling
}
}
${basicWorkspaceFragment}
${workspaceBillingFragment}
`
export const cancelInviteMutation = gql`
mutation CancelWorkspaceInvite($workspaceId: String!, $inviteId: String!) {
workspaceMutations {
@@ -20,7 +20,11 @@ import {
GetWorkspaceTeamDocument,
UpdateWorkspaceDocument,
UpdateWorkspaceRoleDocument,
ActiveUserLeaveWorkspaceDocument
ActiveUserLeaveWorkspaceDocument,
GetWorkspaceWithBillingDocument,
CreateProjectDocument,
CreateObjectDocument,
CreateProjectVersionDocument
} from '@/test/graphql/generated/graphql'
import { beforeEachContext } from '@/test/hooks'
import { AllScopes } from '@/modules/core/helpers/mainConstants'
@@ -33,7 +37,57 @@ import {
import { BasicTestCommit, createTestCommit } from '@/test/speckle-helpers/commitHelper'
import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper'
import knex from '@/db/knex'
import { createRandomPassword } from '@/modules/core/helpers/testHelpers'
import { getBranchesByStreamId } from '@/modules/core/services/branches'
const createProjectWithVersions =
({ apollo }: { apollo: TestApolloServer }) =>
async ({
workspaceId,
versionsCount
}: {
workspaceId?: string
versionsCount: number
}) => {
const resProject1 = await apollo.execute(CreateProjectDocument, {
input: {
name: createRandomPassword(),
workspaceId
}
})
expect(resProject1).to.not.haveGraphQLErrors()
const project1Id = resProject1.data!.projectMutations.create.id
const {
items: [model1]
} = await getBranchesByStreamId({
streamId: project1Id,
limit: 1,
cursor: null
})
expect(model1).to.exist
const resObj1 = await apollo.execute(CreateObjectDocument, {
input: {
streamId: project1Id,
objects: [{ some: 'obj' }]
}
})
expect(resObj1).to.not.haveGraphQLErrors()
await Promise.all(
new Array(versionsCount).fill(0).map(async () => {
const res = await apollo.execute(CreateProjectVersionDocument, {
input: {
projectId: project1Id,
modelId: model1.id,
objectId: resObj1.data!.objectCreate[0]
}
})
expect(res).to.not.haveGraphQLErrors()
})
)
}
describe('Workspaces GQL CRUD', () => {
let apollo: TestApolloServer
@@ -150,6 +204,29 @@ describe('Workspaces GQL CRUD', () => {
})
})
describe('query workspace.billing', () => {
it('should return workspace version limits', async () => {
await createProjectWithVersions({ apollo })({
workspaceId: workspace.id,
versionsCount: 3
})
await createProjectWithVersions({ apollo })({
workspaceId: workspace.id,
versionsCount: 2
})
const res = await apollo.execute(GetWorkspaceWithBillingDocument, {
workspaceId: workspace.id
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.billing.versionsCount).to.deep.equal({
current: 5,
max: 500
})
})
})
describe('query activeUser.workspaces', () => {
it('should return all workspaces for a user', async () => {
const res = await apollo.execute(GetActiveUserWorkspacesDocument, {})