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 9221205f5..37cf22070 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 faecfcd62..a243d2b4c 100644 Binary files a/packages/backend/server/src/__tests__/models/__snapshots__/feature-user.spec.ts.snap and b/packages/backend/server/src/__tests__/models/__snapshots__/feature-user.spec.ts.snap differ 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 4b54096a0..e9a788807 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 1389d3fa8..8137ea9b5 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 43774f8e9..1b0143d16 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 6e87405e4..729e5dad7 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); + } } } }