mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat: add business blob limit (#5734)
This commit is contained in:
@@ -57,6 +57,12 @@ export class QuotaConfig {
|
||||
return this.config.configs.blobLimit;
|
||||
}
|
||||
|
||||
get businessBlobLimit() {
|
||||
return (
|
||||
this.config.configs.businessBlobLimit || this.config.configs.blobLimit
|
||||
);
|
||||
}
|
||||
|
||||
get storageQuota() {
|
||||
return this.config.configs.storageQuota;
|
||||
}
|
||||
|
||||
@@ -71,11 +71,33 @@ export const Quotas: Quota[] = [
|
||||
memberLimit: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
feature: QuotaType.FreePlanV1,
|
||||
type: FeatureKind.Quota,
|
||||
version: 3,
|
||||
configs: {
|
||||
// quota name
|
||||
name: 'Free',
|
||||
// single blob limit 10MB
|
||||
blobLimit: 10 * OneMB,
|
||||
// server limit will larger then client to handle a edge case:
|
||||
// when a user downgrades from pro to free, he can still continue
|
||||
// to upload previously added files that exceed the free limit
|
||||
// NOTE: this is a product decision, may change in future
|
||||
businessBlobLimit: 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_1 = {
|
||||
feature: Quotas[3].feature,
|
||||
version: Quotas[3].version,
|
||||
feature: Quotas[4].feature,
|
||||
version: Quotas[4].version,
|
||||
};
|
||||
|
||||
export const Quota_ProPlanV1 = {
|
||||
|
||||
@@ -7,6 +7,8 @@ import { OneGB } from './constant';
|
||||
import { QuotaService } from './service';
|
||||
import { formatSize, QuotaQueryType } from './types';
|
||||
|
||||
type QuotaBusinessType = QuotaQueryType & { businessBlobLimit: number };
|
||||
|
||||
@Injectable()
|
||||
export class QuotaManagementService {
|
||||
constructor(
|
||||
@@ -25,6 +27,7 @@ export class QuotaManagementService {
|
||||
createAt: quota.createdAt,
|
||||
expiredAt: quota.expiredAt,
|
||||
blobLimit: quota.feature.blobLimit,
|
||||
businessBlobLimit: quota.feature.businessBlobLimit,
|
||||
storageQuota: quota.feature.storageQuota,
|
||||
historyPeriod: quota.feature.historyPeriod,
|
||||
memberLimit: quota.feature.memberLimit,
|
||||
@@ -44,7 +47,7 @@ export class QuotaManagementService {
|
||||
|
||||
// get workspace's owner quota and total size of used
|
||||
// quota was apply to owner's account
|
||||
async getWorkspaceUsage(workspaceId: string): Promise<QuotaQueryType> {
|
||||
async getWorkspaceUsage(workspaceId: string): Promise<QuotaBusinessType> {
|
||||
const { user: owner } =
|
||||
await this.permissions.getWorkspaceOwner(workspaceId);
|
||||
if (!owner) throw new NotFoundException('Workspace owner not found');
|
||||
@@ -52,6 +55,7 @@ export class QuotaManagementService {
|
||||
feature: {
|
||||
name,
|
||||
blobLimit,
|
||||
businessBlobLimit,
|
||||
historyPeriod,
|
||||
memberLimit,
|
||||
storageQuota,
|
||||
@@ -64,6 +68,7 @@ export class QuotaManagementService {
|
||||
const quota = {
|
||||
name,
|
||||
blobLimit,
|
||||
businessBlobLimit,
|
||||
historyPeriod,
|
||||
memberLimit,
|
||||
storageQuota,
|
||||
@@ -84,7 +89,7 @@ export class QuotaManagementService {
|
||||
return quota;
|
||||
}
|
||||
|
||||
private mergeUnlimitedQuota(orig: QuotaQueryType) {
|
||||
private mergeUnlimitedQuota(orig: QuotaBusinessType) {
|
||||
return {
|
||||
...orig,
|
||||
storageQuota: 1000 * OneGB,
|
||||
|
||||
@@ -33,6 +33,7 @@ const quotaPlan = z.object({
|
||||
storageQuota: z.number().positive().int(),
|
||||
historyPeriod: z.number().positive().int(),
|
||||
memberLimit: z.number().positive().int(),
|
||||
businessBlobLimit: z.number().positive().int().nullish(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ export class WorkspaceBlobResolver {
|
||||
Permission.Write
|
||||
);
|
||||
|
||||
const { storageQuota, usedSize, blobLimit } =
|
||||
const { storageQuota, usedSize, businessBlobLimit } =
|
||||
await this.quota.getWorkspaceUsage(workspaceId);
|
||||
|
||||
const unlimited = await this.feature.hasWorkspaceFeature(
|
||||
@@ -152,8 +152,10 @@ export class WorkspaceBlobResolver {
|
||||
`storage size limit exceeded: ${total} > ${storageQuota}`
|
||||
);
|
||||
return true;
|
||||
} else if (recvSize > blobLimit) {
|
||||
this.logger.log(`blob size limit exceeded: ${recvSize} > ${blobLimit}`);
|
||||
} else if (recvSize > businessBlobLimit) {
|
||||
this.logger.log(
|
||||
`blob size limit exceeded: ${recvSize} > ${businessBlobLimit}`
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
||||
@@ -1,65 +1,14 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { FeatureKind } from '../../core/features';
|
||||
import { Quotas } from '../../core/quota';
|
||||
import { upsertFeature } from './utils/user-features';
|
||||
import { upgradeQuotaVersion } from './utils/user-quotas';
|
||||
|
||||
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,
|
||||
})),
|
||||
});
|
||||
});
|
||||
// free plan 1.0
|
||||
const quota = Quotas[3];
|
||||
await upgradeQuotaVersion(db, quota, 'free plan 1.0 migration');
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { Quotas } from '../../core/quota';
|
||||
import { upgradeQuotaVersion } from './utils/user-quotas';
|
||||
|
||||
export class BusinessBlobLimit1706513866287 {
|
||||
// do the migration
|
||||
static async up(db: PrismaClient) {
|
||||
// free plan 1.1
|
||||
const quota = Quotas[4];
|
||||
await upgradeQuotaVersion(db, quota, 'free plan 1.1 migration');
|
||||
}
|
||||
|
||||
// revert the migration
|
||||
static async down(_db: PrismaClient) {}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { FeatureKind } from '../../../core/features';
|
||||
import { Quota } from '../../../core/quota/types';
|
||||
import { upsertFeature } from './user-features';
|
||||
|
||||
export async function upgradeQuotaVersion(
|
||||
db: PrismaClient,
|
||||
quota: Quota,
|
||||
reason: string
|
||||
) {
|
||||
// add new quota
|
||||
await upsertFeature(db, quota);
|
||||
// migrate all users that using old quota to new quota
|
||||
await db.$transaction(async tx => {
|
||||
const latestQuotaVersion = await tx.features.findFirstOrThrow({
|
||||
where: { feature: quota.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: quota.feature,
|
||||
version: { lt: quota.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: latestQuotaVersion.id,
|
||||
reason,
|
||||
activated: true,
|
||||
})),
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -199,9 +199,9 @@ type Query {
|
||||
|
||||
type QuotaQueryType {
|
||||
blobLimit: SafeInt!
|
||||
historyPeriod: Int!
|
||||
historyPeriod: SafeInt!
|
||||
humanReadable: HumanReadableQuotaType!
|
||||
memberLimit: Int!
|
||||
memberLimit: SafeInt!
|
||||
name: String!
|
||||
storageQuota: SafeInt!
|
||||
usedSize: SafeInt!
|
||||
|
||||
@@ -48,7 +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');
|
||||
t.is(q1?.feature.version, 3, 'should be version 2');
|
||||
|
||||
await quota.switchUserQuota(u1.id, QuotaType.ProPlanV1);
|
||||
|
||||
@@ -64,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[3].configs.blobLimit, 'should be free plan');
|
||||
t.is(q1?.storageQuota, Quotas[3].configs.storageQuota, 'should be free plan');
|
||||
t.is(q1?.blobLimit, Quotas[4].configs.blobLimit, 'should be free plan');
|
||||
t.is(q1?.storageQuota, Quotas[4].configs.storageQuota, 'should be free plan');
|
||||
|
||||
await quota.switchUserQuota(u1.id, QuotaType.ProPlanV1);
|
||||
const q2 = await storageQuota.getUserQuota(u1.id);
|
||||
@@ -78,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[3].configs.blobLimit, 'should be free plan');
|
||||
t.is(q1?.storageQuota, Quotas[3].configs.storageQuota, 'should be free plan');
|
||||
t.is(q1?.blobLimit, Quotas[4].configs.blobLimit, 'should be free plan');
|
||||
t.is(q1?.storageQuota, Quotas[4].configs.storageQuota, 'should be free plan');
|
||||
|
||||
await quota.switchUserQuota(u1.id, QuotaType.ProPlanV1);
|
||||
const q2 = await storageQuota.getUserQuota(u1.id);
|
||||
@@ -88,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[3].configs.blobLimit, 'should be free plan');
|
||||
t.is(q3?.blobLimit, Quotas[4].configs.blobLimit, 'should be free plan');
|
||||
|
||||
const quotas = await quota.getUserQuotas(u1.id);
|
||||
t.is(quotas.length, 3, 'should have 3 quotas');
|
||||
|
||||
Reference in New Issue
Block a user