feat: add query quota of workspace (#5603)

This commit is contained in:
DarkSky
2024-01-16 09:45:55 +00:00
parent 4f4d057aad
commit ee2520ec18
9 changed files with 98 additions and 14 deletions

View File

@@ -21,4 +21,4 @@ export class QuotaModule {}
export { QuotaManagementService, QuotaService }; export { QuotaManagementService, QuotaService };
export { Quota_FreePlanV1, Quota_ProPlanV1, Quotas } from './schema'; export { Quota_FreePlanV1, Quota_ProPlanV1, Quotas } from './schema';
export { QuotaType } from './types'; export { QuotaQueryType, QuotaType } from './types';

View File

@@ -3,6 +3,7 @@ import { Injectable, NotFoundException } from '@nestjs/common';
import { WorkspaceBlobStorage } from '../storage'; import { WorkspaceBlobStorage } from '../storage';
import { PermissionService } from '../workspaces/permission'; import { PermissionService } from '../workspaces/permission';
import { QuotaService } from './service'; import { QuotaService } from './service';
import { QuotaQueryType } from './types';
@Injectable() @Injectable()
export class QuotaManagementService { export class QuotaManagementService {
@@ -40,21 +41,21 @@ export class QuotaManagementService {
// get workspace's owner quota and total size of used // get workspace's owner quota and total size of used
// quota was apply to owner's account // quota was apply to owner's account
async getWorkspaceUsage(workspaceId: string) { async getWorkspaceUsage(workspaceId: string): Promise<QuotaQueryType> {
const { user: owner } = const { user: owner } =
await this.permissions.getWorkspaceOwner(workspaceId); await this.permissions.getWorkspaceOwner(workspaceId);
if (!owner) throw new NotFoundException('Workspace owner not found'); if (!owner) throw new NotFoundException('Workspace owner not found');
const { storageQuota, blobLimit } = await this.getUserQuota(owner.id); const { storageQuota, blobLimit } = await this.getUserQuota(owner.id);
// get all workspaces size of owner used // get all workspaces size of owner used
const usageSize = await this.getUserUsage(owner.id); const usedSize = await this.getUserUsage(owner.id);
return { quota: storageQuota, size: usageSize, limit: blobLimit }; return { storageQuota, usedSize, blobLimit };
} }
async checkBlobQuota(workspaceId: string, size: number) { async checkBlobQuota(workspaceId: string, size: number) {
const { quota, size: usageSize } = const { storageQuota, usedSize } =
await this.getWorkspaceUsage(workspaceId); await this.getWorkspaceUsage(workspaceId);
return quota - (size + usageSize); return storageQuota - (size + usedSize);
} }
} }

View File

@@ -1,3 +1,4 @@
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { z } from 'zod'; import { z } from 'zod';
import { commonFeatureSchema, FeatureKind } from '../features'; import { commonFeatureSchema, FeatureKind } from '../features';
@@ -37,6 +38,20 @@ export const QuotaSchema = commonFeatureSchema
export type Quota = z.infer<typeof QuotaSchema>; export type Quota = z.infer<typeof QuotaSchema>;
/// ======== query types ========
@ObjectType()
export class QuotaQueryType {
@Field(() => Int)
storageQuota!: number;
@Field(() => Int)
usedSize!: number;
@Field(() => Int)
blobLimit!: number;
}
/// ======== utils ======== /// ======== utils ========
export function formatSize(bytes: number, decimals: number = 2): string { export function formatSize(bytes: number, decimals: number = 2): string {

View File

@@ -128,7 +128,7 @@ export class WorkspaceBlobResolver {
Permission.Write Permission.Write
); );
const { quota, size, limit } = const { storageQuota, usedSize, blobLimit } =
await this.quota.getWorkspaceUsage(workspaceId); await this.quota.getWorkspaceUsage(workspaceId);
const unlimited = await this.feature.hasWorkspaceFeature( const unlimited = await this.feature.hasWorkspaceFeature(
@@ -137,7 +137,7 @@ export class WorkspaceBlobResolver {
); );
const checkExceeded = (recvSize: number) => { const checkExceeded = (recvSize: number) => {
if (!quota) { if (!storageQuota) {
throw new GraphQLError('cannot find user quota', { throw new GraphQLError('cannot find user quota', {
extensions: { extensions: {
status: HttpStatus[HttpStatus.FORBIDDEN], status: HttpStatus[HttpStatus.FORBIDDEN],
@@ -145,13 +145,15 @@ export class WorkspaceBlobResolver {
}, },
}); });
} }
const total = size + recvSize; const total = usedSize + recvSize;
// only skip total storage check if workspace has unlimited feature // only skip total storage check if workspace has unlimited feature
if (total > quota && !unlimited) { if (total > storageQuota && !unlimited) {
this.logger.log(`storage size limit exceeded: ${total} > ${quota}`); this.logger.log(
`storage size limit exceeded: ${total} > ${storageQuota}`
);
return true; return true;
} else if (recvSize > limit) { } else if (recvSize > blobLimit) {
this.logger.log(`blob size limit exceeded: ${recvSize} > ${limit}`); this.logger.log(`blob size limit exceeded: ${recvSize} > ${blobLimit}`);
return true; return true;
} else { } else {
return false; return false;

View File

@@ -31,7 +31,7 @@ import {
import { Auth, CurrentUser, Public } from '../../auth'; import { Auth, CurrentUser, Public } from '../../auth';
import { AuthService } from '../../auth/service'; import { AuthService } from '../../auth/service';
import { FeatureManagementService, FeatureType } from '../../features'; import { FeatureManagementService, FeatureType } from '../../features';
import { QuotaManagementService } from '../../quota'; import { QuotaManagementService, QuotaQueryType } from '../../quota';
import { WorkspaceBlobStorage } from '../../storage'; import { WorkspaceBlobStorage } from '../../storage';
import { UsersService, UserType } from '../../users'; import { UsersService, UserType } from '../../users';
import { PermissionService } from '../permission'; import { PermissionService } from '../permission';
@@ -149,6 +149,15 @@ export class WorkspaceResolver {
})); }));
} }
@ResolveField(() => QuotaQueryType, {
name: 'quota',
description: 'quota of workspace',
complexity: 2,
})
workspaceQuota(@Parent() workspace: WorkspaceType) {
return this.quota.getWorkspaceUsage(workspace.id);
}
@Query(() => Boolean, { @Query(() => Boolean, {
description: 'Get is owner of workspace', description: 'Get is owner of workspace',
complexity: 2, complexity: 2,

View File

@@ -131,6 +131,9 @@ type WorkspaceType {
"""Owner of workspace""" """Owner of workspace"""
owner: UserType! owner: UserType!
"""quota of workspace"""
quota: QuotaQueryType!
"""Available features of workspace""" """Available features of workspace"""
availableFeatures: [FeatureType!]! availableFeatures: [FeatureType!]!
@@ -183,6 +186,12 @@ type InvitationType {
invitee: UserType! invitee: UserType!
} }
type QuotaQueryType {
storageQuota: Int!
usedSize: Int!
blobLimit: Int!
}
type TokenType { type TokenType {
token: String! token: String!
refresh: String! refresh: String!

View File

@@ -880,3 +880,20 @@ mutation acceptInviteByInviteId($workspaceId: String!, $inviteId: String!, $send
) )
}`, }`,
}; };
export const workspaceQuotaQuery = {
id: 'workspaceQuotaQuery' as const,
operationName: 'workspaceQuota',
definitionName: 'workspace',
containsFile: false,
query: `
query workspaceQuota($id: String!) {
workspace(id: $id) {
quota {
storageQuota
usedSize
blobLimit
}
}
}`,
};

View File

@@ -0,0 +1,9 @@
query workspaceQuota($id: String!) {
workspace(id: $id) {
quota {
storageQuota
usedSize
blobLimit
}
}
}

View File

@@ -827,6 +827,23 @@ export type AcceptInviteByInviteIdMutation = {
acceptInviteById: boolean; acceptInviteById: boolean;
}; };
export type WorkspaceQuotaQueryVariables = Exact<{
id: Scalars['String']['input'];
}>;
export type WorkspaceQuotaQuery = {
__typename?: 'Query';
workspace: {
__typename?: 'WorkspaceType';
quota: {
__typename?: 'QuotaQueryType';
storageQuota: number;
usedSize: number;
blobLimit: number;
};
};
};
export type Queries = export type Queries =
| { | {
name: 'checkBlobSizesQuery'; name: 'checkBlobSizesQuery';
@@ -957,6 +974,11 @@ export type Queries =
name: 'listWorkspaceFeaturesQuery'; name: 'listWorkspaceFeaturesQuery';
variables: ListWorkspaceFeaturesQueryVariables; variables: ListWorkspaceFeaturesQueryVariables;
response: ListWorkspaceFeaturesQuery; response: ListWorkspaceFeaturesQuery;
}
| {
name: 'workspaceQuotaQuery';
variables: WorkspaceQuotaQueryVariables;
response: WorkspaceQuotaQuery;
}; };
export type Mutations = export type Mutations =