@@ -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<
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|
||||||
|
|||||||
@@ -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({}),
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ type WorkspaceType {
|
|||||||
enum FeatureType {
|
enum FeatureType {
|
||||||
Copilot
|
Copilot
|
||||||
EarlyAccess
|
EarlyAccess
|
||||||
|
UnlimitedWorkspace
|
||||||
}
|
}
|
||||||
|
|
||||||
type InvitationWorkspaceType {
|
type InvitationWorkspaceType {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user