feat: new free plan (#5604)

This commit is contained in:
DarkSky
2024-01-17 07:20:18 +00:00
parent 3f87d04481
commit 8f80bdb7af
8 changed files with 104 additions and 15 deletions

View File

@@ -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) {}
}

View File

@@ -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,
}, },
}, },
}, },

View File

@@ -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,
}, },
}, },
}, },

View File

@@ -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';

View File

@@ -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;

View File

@@ -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 = {

View File

@@ -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');

View File

@@ -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));
}); });