feat: add workspace level feature apis (#5503)

This commit is contained in:
DarkSky
2024-01-05 04:13:49 +00:00
parent 04ca554525
commit f6ec786ef9
25 changed files with 497 additions and 244 deletions
@@ -40,15 +40,6 @@ export class EarlyAccessFeatureConfig extends FeatureConfig {
throw new Error('Invalid feature config: type is not EarlyAccess');
}
}
checkWhiteList(email: string) {
for (const domain in this.config.configs.whitelist) {
if (email.endsWith(domain)) {
return true;
}
}
return false;
}
}
const FeatureConfigMap = {
@@ -1,35 +1,32 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { Config } from '../../config';
import { PrismaService } from '../../prisma';
import { EarlyAccessFeatureConfig } from './feature';
import { FeatureService } from './service';
import { FeatureType } from './types';
enum NewFeaturesKind {
EarlyAccess,
}
const STAFF = ['@toeverything.info'];
@Injectable()
export class FeatureManagementService implements OnModuleInit {
export class FeatureManagementService {
protected logger = new Logger(FeatureManagementService.name);
private earlyAccessFeature?: EarlyAccessFeatureConfig;
constructor(
private readonly feature: FeatureService,
private readonly prisma: PrismaService,
private readonly config: Config
) {}
async onModuleInit() {
this.earlyAccessFeature = await this.feature.getFeature(
FeatureType.EarlyAccess
);
}
// ======== Admin ========
// todo(@darkskygit): replace this with abac
isStaff(email: string) {
return this.earlyAccessFeature?.checkWhiteList(email) ?? false;
for (const domain of STAFF) {
if (email.endsWith(domain)) {
return true;
}
}
return false;
}
// ======== Early Access ========
@@ -38,7 +35,7 @@ export class FeatureManagementService implements OnModuleInit {
return this.feature.addUserFeature(
userId,
FeatureType.EarlyAccess,
1,
2,
'Early access user'
);
}
@@ -63,23 +60,8 @@ export class FeatureManagementService implements OnModuleInit {
const canEarlyAccess = await this.feature
.hasUserFeature(user.id, FeatureType.EarlyAccess)
.catch(() => false);
if (canEarlyAccess) {
return true;
}
// TODO: Outdated, switch to feature gates
const oldCanEarlyAccess = await this.prisma.newFeaturesWaitingList
.findUnique({
where: { email, type: NewFeaturesKind.EarlyAccess },
})
.then(x => !!x)
.catch(() => false);
if (oldCanEarlyAccess) {
this.logger.warn(
`User ${email} has early access in old table but not in new table`
);
}
return oldCanEarlyAccess;
return canEarlyAccess;
}
return false;
} else {
@@ -292,9 +292,7 @@ export class FeatureService {
return configs.filter(feature => !!feature.feature);
}
async listFeatureWorkspaces(
feature: FeatureType
): Promise<Omit<WorkspaceType, 'members'>[]> {
async listFeatureWorkspaces(feature: FeatureType): Promise<WorkspaceType[]> {
return this.prisma.workspaceFeatures
.findMany({
where: {
@@ -314,7 +312,7 @@ export class FeatureService {
},
},
})
.then(wss => wss.map(ws => ws.workspace));
.then(wss => wss.map(ws => ws.workspace as WorkspaceType));
}
async hasWorkspaceFeature(workspaceId: string, feature: FeatureType) {
@@ -1,4 +1,11 @@
import { registerEnumType } from '@nestjs/graphql';
export enum FeatureType {
Copilot = 'copilot',
EarlyAccess = 'early_access',
}
registerEnumType(FeatureType, {
name: 'FeatureType',
description: 'The type of workspace feature',
});
@@ -1,24 +1,8 @@
import { URL } from 'node:url';
import { z } from 'zod';
import { FeatureType } from './common';
function checkHostname(host: string) {
try {
return new URL(`https://${host}`).hostname === host;
} catch (_) {
return false;
}
}
export const featureEarlyAccess = z.object({
feature: z.literal(FeatureType.EarlyAccess),
configs: z.object({
whitelist: z
.string()
.startsWith('@')
.refine(domain => checkHostname(domain.slice(1)))
.array(),
}),
configs: z.object({}),
});
@@ -37,6 +37,12 @@ export const Features: Feature[] = [
whitelist: ['@toeverything.info'],
},
},
{
feature: FeatureType.EarlyAccess,
type: FeatureKind.Feature,
version: 2,
configs: {},
},
];
/// ======== schema infer ========