From d873a78534c1b4a415c65bbf8d63222dd15fb8df Mon Sep 17 00:00:00 2001 From: forehalo Date: Thu, 6 Feb 2025 05:25:06 +0000 Subject: [PATCH] feat(server): align pro plan for free in selfhost (#9973) close AF-2099 --- .../__snapshots__/feature-user.spec.ts.md | 13 +++ .../__snapshots__/feature-user.spec.ts.snap | Bin 418 -> 470 bytes .../src/__tests__/models/feature-user.spec.ts | 25 ++++- .../src/__tests__/models/feature.spec.ts | 3 + .../server/src/__tests__/utils/utils.ts | 16 ++- packages/backend/server/src/models/feature.ts | 95 +++++++++++------- 6 files changed, 109 insertions(+), 43 deletions(-) diff --git a/packages/backend/server/src/__tests__/models/__snapshots__/feature-user.spec.ts.md b/packages/backend/server/src/__tests__/models/__snapshots__/feature-user.spec.ts.md index 9221205f5d..37cf22070f 100644 --- a/packages/backend/server/src/__tests__/models/__snapshots__/feature-user.spec.ts.md +++ b/packages/backend/server/src/__tests__/models/__snapshots__/feature-user.spec.ts.md @@ -42,3 +42,16 @@ Generated by [AVA](https://avajs.dev). name: 'Free', storageQuota: 10737418240, } + +## should use pro plan as free for selfhost instance + +> use pro plan as free plan for selfhosted instance + + { + blobLimit: 104857600, + copilotActionLimit: 10, + historyPeriod: 2592000000, + memberLimit: 10, + name: 'Pro', + storageQuota: 107374182400, + } diff --git a/packages/backend/server/src/__tests__/models/__snapshots__/feature-user.spec.ts.snap b/packages/backend/server/src/__tests__/models/__snapshots__/feature-user.spec.ts.snap index faecfcd6236654f458eb704ddc7b1d72f7de4878..a243d2b4cf48011cc97bd37a293cea86e0d88024 100644 GIT binary patch literal 470 zcmV;{0V)1LRzVuKeUa8k|+wsp;0~Akx;L zBx{IhMjwj^00000000B+l)FwGF%*XXvsbQS7br+K1r;4U0Z^b6NFgP3o|&_Z!8;y& zJOUa7NuD7^lp^6p(nWa=9w5&UmR&X*Gg3f8RJgLOPsh^tpX2Yn*x2M)K3=oe-f$_I zElnC7vX4q(7oC*WD=oPyB(q+wuG3U`wND3iv@uvTMN>Nfb^#Osz5@6O;5UFS0UHD? zQL9U>ckvM?o@l)7bUL7UP#d&>C8{k`9n|d7bZl(AtNYsD-834&Hv~*KE=A`ovi*SF zYz6FMVhe4o-%7l;`2hB!mZ}FxBUgPMA9fsb z0L%?uQiuG0V0c)hc`Nj?#zw_#bk8wIg;Dt^nzNNrHgc+cl2yj@;Xad^^foRp0khBjWcpOZmWC-@Gi-b)tv*IJ2i&uHV6`$4XG(2${&ekh^-yJ5Ub2`@k M2T0=*tV#v|0N{VwzW@LL literal 418 zcmV;T0bTwYgit`Y1r;SHfE0)#5}|}{cQZ*0&U&%E z0vZGnXP`h7kc-eE&cOjV1BCn#Oe81}64Fal^t59w? zIONb0O^)Sq8B_x0L5rwTYMIiYY=!0mse-!b3j2PO$pCk7m`acs5wS6~FYIQ{VV6P` z3#sf@XoZTVV48QxQb!o8^jU+oP|fkgtKyo&Ep)jXu$~^3c92ZucX{%&t(hI5IC#l# z^WMPla7gw|=tYX9AFvcH&Fsa}k49-uS4M5jiLha3W}e++w+)tMN M6&El$BAEmL02L*@lmGw# diff --git a/packages/backend/server/src/__tests__/models/feature-user.spec.ts b/packages/backend/server/src/__tests__/models/feature-user.spec.ts index 4b54096a0b..e9a788807d 100644 --- a/packages/backend/server/src/__tests__/models/feature-user.spec.ts +++ b/packages/backend/server/src/__tests__/models/feature-user.spec.ts @@ -1,7 +1,8 @@ import { User } from '@prisma/client'; import ava, { TestFn } from 'ava'; -import { FeatureType, UserFeatureModel, UserModel } from '../../models'; +import { ConfigModule } from '../../base/config'; +import { FeatureType, Models, UserFeatureModel, UserModel } from '../../models'; import { createTestingModule, TestingModule } from '../utils'; interface Context { @@ -123,3 +124,25 @@ test('should not switch user quota if the new quota is the same as the current o t.not(quota?.reason, 'test not switch'); }); + +test('should use pro plan as free for selfhost instance', async t => { + await using module = await createTestingModule({ + imports: [ + ConfigModule.forRoot({ + isSelfhosted: true, + }), + ], + }); + + const models = module.get(Models); + const u1 = await models.user.create({ + email: 'u1@affine.pro', + registered: true, + }); + + const quota = await models.userFeature.getQuota(u1.id); + t.snapshot( + quota?.configs, + 'use pro plan as free plan for selfhosted instance' + ); +}); diff --git a/packages/backend/server/src/__tests__/models/feature.spec.ts b/packages/backend/server/src/__tests__/models/feature.spec.ts index 1389d3fa80..8137ea9b58 100644 --- a/packages/backend/server/src/__tests__/models/feature.spec.ts +++ b/packages/backend/server/src/__tests__/models/feature.spec.ts @@ -90,6 +90,7 @@ test('should get feature if extra fields exist in feature config', async t => { test('should create feature', async t => { const { feature } = t.context; + // @ts-expect-error internal const newFeature = await feature.upsert( 'new_feature' as any, {}, @@ -104,6 +105,7 @@ test('should update feature', async t => { const { feature } = t.context; const freePlanFeature = await feature.get('free_plan_v1'); + // @ts-expect-error internal const newFreePlanFeature = await feature.upsert( 'free_plan_v1', { @@ -123,6 +125,7 @@ test('should update feature', async t => { test('should throw if feature config is invalid when updating', async t => { const { feature } = t.context; await t.throwsAsync( + // @ts-expect-error internal feature.upsert('free_plan_v1', {} as any, FeatureType.Quota, 1), { message: 'Invalid feature config for free_plan_v1', diff --git a/packages/backend/server/src/__tests__/utils/utils.ts b/packages/backend/server/src/__tests__/utils/utils.ts index 43774f8e9e..1b0143d16e 100644 --- a/packages/backend/server/src/__tests__/utils/utils.ts +++ b/packages/backend/server/src/__tests__/utils/utils.ts @@ -52,10 +52,12 @@ const initTestingDB = async (ref: ModuleRef) => { export type TestingModule = BaseTestingModule & { initTestingDB(): Promise; + [Symbol.asyncDispose](): Promise; }; export type TestingApp = INestApplication & { initTestingDB(): Promise; + [Symbol.asyncDispose](): Promise; }; function dedupeModules(modules: NonNullable) { @@ -83,7 +85,7 @@ class MockResolver { export async function createTestingModule( moduleDef: TestingModuleMeatdata = {}, autoInitialize = true -) { +): Promise { // setting up let imports = moduleDef.imports ?? []; imports = @@ -129,6 +131,9 @@ export async function createTestingModule( // by pass password min length validation await runtime.set('auth/password.min', 1); }; + testingModule[Symbol.asyncDispose] = async () => { + await m.close(); + }; if (autoInitialize) { await testingModule.initTestingDB(); @@ -138,7 +143,9 @@ export async function createTestingModule( return testingModule; } -export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) { +export async function createTestingApp( + moduleDef: TestingModuleMeatdata = {} +): Promise<{ module: TestingModule; app: TestingApp }> { const m = await createTestingModule(moduleDef, false); const app = m.createNestApplication({ @@ -169,7 +176,10 @@ export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) { await app.init(); app.initTestingDB = m.initTestingDB.bind(m); - + app[Symbol.asyncDispose] = async () => { + await m[Symbol.asyncDispose](); + await app.close(); + }; return { module: m, app: app, diff --git a/packages/backend/server/src/models/feature.ts b/packages/backend/server/src/models/feature.ts index 6e87405e4a..729e5dad72 100644 --- a/packages/backend/server/src/models/feature.ts +++ b/packages/backend/server/src/models/feature.ts @@ -29,44 +29,6 @@ export class FeatureModel extends BaseModel { }; } - @Transactional() - async upsert( - name: T, - configs: FeatureConfig, - deprecatedType: FeatureType, - deprecatedVersion: number - ) { - const parsedConfigs = this.check(name, configs); - - // 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 latest = await this.try_get_unchecked(name); - - let feature: Feature; - if (!latest) { - feature = await this.db.feature.create({ - data: { - name, - deprecatedType, - deprecatedVersion, - configs: parsedConfigs, - }, - }); - } else { - feature = await this.db.feature.update({ - where: { id: latest.id }, - data: { - configs: parsedConfigs, - }, - }); - } - - this.logger.verbose(`Feature ${name} upserted`); - - return feature as Feature & { configs: FeatureConfig }; - } - /** * Get the latest feature from database. * @@ -121,11 +83,66 @@ export class FeatureModel extends BaseModel { return FeatureConfigs[name].type; } + @Transactional() + private async upsert( + name: T, + configs: FeatureConfig, + deprecatedType: FeatureType, + deprecatedVersion: number + ) { + const parsedConfigs = this.check(name, configs); + + // 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 latest = await this.db.feature.findFirst({ + where: { + name, + }, + orderBy: { + deprecatedVersion: 'desc', + }, + }); + + let feature: Feature; + if (!latest) { + feature = await this.db.feature.create({ + data: { + name, + deprecatedType, + deprecatedVersion, + configs: parsedConfigs, + }, + }); + } else { + feature = await this.db.feature.update({ + where: { id: latest.id }, + data: { + configs: parsedConfigs, + }, + }); + } + + this.logger.verbose(`Feature ${name} upserted`); + + return feature as Feature & { configs: FeatureConfig }; + } + async refreshFeatures() { for (const key in FeatureConfigs) { const name = key as FeatureName; const def = FeatureConfigs[name]; - await this.upsert(name, def.configs, def.type, def.deprecatedVersion); + // self-hosted instance will use pro plan as free plan + if (name === 'free_plan_v1' && this.config.isSelfhosted) { + await this.upsert( + name, + FeatureConfigs['pro_plan_v1'].configs, + def.type, + def.deprecatedVersion + ); + } else { + await this.upsert(name, def.configs, def.type, def.deprecatedVersion); + } } } }