diff --git a/packages/backend/server/src/data/migrations/1705395933447-new-free-plan.ts b/packages/backend/server/src/data/migrations/1705395933447-new-free-plan.ts new file mode 100644 index 0000000000..744991c7d1 --- /dev/null +++ b/packages/backend/server/src/data/migrations/1705395933447-new-free-plan.ts @@ -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) {} +} diff --git a/packages/backend/server/src/modules/auth/next-auth-options.ts b/packages/backend/server/src/modules/auth/next-auth-options.ts index 261ad7d8ec..db711c06d0 100644 --- a/packages/backend/server/src/modules/auth/next-auth-options.ts +++ b/packages/backend/server/src/modules/auth/next-auth-options.ts @@ -15,7 +15,7 @@ import { SessionService, } from '../../fundamentals'; import { FeatureType } from '../features'; -import { Quota_FreePlanV1 } from '../quota'; +import { Quota_FreePlanV1_1 } from '../quota'; import { decode, encode, @@ -52,7 +52,7 @@ export const NextAuthOptionsProvider: FactoryProvider = { activated: true, feature: { connect: { - feature_version: Quota_FreePlanV1, + feature_version: Quota_FreePlanV1_1, }, }, }, diff --git a/packages/backend/server/src/modules/auth/service.ts b/packages/backend/server/src/modules/auth/service.ts index 306ff4e090..754c5b70aa 100644 --- a/packages/backend/server/src/modules/auth/service.ts +++ b/packages/backend/server/src/modules/auth/service.ts @@ -17,7 +17,7 @@ import { PrismaService, verifyChallengeResponse, } from '../../fundamentals'; -import { Quota_FreePlanV1 } from '../quota'; +import { Quota_FreePlanV1_1 } from '../quota'; export type UserClaim = Pick< User, @@ -201,7 +201,7 @@ export class AuthService { activated: true, feature: { connect: { - feature_version: Quota_FreePlanV1, + feature_version: Quota_FreePlanV1_1, }, }, }, @@ -231,7 +231,7 @@ export class AuthService { activated: true, feature: { connect: { - feature_version: Quota_FreePlanV1, + feature_version: Quota_FreePlanV1_1, }, }, }, diff --git a/packages/backend/server/src/modules/quota/index.ts b/packages/backend/server/src/modules/quota/index.ts index ce9b3addbc..c49462a968 100644 --- a/packages/backend/server/src/modules/quota/index.ts +++ b/packages/backend/server/src/modules/quota/index.ts @@ -20,5 +20,5 @@ import { QuotaManagementService } from './storage'; export class QuotaModule {} 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'; diff --git a/packages/backend/server/src/modules/quota/quota.ts b/packages/backend/server/src/modules/quota/quota.ts index 990eb30479..5139dd02c8 100644 --- a/packages/backend/server/src/modules/quota/quota.ts +++ b/packages/backend/server/src/modules/quota/quota.ts @@ -44,6 +44,10 @@ export class QuotaConfig { } } + get version() { + return this.config.version; + } + /// feature name of quota get name() { return this.config.feature; diff --git a/packages/backend/server/src/modules/quota/schema.ts b/packages/backend/server/src/modules/quota/schema.ts index 10e4dbad79..a038ff26f3 100644 --- a/packages/backend/server/src/modules/quota/schema.ts +++ b/packages/backend/server/src/modules/quota/schema.ts @@ -54,11 +54,28 @@ export const Quotas: Quota[] = [ 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 = { - feature: Quotas[0].feature, - version: Quotas[0].version, +export const Quota_FreePlanV1_1 = { + feature: Quotas[3].feature, + version: Quotas[3].version, }; export const Quota_ProPlanV1 = { diff --git a/packages/backend/server/tests/quota.spec.ts b/packages/backend/server/tests/quota.spec.ts index 99d7fe5d29..d1d23a7313 100644 --- a/packages/backend/server/tests/quota.spec.ts +++ b/packages/backend/server/tests/quota.spec.ts @@ -48,6 +48,7 @@ test('should be able to set quota', async t => { const q1 = await quota.getUserQuota(u1.id); t.truthy(q1, 'should have quota'); 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); @@ -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 q1 = await storageQuota.getUserQuota(u1.id); - t.is(q1?.blobLimit, Quotas[0].configs.blobLimit, 'should be free plan'); - t.is(q1?.storageQuota, Quotas[0].configs.storageQuota, 'should be free plan'); + t.is(q1?.blobLimit, Quotas[3].configs.blobLimit, 'should be free plan'); + t.is(q1?.storageQuota, Quotas[3].configs.storageQuota, 'should be free plan'); await quota.switchUserQuota(u1.id, QuotaType.ProPlanV1); 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 q1 = await storageQuota.getUserQuota(u1.id); - t.is(q1?.blobLimit, Quotas[0].configs.blobLimit, 'should be free plan'); - t.is(q1?.storageQuota, Quotas[0].configs.storageQuota, 'should be free plan'); + t.is(q1?.blobLimit, Quotas[3].configs.blobLimit, 'should be free plan'); + t.is(q1?.storageQuota, Quotas[3].configs.storageQuota, 'should be free plan'); await quota.switchUserQuota(u1.id, QuotaType.ProPlanV1); 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); 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); t.is(quotas.length, 3, 'should have 3 quotas'); diff --git a/packages/backend/server/tests/workspace-blobs.spec.ts b/packages/backend/server/tests/workspace-blobs.spec.ts index af0a341f11..5d5b3dc93b 100644 --- a/packages/backend/server/tests/workspace-blobs.spec.ts +++ b/packages/backend/server/tests/workspace-blobs.spec.ts @@ -182,7 +182,7 @@ test('should reject blob exceeded limit', async t => { const buffer2 = Buffer.from(Array.from({ length: OneMB + 1 }, () => 0)); 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)); });