mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat: new free plan (#5604)
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
import { FeatureKind } from '../../modules/features';
|
||||||
|
import { Quotas } from '../../modules/quota';
|
||||||
|
import { upsertFeature } from './utils/user-features';
|
||||||
|
|
||||||
|
export class NewFreePlan1705395933447 {
|
||||||
|
// do the migration
|
||||||
|
static async up(db: PrismaClient) {
|
||||||
|
// add new free plan
|
||||||
|
await upsertFeature(db, Quotas[3]);
|
||||||
|
// migrate all free plan users to new free plan
|
||||||
|
await db.$transaction(async tx => {
|
||||||
|
const latestFreePlan = await tx.features.findFirstOrThrow({
|
||||||
|
where: { feature: Quotas[3].feature },
|
||||||
|
orderBy: { version: 'desc' },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// find all users that have old free plan
|
||||||
|
const userIds = await db.user.findMany({
|
||||||
|
where: {
|
||||||
|
features: {
|
||||||
|
every: {
|
||||||
|
feature: {
|
||||||
|
type: FeatureKind.Quota,
|
||||||
|
feature: Quotas[3].feature,
|
||||||
|
version: { lt: Quotas[3].version },
|
||||||
|
},
|
||||||
|
activated: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// deactivate all old quota for the user
|
||||||
|
await tx.userFeatures.updateMany({
|
||||||
|
where: {
|
||||||
|
id: undefined,
|
||||||
|
userId: {
|
||||||
|
in: userIds.map(({ id }) => id),
|
||||||
|
},
|
||||||
|
feature: {
|
||||||
|
type: FeatureKind.Quota,
|
||||||
|
},
|
||||||
|
activated: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
activated: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.userFeatures.createMany({
|
||||||
|
data: userIds.map(({ id: userId }) => ({
|
||||||
|
userId,
|
||||||
|
featureId: latestFreePlan.id,
|
||||||
|
reason: 'free plan 1.0 migration',
|
||||||
|
activated: true,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// revert the migration
|
||||||
|
static async down(_db: PrismaClient) {}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
SessionService,
|
SessionService,
|
||||||
} from '../../fundamentals';
|
} from '../../fundamentals';
|
||||||
import { FeatureType } from '../features';
|
import { FeatureType } from '../features';
|
||||||
import { Quota_FreePlanV1 } from '../quota';
|
import { Quota_FreePlanV1_1 } from '../quota';
|
||||||
import {
|
import {
|
||||||
decode,
|
decode,
|
||||||
encode,
|
encode,
|
||||||
@@ -52,7 +52,7 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
|
|||||||
activated: true,
|
activated: true,
|
||||||
feature: {
|
feature: {
|
||||||
connect: {
|
connect: {
|
||||||
feature_version: Quota_FreePlanV1,
|
feature_version: Quota_FreePlanV1_1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
PrismaService,
|
PrismaService,
|
||||||
verifyChallengeResponse,
|
verifyChallengeResponse,
|
||||||
} from '../../fundamentals';
|
} from '../../fundamentals';
|
||||||
import { Quota_FreePlanV1 } from '../quota';
|
import { Quota_FreePlanV1_1 } from '../quota';
|
||||||
|
|
||||||
export type UserClaim = Pick<
|
export type UserClaim = Pick<
|
||||||
User,
|
User,
|
||||||
@@ -201,7 +201,7 @@ export class AuthService {
|
|||||||
activated: true,
|
activated: true,
|
||||||
feature: {
|
feature: {
|
||||||
connect: {
|
connect: {
|
||||||
feature_version: Quota_FreePlanV1,
|
feature_version: Quota_FreePlanV1_1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -231,7 +231,7 @@ export class AuthService {
|
|||||||
activated: true,
|
activated: true,
|
||||||
feature: {
|
feature: {
|
||||||
connect: {
|
connect: {
|
||||||
feature_version: Quota_FreePlanV1,
|
feature_version: Quota_FreePlanV1_1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,5 +20,5 @@ import { QuotaManagementService } from './storage';
|
|||||||
export class QuotaModule {}
|
export class QuotaModule {}
|
||||||
|
|
||||||
export { QuotaManagementService, QuotaService };
|
export { QuotaManagementService, QuotaService };
|
||||||
export { Quota_FreePlanV1, Quota_ProPlanV1, Quotas } from './schema';
|
export { Quota_FreePlanV1_1, Quota_ProPlanV1, Quotas } from './schema';
|
||||||
export { QuotaQueryType, QuotaType } from './types';
|
export { QuotaQueryType, QuotaType } from './types';
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ export class QuotaConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get version() {
|
||||||
|
return this.config.version;
|
||||||
|
}
|
||||||
|
|
||||||
/// feature name of quota
|
/// feature name of quota
|
||||||
get name() {
|
get name() {
|
||||||
return this.config.feature;
|
return this.config.feature;
|
||||||
|
|||||||
@@ -54,11 +54,28 @@ export const Quotas: Quota[] = [
|
|||||||
memberLimit: 10,
|
memberLimit: 10,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
feature: QuotaType.FreePlanV1,
|
||||||
|
type: FeatureKind.Quota,
|
||||||
|
version: 2,
|
||||||
|
configs: {
|
||||||
|
// quota name
|
||||||
|
name: 'Free',
|
||||||
|
// single blob limit 10MB
|
||||||
|
blobLimit: 100 * OneMB,
|
||||||
|
// total blob limit 10GB
|
||||||
|
storageQuota: 10 * OneGB,
|
||||||
|
// history period of validity 7 days
|
||||||
|
historyPeriod: 7 * OneDay,
|
||||||
|
// member limit 3
|
||||||
|
memberLimit: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Quota_FreePlanV1 = {
|
export const Quota_FreePlanV1_1 = {
|
||||||
feature: Quotas[0].feature,
|
feature: Quotas[3].feature,
|
||||||
version: Quotas[0].version,
|
version: Quotas[3].version,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Quota_ProPlanV1 = {
|
export const Quota_ProPlanV1 = {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ test('should be able to set quota', async t => {
|
|||||||
const q1 = await quota.getUserQuota(u1.id);
|
const q1 = await quota.getUserQuota(u1.id);
|
||||||
t.truthy(q1, 'should have quota');
|
t.truthy(q1, 'should have quota');
|
||||||
t.is(q1?.feature.name, QuotaType.FreePlanV1, 'should be free plan');
|
t.is(q1?.feature.name, QuotaType.FreePlanV1, 'should be free plan');
|
||||||
|
t.is(q1?.feature.version, 2, 'should be version 2');
|
||||||
|
|
||||||
await quota.switchUserQuota(u1.id, QuotaType.ProPlanV1);
|
await quota.switchUserQuota(u1.id, QuotaType.ProPlanV1);
|
||||||
|
|
||||||
@@ -63,8 +64,8 @@ test('should be able to check storage quota', async t => {
|
|||||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||||
|
|
||||||
const q1 = await storageQuota.getUserQuota(u1.id);
|
const q1 = await storageQuota.getUserQuota(u1.id);
|
||||||
t.is(q1?.blobLimit, Quotas[0].configs.blobLimit, 'should be free plan');
|
t.is(q1?.blobLimit, Quotas[3].configs.blobLimit, 'should be free plan');
|
||||||
t.is(q1?.storageQuota, Quotas[0].configs.storageQuota, 'should be free plan');
|
t.is(q1?.storageQuota, Quotas[3].configs.storageQuota, 'should be free plan');
|
||||||
|
|
||||||
await quota.switchUserQuota(u1.id, QuotaType.ProPlanV1);
|
await quota.switchUserQuota(u1.id, QuotaType.ProPlanV1);
|
||||||
const q2 = await storageQuota.getUserQuota(u1.id);
|
const q2 = await storageQuota.getUserQuota(u1.id);
|
||||||
@@ -77,8 +78,8 @@ test('should be able revert quota', async t => {
|
|||||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||||
|
|
||||||
const q1 = await storageQuota.getUserQuota(u1.id);
|
const q1 = await storageQuota.getUserQuota(u1.id);
|
||||||
t.is(q1?.blobLimit, Quotas[0].configs.blobLimit, 'should be free plan');
|
t.is(q1?.blobLimit, Quotas[3].configs.blobLimit, 'should be free plan');
|
||||||
t.is(q1?.storageQuota, Quotas[0].configs.storageQuota, 'should be free plan');
|
t.is(q1?.storageQuota, Quotas[3].configs.storageQuota, 'should be free plan');
|
||||||
|
|
||||||
await quota.switchUserQuota(u1.id, QuotaType.ProPlanV1);
|
await quota.switchUserQuota(u1.id, QuotaType.ProPlanV1);
|
||||||
const q2 = await storageQuota.getUserQuota(u1.id);
|
const q2 = await storageQuota.getUserQuota(u1.id);
|
||||||
@@ -87,7 +88,7 @@ test('should be able revert quota', async t => {
|
|||||||
|
|
||||||
await quota.switchUserQuota(u1.id, QuotaType.FreePlanV1);
|
await quota.switchUserQuota(u1.id, QuotaType.FreePlanV1);
|
||||||
const q3 = await storageQuota.getUserQuota(u1.id);
|
const q3 = await storageQuota.getUserQuota(u1.id);
|
||||||
t.is(q3?.blobLimit, Quotas[0].configs.blobLimit, 'should be free plan');
|
t.is(q3?.blobLimit, Quotas[3].configs.blobLimit, 'should be free plan');
|
||||||
|
|
||||||
const quotas = await quota.getUserQuotas(u1.id);
|
const quotas = await quota.getUserQuotas(u1.id);
|
||||||
t.is(quotas.length, 3, 'should have 3 quotas');
|
t.is(quotas.length, 3, 'should have 3 quotas');
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ test('should reject blob exceeded limit', async t => {
|
|||||||
const buffer2 = Buffer.from(Array.from({ length: OneMB + 1 }, () => 0));
|
const buffer2 = Buffer.from(Array.from({ length: OneMB + 1 }, () => 0));
|
||||||
await t.notThrowsAsync(setBlob(app, u1.token.token, workspace1.id, buffer2));
|
await t.notThrowsAsync(setBlob(app, u1.token.token, workspace1.id, buffer2));
|
||||||
|
|
||||||
const buffer3 = Buffer.from(Array.from({ length: 10 * OneMB + 1 }, () => 0));
|
const buffer3 = Buffer.from(Array.from({ length: 100 * OneMB + 1 }, () => 0));
|
||||||
await t.throwsAsync(setBlob(app, u1.token.token, workspace1.id, buffer3));
|
await t.throwsAsync(setBlob(app, u1.token.token, workspace1.id, buffer3));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user