Skip to content

Commit

Permalink
Alessandro/web 1659 workspace limits (#2733)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
alemagio authored Aug 26, 2024
1 parent 131918b commit 0ac36af
Show file tree
Hide file tree
Showing 12 changed files with 256 additions and 3 deletions.
19 changes: 19 additions & 0 deletions packages/server/assets/workspacesCore/typedefs/workspaces.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,21 @@ input PendingWorkspaceCollaboratorsFilter {
search: String
}

type WorkspaceVersionsCount {
"""
Total number of versions of all projects in the workspace
"""
current: Int!
"""
Maximum number of version of all projects in the workspace with no additional cost
"""
max: Int!
}

type WorkspaceBilling {
versionsCount: WorkspaceVersionsCount
}

type Workspace {
id: ID!
name: String!
Expand Down Expand Up @@ -217,6 +232,10 @@ type Workspace {
filter: WorkspaceProjectsFilter
): ProjectCollection!
"""
Billing data for Workspaces beta
"""
billing: WorkspaceBilling! @hasWorkspaceRole(role: MEMBER)
"""
Enable/Disable restriction to invite users to workspace as Guests only
"""
domainBasedMembershipProtectionEnabled: Boolean!
Expand Down
33 changes: 33 additions & 0 deletions packages/server/modules/core/graph/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3854,6 +3854,8 @@ export type WebhookUpdateInput = {

export type Workspace = {
__typename?: 'Workspace';
/** Billing data for Workspaces beta */
billing: WorkspaceBilling;
createdAt: Scalars['DateTime']['output'];
/** Selected fallback when `logo` not set */
defaultLogoIndex: Scalars['Int']['output'];
Expand Down Expand Up @@ -3894,6 +3896,11 @@ export type WorkspaceTeamArgs = {
filter?: InputMaybe<WorkspaceTeamFilter>;
};

export type WorkspaceBilling = {
__typename?: 'WorkspaceBilling';
versionsCount?: Maybe<WorkspaceVersionsCount>;
};

export type WorkspaceCollaborator = {
__typename?: 'WorkspaceCollaborator';
id: Scalars['ID']['output'];
Expand Down Expand Up @@ -4097,6 +4104,14 @@ export type WorkspaceUpdateInput = {
name?: InputMaybe<Scalars['String']['input']>;
};

export type WorkspaceVersionsCount = {
__typename?: 'WorkspaceVersionsCount';
/** Total number of versions of all projects in the workspace */
current: Scalars['Int']['output'];
/** Maximum number of version of all projects in the workspace with no additional cost */
max: Scalars['Int']['output'];
};



export type ResolverTypeWrapper<T> = Promise<T> | T;
Expand Down Expand Up @@ -4400,6 +4415,7 @@ export type ResolversTypes = {
WebhookEventCollection: ResolverTypeWrapper<WebhookEventCollection>;
WebhookUpdateInput: WebhookUpdateInput;
Workspace: ResolverTypeWrapper<WorkspaceGraphQLReturn>;
WorkspaceBilling: ResolverTypeWrapper<WorkspaceBilling>;
WorkspaceCollaborator: ResolverTypeWrapper<WorkspaceCollaboratorGraphQLReturn>;
WorkspaceCollection: ResolverTypeWrapper<Omit<WorkspaceCollection, 'items'> & { items: Array<ResolversTypes['Workspace']> }>;
WorkspaceCreateInput: WorkspaceCreateInput;
Expand All @@ -4417,6 +4433,7 @@ export type ResolversTypes = {
WorkspaceRoleUpdateInput: WorkspaceRoleUpdateInput;
WorkspaceTeamFilter: WorkspaceTeamFilter;
WorkspaceUpdateInput: WorkspaceUpdateInput;
WorkspaceVersionsCount: ResolverTypeWrapper<WorkspaceVersionsCount>;
};

/** Mapping between all available schema types and the resolvers parents */
Expand Down Expand Up @@ -4632,6 +4649,7 @@ export type ResolversParentTypes = {
WebhookEventCollection: WebhookEventCollection;
WebhookUpdateInput: WebhookUpdateInput;
Workspace: WorkspaceGraphQLReturn;
WorkspaceBilling: WorkspaceBilling;
WorkspaceCollaborator: WorkspaceCollaboratorGraphQLReturn;
WorkspaceCollection: Omit<WorkspaceCollection, 'items'> & { items: Array<ResolversParentTypes['Workspace']> };
WorkspaceCreateInput: WorkspaceCreateInput;
Expand All @@ -4648,6 +4666,7 @@ export type ResolversParentTypes = {
WorkspaceRoleUpdateInput: WorkspaceRoleUpdateInput;
WorkspaceTeamFilter: WorkspaceTeamFilter;
WorkspaceUpdateInput: WorkspaceUpdateInput;
WorkspaceVersionsCount: WorkspaceVersionsCount;
};

export type HasScopeDirectiveArgs = {
Expand Down Expand Up @@ -5985,6 +6004,7 @@ export type WebhookEventCollectionResolvers<ContextType = GraphQLContext, Parent
};

export type WorkspaceResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['Workspace'] = ResolversParentTypes['Workspace']> = {
billing?: Resolver<ResolversTypes['WorkspaceBilling'], ParentType, ContextType>;
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
defaultLogoIndex?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
Expand All @@ -6002,6 +6022,11 @@ export type WorkspaceResolvers<ContextType = GraphQLContext, ParentType extends
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

export type WorkspaceBillingResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceBilling'] = ResolversParentTypes['WorkspaceBilling']> = {
versionsCount?: Resolver<Maybe<ResolversTypes['WorkspaceVersionsCount']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

export type WorkspaceCollaboratorResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceCollaborator'] = ResolversParentTypes['WorkspaceCollaborator']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
role?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
Expand Down Expand Up @@ -6044,6 +6069,12 @@ export type WorkspaceMutationsResolvers<ContextType = GraphQLContext, ParentType
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

export type WorkspaceVersionsCountResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceVersionsCount'] = ResolversParentTypes['WorkspaceVersionsCount']> = {
current?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
max?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

export type Resolvers<ContextType = GraphQLContext> = {
ActiveUserMutations?: ActiveUserMutationsResolvers<ContextType>;
Activity?: ActivityResolvers<ContextType>;
Expand Down Expand Up @@ -6173,11 +6204,13 @@ export type Resolvers<ContextType = GraphQLContext> = {
WebhookEvent?: WebhookEventResolvers<ContextType>;
WebhookEventCollection?: WebhookEventCollectionResolvers<ContextType>;
Workspace?: WorkspaceResolvers<ContextType>;
WorkspaceBilling?: WorkspaceBillingResolvers<ContextType>;
WorkspaceCollaborator?: WorkspaceCollaboratorResolvers<ContextType>;
WorkspaceCollection?: WorkspaceCollectionResolvers<ContextType>;
WorkspaceDomain?: WorkspaceDomainResolvers<ContextType>;
WorkspaceInviteMutations?: WorkspaceInviteMutationsResolvers<ContextType>;
WorkspaceMutations?: WorkspaceMutationsResolvers<ContextType>;
WorkspaceVersionsCount?: WorkspaceVersionsCountResolvers<ContextType>;
};

export type DirectiveResolvers<ContextType = GraphQLContext> = {
Expand Down
4 changes: 4 additions & 0 deletions packages/server/modules/core/helpers/testHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export function createRandomPassword(length?: number) {
return crs({ length: length ?? 10 })
}

export function createRandomString(length?: number) {
return crs({ length: length ?? 10 })
}

export const randomizeCase = (str: string) =>
str
.split('')
Expand Down
7 changes: 7 additions & 0 deletions packages/server/modules/core/tests/helpers/graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { gql } from 'apollo-server-express'

export const createObjectMutation = gql`
mutation CreateObject($input: ObjectCreateInput!) {
objectCreate(objectInput: $input)
}
`
Original file line number Diff line number Diff line change
Expand Up @@ -3843,6 +3843,8 @@ export type WebhookUpdateInput = {

export type Workspace = {
__typename?: 'Workspace';
/** Billing data for Workspaces beta */
billing: WorkspaceBilling;
createdAt: Scalars['DateTime']['output'];
/** Selected fallback when `logo` not set */
defaultLogoIndex: Scalars['Int']['output'];
Expand Down Expand Up @@ -3883,6 +3885,11 @@ export type WorkspaceTeamArgs = {
filter?: InputMaybe<WorkspaceTeamFilter>;
};

export type WorkspaceBilling = {
__typename?: 'WorkspaceBilling';
versionsCount?: Maybe<WorkspaceVersionsCount>;
};

export type WorkspaceCollaborator = {
__typename?: 'WorkspaceCollaborator';
id: Scalars['ID']['output'];
Expand Down Expand Up @@ -4086,6 +4093,14 @@ export type WorkspaceUpdateInput = {
name?: InputMaybe<Scalars['String']['input']>;
};

export type WorkspaceVersionsCount = {
__typename?: 'WorkspaceVersionsCount';
/** Total number of versions of all projects in the workspace */
current: Scalars['Int']['output'];
/** Maximum number of version of all projects in the workspace with no additional cost */
max: Scalars['Int']['output'];
};

export type CrossSyncCommitBranchMetadataQueryVariables = Exact<{
streamId: Scalars['String']['input'];
commitId: Scalars['String']['input'];
Expand Down
1 change: 1 addition & 0 deletions packages/server/modules/gatekeeper/domain/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const WORKSPACE_MAX_PROJECTS_VERSIONS = 500
6 changes: 6 additions & 0 deletions packages/server/modules/workspaces/domain/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
16 changes: 15 additions & 1 deletion packages/server/modules/workspaces/graph/resolvers/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ import {
deleteWorkspaceDomainFactory,
getWorkspaceDomainsFactory,
getUserDiscoverableWorkspacesFactory,
getWorkspaceWithDomainsFactory
getWorkspaceWithDomainsFactory,
countProjectsVersionsByWorkspaceIdFactory
} from '@/modules/workspaces/repositories/workspaces'
import {
buildWorkspaceInviteEmailContentsFactory,
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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: {
Expand Down
22 changes: 21 additions & 1 deletion packages/server/modules/workspaces/repositories/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
WorkspaceWithOptionalRole
} from '@/modules/workspacesCore/domain/types'
import {
CountProjectsVersionsByWorkspaceId,
DeleteWorkspace,
DeleteWorkspaceDomain,
DeleteWorkspaceRole,
Expand All @@ -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 {
Expand Down Expand Up @@ -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())
}
23 changes: 23 additions & 0 deletions packages/server/modules/workspaces/tests/helpers/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 0ac36af

Please sign in to comment.