refactor(server): use feature model (#9932)

This commit is contained in:
forehalo
2025-02-05 10:27:26 +00:00
parent 0ff8d3af6f
commit 7826e2b7c8
121 changed files with 1723 additions and 3826 deletions
@@ -1,63 +1,75 @@
import { z } from 'zod';
export enum FeatureType {
Feature = 0,
Quota = 1,
}
import { OneDay, OneGB, OneMB } from '../../base';
// TODO(@forehalo): quota is a useless extra concept, merge it with feature
export const UserPlanQuotaConfig = z.object({
const UserPlanQuotaConfig = z.object({
// quota name
name: z.string(),
// single blob limit
blobLimit: z.number(),
// server limit will larger then client to handle a edge case:
// when a user downgrades from pro to free, he can still continue
// to upload previously added files that exceed the free limit
// NOTE: this is a product decision, may change in future
businessBlobLimit: z.number().optional(),
// 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(),
// copilot action limit
copilotActionLimit: z.number().optional(),
});
export const WorkspaceQuotaConfig = UserPlanQuotaConfig.extend({
export type UserQuota = z.infer<typeof UserPlanQuotaConfig>;
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,
});
export type WorkspaceQuota = z.infer<typeof WorkspaceQuotaConfig>;
const EMPTY_CONFIG = z.object({});
export enum FeatureType {
Feature,
Quota,
}
function quota<T extends z.ZodRawShape>(configs: z.ZodObject<T>) {
return z.object({
type: z.literal(FeatureType.Quota),
configs: configs,
});
export enum Feature {
// user
Admin = 'administrator',
EarlyAccess = 'early_access',
AIEarlyAccess = 'ai_early_access',
UnlimitedCopilot = 'unlimited_copilot',
FreePlan = 'free_plan_v1',
ProPlan = 'pro_plan_v1',
LifetimeProPlan = 'lifetime_pro_plan_v1',
// workspace
UnlimitedWorkspace = 'unlimited_workspace',
TeamPlan = 'team_plan_v1',
}
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),
};
// TODO(@forehalo): may merge `FeatureShapes` and `FeatureConfigs`?
export const FeaturesShapes = {
early_access: z.object({ whitelist: z.array(z.string()) }),
unlimited_workspace: EMPTY_CONFIG,
unlimited_copilot: EMPTY_CONFIG,
ai_early_access: EMPTY_CONFIG,
administrator: EMPTY_CONFIG,
free_plan_v1: UserPlanQuotaConfig,
pro_plan_v1: UserPlanQuotaConfig,
lifetime_pro_plan_v1: UserPlanQuotaConfig,
team_plan_v1: WorkspaceQuotaConfig,
} satisfies Record<Feature, z.ZodObject<any>>;
export type UserFeatureName = keyof Pick<
typeof Features,
typeof FeaturesShapes,
| 'early_access'
| 'ai_early_access'
| 'unlimited_copilot'
@@ -65,14 +77,97 @@ export type UserFeatureName = keyof Pick<
| 'free_plan_v1'
| 'pro_plan_v1'
| 'lifetime_pro_plan_v1'
| 'restricted_plan_v1'
>;
export type WorkspaceFeatureName = keyof Pick<
typeof Features,
typeof FeaturesShapes,
'unlimited_workspace' | 'team_plan_v1'
>;
export type FeatureName = UserFeatureName | WorkspaceFeatureName;
export type FeatureConfigs<T extends FeatureName> = z.infer<
(typeof Features)[T]['shape']['configs']
export type FeatureConfig<T extends FeatureName> = z.infer<
(typeof FeaturesShapes)[T]
>;
export const FeatureConfigs: {
[K in FeatureName]: {
type: FeatureType;
configs: FeatureConfig<K>;
deprecatedVersion: number;
};
} = {
free_plan_v1: {
type: FeatureType.Quota,
deprecatedVersion: 4,
configs: {
// quota name
name: 'Free',
blobLimit: 10 * OneMB,
businessBlobLimit: 100 * OneMB,
storageQuota: 10 * OneGB,
historyPeriod: 7 * OneDay,
memberLimit: 3,
copilotActionLimit: 10,
},
},
pro_plan_v1: {
type: FeatureType.Quota,
deprecatedVersion: 2,
configs: {
name: 'Pro',
blobLimit: 100 * OneMB,
storageQuota: 100 * OneGB,
historyPeriod: 30 * OneDay,
memberLimit: 10,
copilotActionLimit: 10,
},
},
lifetime_pro_plan_v1: {
type: FeatureType.Quota,
deprecatedVersion: 1,
configs: {
name: 'Lifetime Pro',
blobLimit: 100 * OneMB,
storageQuota: 1024 * OneGB,
historyPeriod: 30 * OneDay,
memberLimit: 10,
copilotActionLimit: 10,
},
},
team_plan_v1: {
type: FeatureType.Quota,
deprecatedVersion: 1,
configs: {
name: 'Team Workspace',
blobLimit: 500 * OneMB,
storageQuota: 100 * OneGB,
seatQuota: 20 * OneGB,
historyPeriod: 30 * OneDay,
memberLimit: 1,
},
},
early_access: {
type: FeatureType.Feature,
deprecatedVersion: 2,
configs: { whitelist: [] },
},
unlimited_workspace: {
type: FeatureType.Feature,
deprecatedVersion: 1,
configs: {},
},
unlimited_copilot: {
type: FeatureType.Feature,
deprecatedVersion: 1,
configs: {},
},
ai_early_access: {
type: FeatureType.Feature,
deprecatedVersion: 1,
configs: {},
},
administrator: {
type: FeatureType.Feature,
deprecatedVersion: 1,
configs: {},
},
};