mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-31 17:19:56 +08:00
@@ -0,0 +1,57 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export enum FeatureType {
|
||||
Feature = 0,
|
||||
Quota = 1,
|
||||
}
|
||||
|
||||
// TODO(@forehalo): quota is a useless extra concept, merge it with feature
|
||||
export const UserPlanQuotaConfig = z.object({
|
||||
// quota name
|
||||
name: z.string(),
|
||||
// single blob limit
|
||||
blobLimit: z.number(),
|
||||
// total blob limit
|
||||
storageQuota: z.number(),
|
||||
// history period of validity
|
||||
historyPeriod: z.number(),
|
||||
// member limit
|
||||
memberLimit: z.number(),
|
||||
// copilot action limit 10
|
||||
copilotActionLimit: z.number(),
|
||||
});
|
||||
|
||||
export const WorkspaceQuotaConfig = UserPlanQuotaConfig.extend({
|
||||
// seat quota
|
||||
seatQuota: z.number(),
|
||||
}).omit({
|
||||
copilotActionLimit: true,
|
||||
});
|
||||
|
||||
function feature<T extends z.ZodRawShape>(configs: z.ZodObject<T>) {
|
||||
return z.object({
|
||||
type: z.literal(FeatureType.Feature),
|
||||
configs: configs,
|
||||
});
|
||||
}
|
||||
|
||||
function quota<T extends z.ZodRawShape>(configs: z.ZodObject<T>) {
|
||||
return z.object({
|
||||
type: z.literal(FeatureType.Quota),
|
||||
configs: configs,
|
||||
});
|
||||
}
|
||||
|
||||
export const Features = {
|
||||
copilot: feature(z.object({})),
|
||||
early_access: feature(z.object({ whitelist: z.array(z.string()) })),
|
||||
unlimited_workspace: feature(z.object({})),
|
||||
unlimited_copilot: feature(z.object({})),
|
||||
ai_early_access: feature(z.object({})),
|
||||
administrator: feature(z.object({})),
|
||||
free_plan_v1: quota(UserPlanQuotaConfig),
|
||||
pro_plan_v1: quota(UserPlanQuotaConfig),
|
||||
lifetime_pro_plan_v1: quota(UserPlanQuotaConfig),
|
||||
restricted_plan_v1: quota(UserPlanQuotaConfig),
|
||||
team_plan_v1: quota(WorkspaceQuotaConfig),
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './feature';
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Feature, PrismaClient } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PrismaTransaction } from '../base';
|
||||
import { Features, FeatureType } from './common';
|
||||
|
||||
type FeatureNames = keyof typeof Features;
|
||||
type FeatureConfigs<T extends FeatureNames> = z.infer<
|
||||
(typeof Features)[T]['shape']['configs']
|
||||
>;
|
||||
|
||||
// TODO(@forehalo):
|
||||
// `version` column in `features` table will deprecated because it's makes the whole system complicated without any benefits.
|
||||
// It was brought to introduce a version control for features, but the version controlling is not and will not actually needed.
|
||||
// It even makes things harder when a new version of an existing feature is released.
|
||||
// We have to manually update all the users and workspaces binding to the latest version, which are thousands of handreds.
|
||||
// This is a huge burden for us and we should remove it.
|
||||
@Injectable()
|
||||
export class FeatureModel {
|
||||
private readonly logger = new Logger(FeatureModel.name);
|
||||
|
||||
constructor(private readonly db: PrismaClient) {}
|
||||
|
||||
async get<T extends FeatureNames>(name: T) {
|
||||
const feature = await this.getLatest(this.db, name);
|
||||
|
||||
// All features are hardcoded in the codebase
|
||||
// It would be a fatal error if the feature is not found in DB.
|
||||
if (!feature) {
|
||||
throw new Error(`Feature ${name} not found`);
|
||||
}
|
||||
|
||||
const shape = this.getConfigShape(name);
|
||||
const parseResult = shape.safeParse(feature.configs);
|
||||
|
||||
if (!parseResult.success) {
|
||||
throw new Error(`Invalid feature config for ${name}`, {
|
||||
cause: parseResult.error,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...feature,
|
||||
configs: parseResult.data as FeatureConfigs<T>,
|
||||
};
|
||||
}
|
||||
|
||||
async upsert<T extends FeatureNames>(name: T, configs: FeatureConfigs<T>) {
|
||||
const shape = this.getConfigShape(name);
|
||||
const parseResult = shape.safeParse(configs);
|
||||
|
||||
if (!parseResult.success) {
|
||||
throw new Error(`Invalid feature config for ${name}`, {
|
||||
cause: parseResult.error,
|
||||
});
|
||||
}
|
||||
|
||||
const parsedConfigs = parseResult.data;
|
||||
|
||||
// TODO(@forehalo):
|
||||
// could be a simple upsert operation, but we got useless `version` column in the database
|
||||
// will be fixed when `version` column gets deprecated
|
||||
const feature = await this.db.$transaction(async tx => {
|
||||
const latest = await this.getLatest(tx, name);
|
||||
|
||||
if (!latest) {
|
||||
return await tx.feature.create({
|
||||
data: {
|
||||
type: FeatureType.Feature,
|
||||
feature: name,
|
||||
configs: parsedConfigs,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return await tx.feature.update({
|
||||
where: { id: latest.id },
|
||||
data: {
|
||||
configs: parsedConfigs,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.verbose(`Feature ${name} upserted`);
|
||||
|
||||
return feature as Feature & { configs: FeatureConfigs<T> };
|
||||
}
|
||||
|
||||
private async getLatest<T extends FeatureNames>(
|
||||
client: PrismaTransaction,
|
||||
name: T
|
||||
) {
|
||||
return client.feature.findFirst({
|
||||
where: { feature: name },
|
||||
orderBy: { version: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
private getConfigShape(name: FeatureNames): z.ZodObject<any> {
|
||||
return Features[name]?.shape.configs ?? z.object({});
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,24 @@
|
||||
import { Global, Injectable, Module } from '@nestjs/common';
|
||||
|
||||
import { FeatureModel } from './feature';
|
||||
import { SessionModel } from './session';
|
||||
import { UserModel } from './user';
|
||||
import { VerificationTokenModel } from './verification-token';
|
||||
|
||||
export * from './session';
|
||||
export * from './user';
|
||||
export * from './verification-token';
|
||||
|
||||
const models = [UserModel, SessionModel, VerificationTokenModel] as const;
|
||||
const models = [
|
||||
UserModel,
|
||||
SessionModel,
|
||||
VerificationTokenModel,
|
||||
FeatureModel,
|
||||
] as const;
|
||||
|
||||
@Injectable()
|
||||
export class Models {
|
||||
constructor(
|
||||
public readonly user: UserModel,
|
||||
public readonly session: SessionModel,
|
||||
public readonly verificationToken: VerificationTokenModel
|
||||
public readonly verificationToken: VerificationTokenModel,
|
||||
public readonly feature: FeatureModel
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -25,3 +28,8 @@ export class Models {
|
||||
exports: [Models],
|
||||
})
|
||||
export class ModelModules {}
|
||||
|
||||
export * from './feature';
|
||||
export * from './session';
|
||||
export * from './user';
|
||||
export * from './verification-token';
|
||||
|
||||
Reference in New Issue
Block a user