From 0d7ffb05113162992d0e126162bb9a1f8cdbf222 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Wed, 10 Jan 2024 07:28:52 +0000 Subject: [PATCH] feat: add unlimited workspace support (#5523) fix AFF-505 --- .../server/src/modules/features/feature.ts | 13 +++++++ .../src/modules/features/types/common.ts | 1 + .../src/modules/features/types/index.ts | 15 +++++++- .../features/types/unlimited-workspace.ts | 8 +++++ .../modules/workspaces/resolvers/workspace.ts | 36 +++++++++++-------- packages/backend/server/src/schema.gql | 1 + packages/backend/server/tests/mailer.spec.ts | 6 +++- 7 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 packages/backend/server/src/modules/features/types/unlimited-workspace.ts diff --git a/packages/backend/server/src/modules/features/feature.ts b/packages/backend/server/src/modules/features/feature.ts index 9f585ed99..3945790b6 100644 --- a/packages/backend/server/src/modules/features/feature.ts +++ b/packages/backend/server/src/modules/features/feature.ts @@ -42,9 +42,22 @@ export class EarlyAccessFeatureConfig extends FeatureConfig { } } +export class UnlimitedWorkspaceFeatureConfig extends FeatureConfig { + override config!: Feature & { feature: FeatureType.UnlimitedWorkspace }; + + constructor(data: any) { + super(data); + + if (this.config.feature !== FeatureType.UnlimitedWorkspace) { + throw new Error('Invalid feature config: type is not EarlyAccess'); + } + } +} + const FeatureConfigMap = { [FeatureType.Copilot]: CopilotFeatureConfig, [FeatureType.EarlyAccess]: EarlyAccessFeatureConfig, + [FeatureType.UnlimitedWorkspace]: UnlimitedWorkspaceFeatureConfig, }; export type FeatureConfigType = InstanceType< diff --git a/packages/backend/server/src/modules/features/types/common.ts b/packages/backend/server/src/modules/features/types/common.ts index 913705567..3095b49e0 100644 --- a/packages/backend/server/src/modules/features/types/common.ts +++ b/packages/backend/server/src/modules/features/types/common.ts @@ -3,6 +3,7 @@ import { registerEnumType } from '@nestjs/graphql'; export enum FeatureType { Copilot = 'copilot', EarlyAccess = 'early_access', + UnlimitedWorkspace = 'unlimited_workspace', } registerEnumType(FeatureType, { diff --git a/packages/backend/server/src/modules/features/types/index.ts b/packages/backend/server/src/modules/features/types/index.ts index 0fd8b6232..0f82a393a 100644 --- a/packages/backend/server/src/modules/features/types/index.ts +++ b/packages/backend/server/src/modules/features/types/index.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { FeatureType } from './common'; import { featureCopilot } from './copilot'; import { featureEarlyAccess } from './early-access'; +import { featureUnlimitedWorkspace } from './unlimited-workspace'; /// ======== common schema ======== @@ -43,6 +44,12 @@ export const Features: Feature[] = [ version: 2, configs: {}, }, + { + feature: FeatureType.UnlimitedWorkspace, + type: FeatureKind.Feature, + version: 1, + configs: {}, + }, ]; /// ======== schema infer ======== @@ -51,7 +58,13 @@ export const FeatureSchema = commonFeatureSchema .extend({ type: z.literal(FeatureKind.Feature), }) - .and(z.discriminatedUnion('feature', [featureCopilot, featureEarlyAccess])); + .and( + z.discriminatedUnion('feature', [ + featureCopilot, + featureEarlyAccess, + featureUnlimitedWorkspace, + ]) + ); export type Feature = z.infer; diff --git a/packages/backend/server/src/modules/features/types/unlimited-workspace.ts b/packages/backend/server/src/modules/features/types/unlimited-workspace.ts new file mode 100644 index 000000000..b9b471e9e --- /dev/null +++ b/packages/backend/server/src/modules/features/types/unlimited-workspace.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +import { FeatureType } from './common'; + +export const featureUnlimitedWorkspace = z.object({ + feature: z.literal(FeatureType.UnlimitedWorkspace), + configs: z.object({}), +}); diff --git a/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts b/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts index abd7a9c52..806952155 100644 --- a/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts +++ b/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts @@ -28,6 +28,7 @@ import type { FileUpload } from '../../../types'; import { Auth, CurrentUser, Public } from '../../auth'; import { MailService } from '../../auth/mailer'; import { AuthService } from '../../auth/service'; +import { FeatureManagementService, FeatureType } from '../../features'; import { QuotaManagementService } from '../../quota'; import { WorkspaceBlobStorage } from '../../storage'; import { UsersService, UserType } from '../../users'; @@ -57,6 +58,7 @@ export class WorkspaceResolver { private readonly mailer: MailService, private readonly prisma: PrismaService, private readonly permissions: PermissionService, + private readonly feature: FeatureManagementService, private readonly quota: QuotaManagementService, private readonly users: UsersService, private readonly event: EventEmitter, @@ -325,20 +327,26 @@ export class WorkspaceResolver { throw new ForbiddenException('Cannot change owner'); } - // member limit check - const [memberCount, quota] = await Promise.all([ - this.prisma.workspaceUserPermission.count({ - where: { workspaceId }, - }), - this.quota.getUserQuota(user.id), - ]); - if (memberCount >= quota.memberLimit) { - throw new GraphQLError('Workspace member limit reached', { - extensions: { - status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE], - code: HttpStatus.PAYLOAD_TOO_LARGE, - }, - }); + const unlimited = await this.feature.hasWorkspaceFeature( + workspaceId, + FeatureType.UnlimitedWorkspace + ); + if (!unlimited) { + // member limit check + const [memberCount, quota] = await Promise.all([ + this.prisma.workspaceUserPermission.count({ + where: { workspaceId }, + }), + this.quota.getUserQuota(user.id), + ]); + if (memberCount >= quota.memberLimit) { + throw new GraphQLError('Workspace member limit reached', { + extensions: { + status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE], + code: HttpStatus.PAYLOAD_TOO_LARGE, + }, + }); + } } let target = await this.users.findUserByEmail(email); diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 949bea685..c16ac448b 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -152,6 +152,7 @@ type WorkspaceType { enum FeatureType { Copilot EarlyAccess + UnlimitedWorkspace } type InvitationWorkspaceType { diff --git a/packages/backend/server/tests/mailer.spec.ts b/packages/backend/server/tests/mailer.spec.ts index e25355bf1..1e1da0c2e 100644 --- a/packages/backend/server/tests/mailer.spec.ts +++ b/packages/backend/server/tests/mailer.spec.ts @@ -129,7 +129,11 @@ test.beforeEach(async t => { .overrideProvider(PrismaService) .useValue(FakePrisma) .overrideProvider(FeatureManagementService) - .useValue({}) + .useValue({ + hasWorkspaceFeature() { + return false; + }, + }) .compile(); const app = module.createNestApplication(); app.use(