mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 15:46:29 +08:00
feat(server): align pro plan for free in selfhost (#9973)
close AF-2099
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -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'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -52,10 +52,12 @@ const initTestingDB = async (ref: ModuleRef) => {
|
||||
|
||||
export type TestingModule = BaseTestingModule & {
|
||||
initTestingDB(): Promise<void>;
|
||||
[Symbol.asyncDispose](): Promise<void>;
|
||||
};
|
||||
|
||||
export type TestingApp = INestApplication & {
|
||||
initTestingDB(): Promise<void>;
|
||||
[Symbol.asyncDispose](): Promise<void>;
|
||||
};
|
||||
|
||||
function dedupeModules(modules: NonNullable<ModuleMetadata['imports']>) {
|
||||
@@ -83,7 +85,7 @@ class MockResolver {
|
||||
export async function createTestingModule(
|
||||
moduleDef: TestingModuleMeatdata = {},
|
||||
autoInitialize = true
|
||||
) {
|
||||
): Promise<TestingModule> {
|
||||
// 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,
|
||||
|
||||
@@ -29,44 +29,6 @@ export class FeatureModel extends BaseModel {
|
||||
};
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async upsert<T extends FeatureName>(
|
||||
name: T,
|
||||
configs: FeatureConfig<T>,
|
||||
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<T> };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest feature from database.
|
||||
*
|
||||
@@ -121,11 +83,66 @@ export class FeatureModel extends BaseModel {
|
||||
return FeatureConfigs[name].type;
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
private async upsert<T extends FeatureName>(
|
||||
name: T,
|
||||
configs: FeatureConfig<T>,
|
||||
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<T> };
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user