mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 02:13:00 +08:00
feat(server): add Blob Model (#12894)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced comprehensive management for workspace blobs, including creating, updating, soft and permanent deletion, and retrieval of blobs. - Added the ability to list all blobs, list deleted blobs, and calculate the total size of blobs within a workspace. - **Tests** - Added extensive automated tests to validate blob creation, updating, deletion, retrieval, and aggregation functionalities. <!-- end of auto-generated comment: release notes by coderabbit.ai --> #### PR Dependency Tree * **PR #12894** 👈 * **PR #12897** This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
173
packages/backend/server/src/models/__tests__/blob.spec.ts
Normal file
173
packages/backend/server/src/models/__tests__/blob.spec.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import test from 'ava';
|
||||||
|
|
||||||
|
import { createModule } from '../../__tests__/create-module';
|
||||||
|
import { Mockers } from '../../__tests__/mocks';
|
||||||
|
import { Models } from '..';
|
||||||
|
|
||||||
|
const module = await createModule();
|
||||||
|
const models = module.get(Models);
|
||||||
|
|
||||||
|
test.after.always(async () => {
|
||||||
|
await module.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should upsert blob', async t => {
|
||||||
|
const workspace = await module.create(Mockers.Workspace);
|
||||||
|
|
||||||
|
// add
|
||||||
|
const blob = await models.blob.upsert({
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
key: 'test-key',
|
||||||
|
mime: 'text/plain',
|
||||||
|
size: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
t.is(blob.workspaceId, workspace.id);
|
||||||
|
t.is(blob.key, 'test-key');
|
||||||
|
t.is(blob.mime, 'text/plain');
|
||||||
|
t.is(blob.size, 100);
|
||||||
|
t.is(blob.deletedAt, null);
|
||||||
|
t.truthy(blob.createdAt);
|
||||||
|
|
||||||
|
// update
|
||||||
|
const blob2 = await models.blob.upsert({
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
key: 'test-key',
|
||||||
|
mime: 'text/html',
|
||||||
|
size: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
t.is(blob2.workspaceId, workspace.id);
|
||||||
|
t.is(blob2.key, 'test-key');
|
||||||
|
t.is(blob2.mime, 'text/html');
|
||||||
|
t.is(blob2.size, 200);
|
||||||
|
t.is(blob2.deletedAt, null);
|
||||||
|
|
||||||
|
// make sure only one blob is created
|
||||||
|
const blobs = await models.blob.list(workspace.id);
|
||||||
|
t.is(blobs.length, 1);
|
||||||
|
t.deepEqual(blobs[0], blob2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should delete blob', async t => {
|
||||||
|
const workspace = await module.create(Mockers.Workspace);
|
||||||
|
const blob = await models.blob.upsert({
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
key: 'test-key',
|
||||||
|
mime: 'text/plain',
|
||||||
|
size: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
await models.blob.delete(workspace.id, blob.key);
|
||||||
|
|
||||||
|
const blob2 = await models.blob.get(workspace.id, blob.key);
|
||||||
|
|
||||||
|
t.truthy(blob2);
|
||||||
|
t.truthy(blob2?.deletedAt);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should delete blob permanently', async t => {
|
||||||
|
const workspace = await module.create(Mockers.Workspace);
|
||||||
|
const blob = await models.blob.upsert({
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
key: 'test-key',
|
||||||
|
mime: 'text/plain',
|
||||||
|
size: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
await models.blob.delete(workspace.id, blob.key, true);
|
||||||
|
|
||||||
|
const blob2 = await models.blob.get(workspace.id, blob.key);
|
||||||
|
t.is(blob2, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should list blobs', async t => {
|
||||||
|
const workspace = await module.create(Mockers.Workspace);
|
||||||
|
const blob1 = await models.blob.upsert({
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
key: 'test-key',
|
||||||
|
mime: 'text/plain',
|
||||||
|
size: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob2 = await models.blob.upsert({
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
key: 'test-key2',
|
||||||
|
mime: 'text/plain',
|
||||||
|
size: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
const blobs = await models.blob.list(workspace.id);
|
||||||
|
|
||||||
|
t.is(blobs.length, 2);
|
||||||
|
blobs.sort((a, b) => a.key.localeCompare(b.key));
|
||||||
|
t.is(blobs[0].key, blob1.key);
|
||||||
|
t.is(blobs[1].key, blob2.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should list deleted blobs', async t => {
|
||||||
|
const workspace = await module.create(Mockers.Workspace);
|
||||||
|
const blob = await models.blob.upsert({
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
key: 'test-key',
|
||||||
|
mime: 'text/plain',
|
||||||
|
size: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
await models.blob.delete(workspace.id, blob.key);
|
||||||
|
|
||||||
|
const blobs = await models.blob.listDeleted(workspace.id);
|
||||||
|
|
||||||
|
t.is(blobs.length, 1);
|
||||||
|
t.is(blobs[0].key, blob.key);
|
||||||
|
t.truthy(blobs[0].deletedAt);
|
||||||
|
|
||||||
|
// delete permanently
|
||||||
|
await models.blob.delete(workspace.id, blob.key, true);
|
||||||
|
|
||||||
|
const blobs2 = await models.blob.listDeleted(workspace.id);
|
||||||
|
|
||||||
|
t.is(blobs2.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get blob', async t => {
|
||||||
|
const workspace = await module.create(Mockers.Workspace);
|
||||||
|
const blob = await models.blob.upsert({
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
key: 'test-key',
|
||||||
|
mime: 'text/plain',
|
||||||
|
size: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob2 = await models.blob.get(workspace.id, blob.key);
|
||||||
|
|
||||||
|
t.truthy(blob2);
|
||||||
|
t.is(blob2?.key, blob.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should total blob size in workspace', async t => {
|
||||||
|
const workspace = await module.create(Mockers.Workspace);
|
||||||
|
|
||||||
|
// default size is 0
|
||||||
|
const size1 = await models.blob.totalSize(workspace.id);
|
||||||
|
|
||||||
|
t.is(size1, 0);
|
||||||
|
|
||||||
|
await models.blob.upsert({
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
key: 'test-key',
|
||||||
|
mime: 'text/plain',
|
||||||
|
size: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
await models.blob.upsert({
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
key: 'test-key2',
|
||||||
|
mime: 'text/plain',
|
||||||
|
size: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
const size2 = await models.blob.totalSize(workspace.id);
|
||||||
|
|
||||||
|
t.is(size2, 300);
|
||||||
|
});
|
||||||
101
packages/backend/server/src/models/blob.ts
Normal file
101
packages/backend/server/src/models/blob.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
import { BaseModel } from './base';
|
||||||
|
|
||||||
|
export type CreateBlobInput = Prisma.BlobUncheckedCreateInput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blob Model
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class BlobModel extends BaseModel {
|
||||||
|
async upsert(blob: CreateBlobInput) {
|
||||||
|
return await this.db.blob.upsert({
|
||||||
|
where: {
|
||||||
|
workspaceId_key: {
|
||||||
|
workspaceId: blob.workspaceId,
|
||||||
|
key: blob.key,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
mime: blob.mime,
|
||||||
|
size: blob.size,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
workspaceId: blob.workspaceId,
|
||||||
|
key: blob.key,
|
||||||
|
mime: blob.mime,
|
||||||
|
size: blob.size,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(workspaceId: string, key: string, permanently = false) {
|
||||||
|
if (permanently) {
|
||||||
|
await this.db.blob.deleteMany({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
key,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logger.log(`deleted blob ${workspaceId}/${key} permanently`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.db.blob.update({
|
||||||
|
where: {
|
||||||
|
workspaceId_key: {
|
||||||
|
workspaceId,
|
||||||
|
key,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
deletedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(workspaceId: string, key: string) {
|
||||||
|
return await this.db.blob.findUnique({
|
||||||
|
where: {
|
||||||
|
workspaceId_key: {
|
||||||
|
workspaceId,
|
||||||
|
key,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(workspaceId: string) {
|
||||||
|
return await this.db.blob.findMany({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async listDeleted(workspaceId: string) {
|
||||||
|
return await this.db.blob.findMany({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
deletedAt: { not: null },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async totalSize(workspaceId: string) {
|
||||||
|
const sum = await this.db.blob.aggregate({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
_sum: {
|
||||||
|
size: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return sum._sum.size ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
|
|
||||||
import { ApplyType } from '../base';
|
import { ApplyType } from '../base';
|
||||||
|
import { BlobModel } from './blob';
|
||||||
import { CommentModel } from './comment';
|
import { CommentModel } from './comment';
|
||||||
import { CommentAttachmentModel } from './comment-attachment';
|
import { CommentAttachmentModel } from './comment-attachment';
|
||||||
import { AppConfigModel } from './config';
|
import { AppConfigModel } from './config';
|
||||||
@@ -52,6 +53,7 @@ const MODELS = {
|
|||||||
appConfig: AppConfigModel,
|
appConfig: AppConfigModel,
|
||||||
comment: CommentModel,
|
comment: CommentModel,
|
||||||
commentAttachment: CommentAttachmentModel,
|
commentAttachment: CommentAttachmentModel,
|
||||||
|
blob: BlobModel,
|
||||||
};
|
};
|
||||||
|
|
||||||
type ModelsType = {
|
type ModelsType = {
|
||||||
@@ -103,6 +105,7 @@ const ModelsSymbolProvider: ExistingProvider = {
|
|||||||
})
|
})
|
||||||
export class ModelsModule {}
|
export class ModelsModule {}
|
||||||
|
|
||||||
|
export * from './blob';
|
||||||
export * from './comment';
|
export * from './comment';
|
||||||
export * from './comment-attachment';
|
export * from './comment-attachment';
|
||||||
export * from './common';
|
export * from './common';
|
||||||
|
|||||||
Reference in New Issue
Block a user