feat: add unlimited workspace support (#5523)

fix AFF-505
This commit is contained in:
DarkSky
2024-01-10 07:28:52 +00:00
parent a59fe1b49e
commit 0d7ffb0511
7 changed files with 64 additions and 16 deletions

View File

@@ -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 = { const FeatureConfigMap = {
[FeatureType.Copilot]: CopilotFeatureConfig, [FeatureType.Copilot]: CopilotFeatureConfig,
[FeatureType.EarlyAccess]: EarlyAccessFeatureConfig, [FeatureType.EarlyAccess]: EarlyAccessFeatureConfig,
[FeatureType.UnlimitedWorkspace]: UnlimitedWorkspaceFeatureConfig,
}; };
export type FeatureConfigType<F extends FeatureType> = InstanceType< export type FeatureConfigType<F extends FeatureType> = InstanceType<

View File

@@ -3,6 +3,7 @@ import { registerEnumType } from '@nestjs/graphql';
export enum FeatureType { export enum FeatureType {
Copilot = 'copilot', Copilot = 'copilot',
EarlyAccess = 'early_access', EarlyAccess = 'early_access',
UnlimitedWorkspace = 'unlimited_workspace',
} }
registerEnumType(FeatureType, { registerEnumType(FeatureType, {

View File

@@ -3,6 +3,7 @@ import { z } from 'zod';
import { FeatureType } from './common'; import { FeatureType } from './common';
import { featureCopilot } from './copilot'; import { featureCopilot } from './copilot';
import { featureEarlyAccess } from './early-access'; import { featureEarlyAccess } from './early-access';
import { featureUnlimitedWorkspace } from './unlimited-workspace';
/// ======== common schema ======== /// ======== common schema ========
@@ -43,6 +44,12 @@ export const Features: Feature[] = [
version: 2, version: 2,
configs: {}, configs: {},
}, },
{
feature: FeatureType.UnlimitedWorkspace,
type: FeatureKind.Feature,
version: 1,
configs: {},
},
]; ];
/// ======== schema infer ======== /// ======== schema infer ========
@@ -51,7 +58,13 @@ export const FeatureSchema = commonFeatureSchema
.extend({ .extend({
type: z.literal(FeatureKind.Feature), type: z.literal(FeatureKind.Feature),
}) })
.and(z.discriminatedUnion('feature', [featureCopilot, featureEarlyAccess])); .and(
z.discriminatedUnion('feature', [
featureCopilot,
featureEarlyAccess,
featureUnlimitedWorkspace,
])
);
export type Feature = z.infer<typeof FeatureSchema>; export type Feature = z.infer<typeof FeatureSchema>;

View File

@@ -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({}),
});

View File

@@ -28,6 +28,7 @@ import type { FileUpload } from '../../../types';
import { Auth, CurrentUser, Public } from '../../auth'; import { Auth, CurrentUser, Public } from '../../auth';
import { MailService } from '../../auth/mailer'; import { MailService } from '../../auth/mailer';
import { AuthService } from '../../auth/service'; import { AuthService } from '../../auth/service';
import { FeatureManagementService, FeatureType } from '../../features';
import { QuotaManagementService } from '../../quota'; import { QuotaManagementService } from '../../quota';
import { WorkspaceBlobStorage } from '../../storage'; import { WorkspaceBlobStorage } from '../../storage';
import { UsersService, UserType } from '../../users'; import { UsersService, UserType } from '../../users';
@@ -57,6 +58,7 @@ export class WorkspaceResolver {
private readonly mailer: MailService, private readonly mailer: MailService,
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly permissions: PermissionService, private readonly permissions: PermissionService,
private readonly feature: FeatureManagementService,
private readonly quota: QuotaManagementService, private readonly quota: QuotaManagementService,
private readonly users: UsersService, private readonly users: UsersService,
private readonly event: EventEmitter, private readonly event: EventEmitter,
@@ -325,20 +327,26 @@ export class WorkspaceResolver {
throw new ForbiddenException('Cannot change owner'); throw new ForbiddenException('Cannot change owner');
} }
// member limit check const unlimited = await this.feature.hasWorkspaceFeature(
const [memberCount, quota] = await Promise.all([ workspaceId,
this.prisma.workspaceUserPermission.count({ FeatureType.UnlimitedWorkspace
where: { workspaceId }, );
}), if (!unlimited) {
this.quota.getUserQuota(user.id), // member limit check
]); const [memberCount, quota] = await Promise.all([
if (memberCount >= quota.memberLimit) { this.prisma.workspaceUserPermission.count({
throw new GraphQLError('Workspace member limit reached', { where: { workspaceId },
extensions: { }),
status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE], this.quota.getUserQuota(user.id),
code: HttpStatus.PAYLOAD_TOO_LARGE, ]);
}, 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); let target = await this.users.findUserByEmail(email);

View File

@@ -152,6 +152,7 @@ type WorkspaceType {
enum FeatureType { enum FeatureType {
Copilot Copilot
EarlyAccess EarlyAccess
UnlimitedWorkspace
} }
type InvitationWorkspaceType { type InvitationWorkspaceType {

View File

@@ -129,7 +129,11 @@ test.beforeEach(async t => {
.overrideProvider(PrismaService) .overrideProvider(PrismaService)
.useValue(FakePrisma) .useValue(FakePrisma)
.overrideProvider(FeatureManagementService) .overrideProvider(FeatureManagementService)
.useValue({}) .useValue({
hasWorkspaceFeature() {
return false;
},
})
.compile(); .compile();
const app = module.createNestApplication(); const app = module.createNestApplication();
app.use( app.use(