mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 09:21:24 +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',
|
name: 'Free',
|
||||||
storageQuota: 10737418240,
|
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 { User } from '@prisma/client';
|
||||||
import ava, { TestFn } from 'ava';
|
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';
|
import { createTestingModule, TestingModule } from '../utils';
|
||||||
|
|
||||||
interface Context {
|
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');
|
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 => {
|
test('should create feature', async t => {
|
||||||
const { feature } = t.context;
|
const { feature } = t.context;
|
||||||
|
|
||||||
|
// @ts-expect-error internal
|
||||||
const newFeature = await feature.upsert(
|
const newFeature = await feature.upsert(
|
||||||
'new_feature' as any,
|
'new_feature' as any,
|
||||||
{},
|
{},
|
||||||
@@ -104,6 +105,7 @@ test('should update feature', async t => {
|
|||||||
const { feature } = t.context;
|
const { feature } = t.context;
|
||||||
const freePlanFeature = await feature.get('free_plan_v1');
|
const freePlanFeature = await feature.get('free_plan_v1');
|
||||||
|
|
||||||
|
// @ts-expect-error internal
|
||||||
const newFreePlanFeature = await feature.upsert(
|
const newFreePlanFeature = await feature.upsert(
|
||||||
'free_plan_v1',
|
'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 => {
|
test('should throw if feature config is invalid when updating', async t => {
|
||||||
const { feature } = t.context;
|
const { feature } = t.context;
|
||||||
await t.throwsAsync(
|
await t.throwsAsync(
|
||||||
|
// @ts-expect-error internal
|
||||||
feature.upsert('free_plan_v1', {} as any, FeatureType.Quota, 1),
|
feature.upsert('free_plan_v1', {} as any, FeatureType.Quota, 1),
|
||||||
{
|
{
|
||||||
message: 'Invalid feature config for free_plan_v1',
|
message: 'Invalid feature config for free_plan_v1',
|
||||||
|
|||||||
@@ -52,10 +52,12 @@ const initTestingDB = async (ref: ModuleRef) => {
|
|||||||
|
|
||||||
export type TestingModule = BaseTestingModule & {
|
export type TestingModule = BaseTestingModule & {
|
||||||
initTestingDB(): Promise<void>;
|
initTestingDB(): Promise<void>;
|
||||||
|
[Symbol.asyncDispose](): Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TestingApp = INestApplication & {
|
export type TestingApp = INestApplication & {
|
||||||
initTestingDB(): Promise<void>;
|
initTestingDB(): Promise<void>;
|
||||||
|
[Symbol.asyncDispose](): Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function dedupeModules(modules: NonNullable<ModuleMetadata['imports']>) {
|
function dedupeModules(modules: NonNullable<ModuleMetadata['imports']>) {
|
||||||
@@ -83,7 +85,7 @@ class MockResolver {
|
|||||||
export async function createTestingModule(
|
export async function createTestingModule(
|
||||||
moduleDef: TestingModuleMeatdata = {},
|
moduleDef: TestingModuleMeatdata = {},
|
||||||
autoInitialize = true
|
autoInitialize = true
|
||||||
) {
|
): Promise<TestingModule> {
|
||||||
// setting up
|
// setting up
|
||||||
let imports = moduleDef.imports ?? [];
|
let imports = moduleDef.imports ?? [];
|
||||||
imports =
|
imports =
|
||||||
@@ -129,6 +131,9 @@ export async function createTestingModule(
|
|||||||
// by pass password min length validation
|
// by pass password min length validation
|
||||||
await runtime.set('auth/password.min', 1);
|
await runtime.set('auth/password.min', 1);
|
||||||
};
|
};
|
||||||
|
testingModule[Symbol.asyncDispose] = async () => {
|
||||||
|
await m.close();
|
||||||
|
};
|
||||||
|
|
||||||
if (autoInitialize) {
|
if (autoInitialize) {
|
||||||
await testingModule.initTestingDB();
|
await testingModule.initTestingDB();
|
||||||
@@ -138,7 +143,9 @@ export async function createTestingModule(
|
|||||||
return testingModule;
|
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 m = await createTestingModule(moduleDef, false);
|
||||||
|
|
||||||
const app = m.createNestApplication({
|
const app = m.createNestApplication({
|
||||||
@@ -169,7 +176,10 @@ export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) {
|
|||||||
await app.init();
|
await app.init();
|
||||||
|
|
||||||
app.initTestingDB = m.initTestingDB.bind(m);
|
app.initTestingDB = m.initTestingDB.bind(m);
|
||||||
|
app[Symbol.asyncDispose] = async () => {
|
||||||
|
await m[Symbol.asyncDispose]();
|
||||||
|
await app.close();
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
module: m,
|
module: m,
|
||||||
app: app,
|
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.
|
* Get the latest feature from database.
|
||||||
*
|
*
|
||||||
@@ -121,11 +83,66 @@ export class FeatureModel extends BaseModel {
|
|||||||
return FeatureConfigs[name].type;
|
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() {
|
async refreshFeatures() {
|
||||||
for (const key in FeatureConfigs) {
|
for (const key in FeatureConfigs) {
|
||||||
const name = key as FeatureName;
|
const name = key as FeatureName;
|
||||||
const def = FeatureConfigs[name];
|
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