From 42a3149fe3736597bbc96de1ad4ea93e0f34f8b6 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Sat, 28 Jan 2023 00:57:37 -0500 Subject: [PATCH] refactor(server): move asset upload job to domain (#1434) * refactor: move to domain * refactor: rename method * Update comments --------- Co-authored-by: Alex Tran --- .../processors/asset-uploaded.processor.ts | 47 ++-------------- server/libs/domain/src/domain.module.ts | 4 +- server/libs/domain/src/job/index.ts | 1 + server/libs/domain/src/job/job.repository.ts | 4 ++ .../libs/domain/src/job/job.service.spec.ts | 54 +++++++++++++++++++ server/libs/domain/src/job/job.service.ts | 17 ++++++ server/libs/domain/src/job/job.upload.core.ts | 32 +++++++++++ 7 files changed, 116 insertions(+), 43 deletions(-) create mode 100644 server/libs/domain/src/job/job.service.spec.ts create mode 100644 server/libs/domain/src/job/job.service.ts create mode 100644 server/libs/domain/src/job/job.upload.core.ts diff --git a/server/apps/microservices/src/processors/asset-uploaded.processor.ts b/server/apps/microservices/src/processors/asset-uploaded.processor.ts index ea0005ee3c..4c402ee4f4 100644 --- a/server/apps/microservices/src/processors/asset-uploaded.processor.ts +++ b/server/apps/microservices/src/processors/asset-uploaded.processor.ts @@ -1,50 +1,13 @@ -import { AssetType } from '@app/infra'; -import { - IAssetUploadedJob, - IMetadataExtractionJob, - IThumbnailGenerationJob, - IVideoTranscodeJob, - QueueName, - JobName, -} from '@app/domain'; -import { InjectQueue, Process, Processor } from '@nestjs/bull'; -import { Job, Queue } from 'bull'; +import { IAssetUploadedJob, JobName, JobService, QueueName } from '@app/domain'; +import { Process, Processor } from '@nestjs/bull'; +import { Job } from 'bull'; @Processor(QueueName.ASSET_UPLOADED) export class AssetUploadedProcessor { - constructor( - @InjectQueue(QueueName.THUMBNAIL_GENERATION) - private thumbnailGeneratorQueue: Queue, + constructor(private jobService: JobService) {} - @InjectQueue(QueueName.METADATA_EXTRACTION) - private metadataExtractionQueue: Queue, - - @InjectQueue(QueueName.VIDEO_CONVERSION) - private videoConversionQueue: Queue, - ) {} - - /** - * Post processing uploaded asset to perform the following function if missing - * 1. Generate JPEG Thumbnail - * 2. Generate Webp Thumbnail - * 3. EXIF extractor - * 4. Reverse Geocoding - * - * @param job asset-uploaded - */ @Process(JobName.ASSET_UPLOADED) async processUploadedVideo(job: Job) { - const { asset, fileName } = job.data; - - await this.thumbnailGeneratorQueue.add(JobName.GENERATE_JPEG_THUMBNAIL, { asset }); - - // Video Conversion - if (asset.type == AssetType.VIDEO) { - await this.videoConversionQueue.add(JobName.VIDEO_CONVERSION, { asset }); - await this.metadataExtractionQueue.add(JobName.EXTRACT_VIDEO_METADATA, { asset, fileName }); - } else { - // Extract Metadata/Exif for Images - Currently the EXIF library on the web cannot extract EXIF for video yet - await this.metadataExtractionQueue.add(JobName.EXIF_EXTRACTION, { asset, fileName }); - } + await this.jobService.handleUploadedAsset(job); } } diff --git a/server/libs/domain/src/domain.module.ts b/server/libs/domain/src/domain.module.ts index 23b4adf190..1961b95a8d 100644 --- a/server/libs/domain/src/domain.module.ts +++ b/server/libs/domain/src/domain.module.ts @@ -1,14 +1,16 @@ import { DynamicModule, Global, Module, ModuleMetadata, Provider } from '@nestjs/common'; import { APIKeyService } from './api-key'; -import { ShareService } from './share'; import { AuthService } from './auth'; +import { JobService } from './job'; import { OAuthService } from './oauth'; +import { ShareService } from './share'; import { INITIAL_SYSTEM_CONFIG, SystemConfigService } from './system-config'; import { UserService } from './user'; const providers: Provider[] = [ APIKeyService, AuthService, + JobService, OAuthService, SystemConfigService, UserService, diff --git a/server/libs/domain/src/job/index.ts b/server/libs/domain/src/job/index.ts index f790bf20ef..205aa3d20d 100644 --- a/server/libs/domain/src/job/index.ts +++ b/server/libs/domain/src/job/index.ts @@ -1,3 +1,4 @@ export * from './interfaces'; export * from './job.constants'; export * from './job.repository'; +export * from './job.service'; diff --git a/server/libs/domain/src/job/job.repository.ts b/server/libs/domain/src/job/job.repository.ts index 3e5bc7c18a..74cd7517d8 100644 --- a/server/libs/domain/src/job/job.repository.ts +++ b/server/libs/domain/src/job/job.repository.ts @@ -20,6 +20,10 @@ export interface JobCounts { waiting: number; } +export interface Job { + data: T; +} + export type JobItem = | { name: JobName.ASSET_UPLOADED; data: IAssetUploadedJob } | { name: JobName.VIDEO_CONVERSION; data: IVideoConversionProcessor } diff --git a/server/libs/domain/src/job/job.service.spec.ts b/server/libs/domain/src/job/job.service.spec.ts new file mode 100644 index 0000000000..8b7b47ef80 --- /dev/null +++ b/server/libs/domain/src/job/job.service.spec.ts @@ -0,0 +1,54 @@ +import { AssetEntity, AssetType } from '@app/infra/db/entities'; +import { newJobRepositoryMock } from '../../test'; +import { IAssetUploadedJob } from './interfaces'; +import { JobName } from './job.constants'; +import { IJobRepository, Job } from './job.repository'; +import { JobService } from './job.service'; + +const jobStub = { + upload: { + video: Object.freeze>({ + data: { asset: { type: AssetType.VIDEO } as AssetEntity, fileName: 'video.mp4' }, + }), + image: Object.freeze>({ + data: { asset: { type: AssetType.IMAGE } as AssetEntity, fileName: 'image.jpg' }, + }), + }, +}; + +describe(JobService.name, () => { + let sut: JobService; + let jobMock: jest.Mocked; + + it('should work', () => { + expect(sut).toBeDefined(); + }); + + beforeEach(async () => { + jobMock = newJobRepositoryMock(); + sut = new JobService(jobMock); + }); + + describe('handleUploadedAsset', () => { + it('should process a video', async () => { + await expect(sut.handleUploadedAsset(jobStub.upload.video)).resolves.toBeUndefined(); + + expect(jobMock.add).toHaveBeenCalledTimes(3); + expect(jobMock.add.mock.calls).toEqual([ + [{ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { asset: { type: AssetType.VIDEO } } }], + [{ name: JobName.VIDEO_CONVERSION, data: { asset: { type: AssetType.VIDEO } } }], + [{ name: JobName.EXTRACT_VIDEO_METADATA, data: { asset: { type: AssetType.VIDEO }, fileName: 'video.mp4' } }], + ]); + }); + + it('should process an image', async () => { + await sut.handleUploadedAsset(jobStub.upload.image); + + expect(jobMock.add).toHaveBeenCalledTimes(2); + expect(jobMock.add.mock.calls).toEqual([ + [{ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { asset: { type: AssetType.IMAGE } } }], + [{ name: JobName.EXIF_EXTRACTION, data: { asset: { type: AssetType.IMAGE }, fileName: 'image.jpg' } }], + ]); + }); + }); +}); diff --git a/server/libs/domain/src/job/job.service.ts b/server/libs/domain/src/job/job.service.ts new file mode 100644 index 0000000000..be142090b5 --- /dev/null +++ b/server/libs/domain/src/job/job.service.ts @@ -0,0 +1,17 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { IAssetUploadedJob } from './interfaces'; +import { JobUploadCore } from './job.upload.core'; +import { IJobRepository, Job } from './job.repository'; + +@Injectable() +export class JobService { + private uploadCore: JobUploadCore; + + constructor(@Inject(IJobRepository) repository: IJobRepository) { + this.uploadCore = new JobUploadCore(repository); + } + + async handleUploadedAsset(job: Job) { + await this.uploadCore.handleAsset(job); + } +} diff --git a/server/libs/domain/src/job/job.upload.core.ts b/server/libs/domain/src/job/job.upload.core.ts new file mode 100644 index 0000000000..79efe93b18 --- /dev/null +++ b/server/libs/domain/src/job/job.upload.core.ts @@ -0,0 +1,32 @@ +import { AssetType } from '@app/infra/db/entities'; +import { IAssetUploadedJob } from './interfaces'; +import { JobName } from './job.constants'; +import { IJobRepository, Job } from './job.repository'; + +export class JobUploadCore { + constructor(private repository: IJobRepository) {} + + /** + * Post processing uploaded asset to perform the following function + * 1. Generate JPEG Thumbnail + * 2. Generate Webp Thumbnail + * 3. EXIF extractor + * 4. Reverse Geocoding + * + * @param job asset-uploaded + */ + async handleAsset(job: Job) { + const { asset, fileName } = job.data; + + await this.repository.add({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { asset } }); + + // Video Conversion + if (asset.type == AssetType.VIDEO) { + await this.repository.add({ name: JobName.VIDEO_CONVERSION, data: { asset } }); + await this.repository.add({ name: JobName.EXTRACT_VIDEO_METADATA, data: { asset, fileName } }); + } else { + // Extract Metadata/Exif for Images - Currently the EXIF library on the web cannot extract EXIF for video yet + await this.repository.add({ name: JobName.EXIF_EXTRACTION, data: { asset, fileName } }); + } + } +}