refactor(server): jobs and processors (#1787)

* refactor: jobs and processors

* refactor: storage migration processor

* fix: tests

* fix: code warning

* chore: ignore coverage from infra

* fix: sync move asset logic between job core and asset core

* refactor: move error handling inside of catch

* refactor(server): job core into dedicated service calls

* refactor: smart info

* fix: tests

* chore: smart info tests

* refactor: use asset repository

* refactor: thumbnail processor

* chore: coverage reqs
This commit is contained in:
Jason Rasmussen
2023-02-25 09:12:03 -05:00
committed by GitHub
parent 71d8567f18
commit 6c7679714b
108 changed files with 1645 additions and 1072 deletions

View File

@@ -23,6 +23,7 @@ export interface IAssetRepository {
asset: Omit<AssetEntity, 'id' | 'createdAt' | 'updatedAt' | 'ownerId' | 'livePhotoVideoId'>,
): Promise<AssetEntity>;
remove(asset: AssetEntity): Promise<void>;
save(asset: Partial<AssetEntity>): Promise<AssetEntity>;
update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity>;
getAll(): Promise<AssetEntity[]>;
@@ -292,6 +293,11 @@ export class AssetRepository implements IAssetRepository {
await this.assetRepository.remove(asset);
}
async save(asset: Partial<AssetEntity>): Promise<AssetEntity> {
const { id } = await this.assetRepository.save(asset);
return this.assetRepository.findOneOrFail({ where: { id } });
}
/**
* Update asset
*/

View File

@@ -1,15 +1,29 @@
import { AuthUserDto, IJobRepository, JobName } from '@app/domain';
import { AssetEntity, UserEntity } from '@app/infra/db/entities';
import { StorageService } from '@app/storage';
import {
AuthUserDto,
IJobRepository,
IStorageRepository,
ISystemConfigRepository,
JobName,
StorageTemplateCore,
} from '@app/domain';
import { AssetEntity, SystemConfig, UserEntity } from '@app/infra/db/entities';
import { Logger } from '@nestjs/common';
import { IAssetRepository } from './asset-repository';
import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
export class AssetCore {
private templateCore: StorageTemplateCore;
private logger = new Logger(AssetCore.name);
constructor(
private repository: IAssetRepository,
private jobRepository: IJobRepository,
private storageService: StorageService,
) {}
configRepository: ISystemConfigRepository,
config: SystemConfig,
private storageRepository: IStorageRepository,
) {
this.templateCore = new StorageTemplateCore(configRepository, config, storageRepository);
}
async create(
authUser: AuthUserDto,
@@ -42,10 +56,31 @@ export class AssetCore {
sharedLinks: [],
});
asset = await this.storageService.moveAsset(asset, file.originalName);
asset = await this.moveAsset(asset, file.originalName);
await this.jobRepository.add({ name: JobName.ASSET_UPLOADED, data: { asset, fileName: file.originalName } });
await this.jobRepository.queue({ name: JobName.ASSET_UPLOADED, data: { asset, fileName: file.originalName } });
return asset;
}
async moveAsset(asset: AssetEntity, originalName: string) {
const destination = await this.templateCore.getTemplatePath(asset, originalName);
if (asset.originalPath !== destination) {
const source = asset.originalPath;
try {
await this.storageRepository.moveFile(asset.originalPath, destination);
try {
await this.repository.save({ id: asset.id, originalPath: destination });
asset.originalPath = destination;
} catch (error: any) {
this.logger.warn('Unable to save new originalPath to database, undoing move', error?.stack);
await this.storageRepository.moveFile(destination, source);
}
} catch (error: any) {
this.logger.error(`Problem applying storage template`, error?.stack, { id: asset.id, source, destination });
}
}
return asset;
}
}

View File

@@ -3,12 +3,10 @@ import { AssetService } from './asset.service';
import { AssetController } from './asset.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AssetEntity } from '@app/infra';
import { CommunicationModule } from '../communication/communication.module';
import { AssetRepository, IAssetRepository } from './asset-repository';
import { DownloadModule } from '../../modules/download/download.module';
import { TagModule } from '../tag/tag.module';
import { AlbumModule } from '../album/album.module';
import { StorageModule } from '@app/storage';
const ASSET_REPOSITORY_PROVIDER = {
provide: IAssetRepository,
@@ -17,11 +15,10 @@ const ASSET_REPOSITORY_PROVIDER = {
@Module({
imports: [
//
TypeOrmModule.forFeature([AssetEntity]),
CommunicationModule,
DownloadModule,
TagModule,
StorageModule,
AlbumModule,
],
controllers: [AssetController],

View File

@@ -8,19 +8,30 @@ import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
import { DownloadService } from '../../modules/download/download.service';
import { AlbumRepository, IAlbumRepository } from '../album/album-repository';
import { StorageService } from '@app/storage';
import { ICryptoRepository, IJobRepository, ISharedLinkRepository, IStorageRepository, JobName } from '@app/domain';
import {
ICryptoRepository,
IJobRepository,
ISharedLinkRepository,
IStorageRepository,
ISystemConfigRepository,
JobName,
} from '@app/domain';
import {
assetEntityStub,
authStub,
fileStub,
newCryptoRepositoryMock,
newJobRepositoryMock,
newSharedLinkRepositoryMock,
newStorageRepositoryMock,
newSystemConfigRepositoryMock,
sharedLinkResponseStub,
sharedLinkStub,
systemConfigStub,
} from '@app/domain/../test';
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import { when } from 'jest-when';
const _getCreateAssetDto = (): CreateAssetDto => {
const createAssetDto = new CreateAssetDto();
@@ -109,8 +120,8 @@ describe('AssetService', () => {
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
let storageServiceMock: jest.Mocked<StorageService>;
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
let configMock: jest.Mocked<ISystemConfigRepository>;
let cryptoMock: jest.Mocked<ICryptoRepository>;
let jobMock: jest.Mocked<IJobRepository>;
let storageMock: jest.Mocked<IStorageRepository>;
@@ -120,6 +131,7 @@ describe('AssetService', () => {
get: jest.fn(),
create: jest.fn(),
remove: jest.fn(),
save: jest.fn(),
update: jest.fn(),
getAll: jest.fn(),
@@ -150,13 +162,9 @@ describe('AssetService', () => {
downloadArchive: jest.fn(),
};
storageServiceMock = {
moveAsset: jest.fn(),
removeEmptyDirectories: jest.fn(),
} as unknown as jest.Mocked<StorageService>;
sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
jobMock = newJobRepositoryMock();
configMock = newSystemConfigRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
storageMock = newStorageRepositoryMock();
@@ -165,12 +173,20 @@ describe('AssetService', () => {
albumRepositoryMock,
a,
downloadServiceMock as DownloadService,
storageServiceMock,
sharedLinkRepositoryMock,
jobMock,
configMock,
systemConfigStub.defaults,
cryptoMock,
storageMock,
);
when(assetRepositoryMock.get)
.calledWith(assetEntityStub.livePhotoStillAsset.id)
.mockResolvedValue(assetEntityStub.livePhotoStillAsset);
when(assetRepositoryMock.get)
.calledWith(assetEntityStub.livePhotoMotionAsset.id)
.mockResolvedValue(assetEntityStub.livePhotoMotionAsset);
});
describe('createAssetsSharedLink', () => {
@@ -255,10 +271,16 @@ describe('AssetService', () => {
};
const dto = _getCreateAssetDto();
assetRepositoryMock.create.mockImplementation(() => Promise.resolve(assetEntity));
storageServiceMock.moveAsset.mockResolvedValue({ ...assetEntity, originalPath: 'fake_new_path/asset_123.jpeg' });
assetRepositoryMock.create.mockResolvedValue(assetEntity);
assetRepositoryMock.save.mockResolvedValue(assetEntity);
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' });
expect(assetRepositoryMock.create).toHaveBeenCalled();
expect(assetRepositoryMock.save).toHaveBeenCalledWith({
id: 'id_1',
originalPath: 'upload/user_id_1/2022/2022-06-19/asset_1.jpeg',
});
});
it('should handle a duplicate', async () => {
@@ -277,59 +299,43 @@ describe('AssetService', () => {
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: true, id: 'id_1' });
expect(jobMock.add).toHaveBeenCalledWith({
name: JobName.DELETE_FILE_ON_DISK,
data: { assets: [{ originalPath: 'fake_path/asset_1.jpeg', resizePath: null }] },
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.DELETE_FILES,
data: { files: ['fake_path/asset_1.jpeg', undefined] },
});
expect(storageServiceMock.moveAsset).not.toHaveBeenCalled();
expect(storageMock.moveFile).not.toHaveBeenCalled();
});
it('should handle a live photo', async () => {
const file = {
originalPath: 'fake_path/asset_1.jpeg',
mimeType: 'image/jpeg',
checksum: Buffer.from('file hash', 'utf8'),
originalName: 'asset_1.jpeg',
};
const asset = {
id: 'live-photo-asset',
originalPath: file.originalPath,
ownerId: authStub.user1.id,
type: AssetType.IMAGE,
isVisible: true,
} as AssetEntity;
const livePhotoFile = {
originalPath: 'fake_path/asset_1.mp4',
mimeType: 'image/jpeg',
checksum: Buffer.from('live photo file hash', 'utf8'),
originalName: 'asset_1.jpeg',
};
const livePhotoAsset = {
id: 'live-photo-motion',
originalPath: livePhotoFile.originalPath,
ownerId: authStub.user1.id,
type: AssetType.VIDEO,
isVisible: false,
} as AssetEntity;
const dto = _getCreateAssetDto();
const error = new QueryFailedError('', [], '');
(error as any).constraint = 'UQ_userid_checksum';
assetRepositoryMock.create.mockResolvedValueOnce(livePhotoAsset);
assetRepositoryMock.create.mockResolvedValueOnce(asset);
storageServiceMock.moveAsset.mockImplementation((asset) => Promise.resolve(asset));
assetRepositoryMock.create.mockResolvedValueOnce(assetEntityStub.livePhotoMotionAsset);
assetRepositoryMock.save.mockResolvedValueOnce(assetEntityStub.livePhotoMotionAsset);
assetRepositoryMock.create.mockResolvedValueOnce(assetEntityStub.livePhotoStillAsset);
assetRepositoryMock.save.mockResolvedValueOnce(assetEntityStub.livePhotoStillAsset);
await expect(sut.uploadFile(authStub.user1, dto, file, livePhotoFile)).resolves.toEqual({
await expect(
sut.uploadFile(authStub.user1, dto, fileStub.livePhotoStill, fileStub.livePhotoMotion),
).resolves.toEqual({
duplicate: false,
id: 'live-photo-asset',
id: 'live-photo-still-asset',
});
expect(jobMock.add.mock.calls).toEqual([
[{ name: JobName.ASSET_UPLOADED, data: { asset: livePhotoAsset, fileName: file.originalName } }],
[{ name: JobName.ASSET_UPLOADED, data: { asset, fileName: file.originalName } }],
expect(jobMock.queue.mock.calls).toEqual([
[
{
name: JobName.ASSET_UPLOADED,
data: { asset: assetEntityStub.livePhotoMotionAsset, fileName: 'asset_1.mp4' },
},
],
[
{
name: JobName.ASSET_UPLOADED,
data: { asset: assetEntityStub.livePhotoStillAsset, fileName: 'asset_1.jpeg' },
},
],
]);
});
});
@@ -383,7 +389,7 @@ describe('AssetService', () => {
{ id: 'asset1', status: 'FAILED' },
]);
expect(jobMock.add).not.toHaveBeenCalled();
expect(jobMock.queue).not.toHaveBeenCalled();
});
it('should return failed status a delete fails', async () => {
@@ -394,35 +400,66 @@ describe('AssetService', () => {
{ id: 'asset1', status: 'FAILED' },
]);
expect(jobMock.add).not.toHaveBeenCalled();
expect(jobMock.queue).not.toHaveBeenCalled();
});
it('should delete a live photo', async () => {
assetRepositoryMock.get.mockResolvedValueOnce({ id: 'asset1', livePhotoVideoId: 'live-photo' } as AssetEntity);
assetRepositoryMock.get.mockResolvedValueOnce({ id: 'live-photo' } as AssetEntity);
await expect(sut.deleteAll(authStub.user1, { ids: ['asset1'] })).resolves.toEqual([
{ id: 'asset1', status: 'SUCCESS' },
{ id: 'live-photo', status: 'SUCCESS' },
await expect(sut.deleteAll(authStub.user1, { ids: [assetEntityStub.livePhotoStillAsset.id] })).resolves.toEqual([
{ id: assetEntityStub.livePhotoStillAsset.id, status: 'SUCCESS' },
{ id: assetEntityStub.livePhotoMotionAsset.id, status: 'SUCCESS' },
]);
expect(jobMock.add).toHaveBeenCalledWith({
name: JobName.DELETE_FILE_ON_DISK,
data: { assets: [{ id: 'asset1', livePhotoVideoId: 'live-photo' }, { id: 'live-photo' }] },
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.DELETE_FILES,
data: {
files: ['fake_path/asset_1.jpeg', undefined, undefined, 'fake_path/asset_1.mp4', undefined, undefined],
},
});
});
it('should delete a batch of assets', async () => {
assetRepositoryMock.get.mockImplementation((id) => Promise.resolve({ id } as AssetEntity));
assetRepositoryMock.remove.mockImplementation(() => Promise.resolve());
const asset1 = {
id: 'asset1',
originalPath: 'original-path-1',
resizePath: 'resize-path-1',
webpPath: 'web-path-1',
};
const asset2 = {
id: 'asset2',
originalPath: 'original-path-2',
resizePath: 'resize-path-2',
webpPath: 'web-path-2',
};
when(assetRepositoryMock.get)
.calledWith(asset1.id)
.mockResolvedValue(asset1 as AssetEntity);
when(assetRepositoryMock.get)
.calledWith(asset2.id)
.mockResolvedValue(asset2 as AssetEntity);
await expect(sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'] })).resolves.toEqual([
{ id: 'asset1', status: 'SUCCESS' },
{ id: 'asset2', status: 'SUCCESS' },
]);
expect(jobMock.add.mock.calls).toEqual([
[{ name: JobName.DELETE_FILE_ON_DISK, data: { assets: [{ id: 'asset1' }, { id: 'asset2' }] } }],
expect(jobMock.queue.mock.calls).toEqual([
[
{
name: JobName.DELETE_FILES,
data: {
files: [
'original-path-1',
'web-path-1',
'resize-path-1',
'original-path-2',
'web-path-2',
'resize-path-2',
],
},
},
],
]);
});
});

View File

@@ -12,7 +12,7 @@ import {
import { InjectRepository } from '@nestjs/typeorm';
import { QueryFailedError, Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { AssetEntity, AssetType, SharedLinkType } from '@app/infra';
import { AssetEntity, AssetType, SharedLinkType, SystemConfig } from '@app/infra';
import { constants, createReadStream, ReadStream, stat } from 'fs';
import { ServeFileDto } from './dto/serve-file.dto';
import { Response as Res } from 'express';
@@ -25,7 +25,9 @@ import { CuratedObjectsResponseDto } from './response-dto/curated-objects-respon
import {
AssetResponseDto,
ImmichReadStream,
INITIAL_SYSTEM_CONFIG,
IStorageRepository,
ISystemConfigRepository,
JobName,
mapAsset,
mapAssetWithoutExif,
@@ -52,7 +54,6 @@ import { ICryptoRepository, IJobRepository } from '@app/domain';
import { DownloadService } from '../../modules/download/download.service';
import { DownloadDto } from './dto/download-library.dto';
import { IAlbumRepository } from '../album/album-repository';
import { StorageService } from '@app/storage';
import { ShareCore } from '@app/domain';
import { ISharedLinkRepository } from '@app/domain';
import { DownloadFilesDto } from './dto/download-files.dto';
@@ -61,6 +62,8 @@ import { mapSharedLink, SharedLinkResponseDto } from '@app/domain';
import { AssetSearchDto } from './dto/asset-search.dto';
import { AddAssetsDto } from '../album/dto/add-assets.dto';
import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
import path from 'path';
import { getFileNameWithoutExtension } from '../../utils/file-name.util';
const fileInfo = promisify(stat);
@@ -76,13 +79,14 @@ export class AssetService {
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
private downloadService: DownloadService,
storageService: StorageService,
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(INITIAL_SYSTEM_CONFIG) config: SystemConfig,
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
@Inject(IStorageRepository) private storage: IStorageRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
) {
this.assetCore = new AssetCore(_assetRepository, jobRepository, storageService);
this.assetCore = new AssetCore(_assetRepository, jobRepository, configRepository, config, storageRepository);
this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
}
@@ -93,7 +97,10 @@ export class AssetService {
livePhotoFile?: UploadFile,
): Promise<AssetFileUploadResponseDto> {
if (livePhotoFile) {
livePhotoFile.originalName = file.originalName;
livePhotoFile = {
...livePhotoFile,
originalName: getFileNameWithoutExtension(file.originalName) + path.extname(livePhotoFile.originalName),
};
}
let livePhotoAsset: AssetEntity | null = null;
@@ -109,16 +116,9 @@ export class AssetService {
return { id: asset.id, duplicate: false };
} catch (error: any) {
// clean up files
await this.jobRepository.add({
name: JobName.DELETE_FILE_ON_DISK,
data: {
assets: [
{
originalPath: file.originalPath,
resizePath: livePhotoFile?.originalPath || null,
} as AssetEntity,
],
},
await this.jobRepository.queue({
name: JobName.DELETE_FILES,
data: { files: [file.originalPath, livePhotoFile?.originalPath] },
});
// handle duplicates with a success response
@@ -204,7 +204,7 @@ export class AssetService {
try {
const asset = await this._assetRepository.get(assetId);
if (asset && asset.originalPath && asset.mimeType) {
return this.storage.createReadStream(asset.originalPath, asset.mimeType);
return this.storageRepository.createReadStream(asset.originalPath, asset.mimeType);
}
} catch (e) {
Logger.error(`Error download asset ${e}`, 'downloadFile');
@@ -412,7 +412,7 @@ export class AssetService {
}
public async deleteAll(authUser: AuthUserDto, dto: DeleteAssetDto): Promise<DeleteAssetResponseDto[]> {
const deleteQueue: AssetEntity[] = [];
const deleteQueue: Array<string | null> = [];
const result: DeleteAssetResponseDto[] = [];
const ids = dto.ids.slice();
@@ -427,7 +427,7 @@ export class AssetService {
await this._assetRepository.remove(asset);
result.push({ id, status: DeleteAssetStatusEnum.SUCCESS });
deleteQueue.push(asset as any);
deleteQueue.push(asset.originalPath, asset.webpPath, asset.resizePath);
// TODO refactor this to use cascades
if (asset.livePhotoVideoId && !ids.includes(asset.livePhotoVideoId)) {
@@ -439,7 +439,7 @@ export class AssetService {
}
if (deleteQueue.length > 0) {
await this.jobRepository.add({ name: JobName.DELETE_FILE_ON_DISK, data: { assets: deleteQueue } });
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: deleteQueue } });
}
return result;

View File

@@ -1,34 +0,0 @@
import { Logger } from '@nestjs/common';
import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { AuthService } from '@app/domain';
@WebSocketGateway({ cors: true })
export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisconnect {
private logger = new Logger(CommunicationGateway.name);
constructor(private authService: AuthService) {}
@WebSocketServer() server!: Server;
handleDisconnect(client: Socket) {
client.leave(client.nsp.name);
this.logger.log(`Client ${client.id} disconnected from Websocket`);
}
async handleConnection(client: Socket) {
try {
this.logger.log(`New websocket connection: ${client.id}`);
const user = await this.authService.validate(client.request.headers, {});
if (user) {
client.join(user.id);
} else {
client.emit('error', 'unauthorized');
client.disconnect();
}
} catch (e) {
client.emit('error', 'unauthorized');
client.disconnect();
}
}
}

View File

@@ -1,8 +0,0 @@
import { Module } from '@nestjs/common';
import { CommunicationGateway } from './communication.gateway';
@Module({
providers: [CommunicationGateway],
exports: [CommunicationGateway],
})
export class CommunicationModule {}

View File

@@ -48,14 +48,14 @@ export class JobService {
? await this._assetRepository.getAllVideos()
: await this._assetRepository.getAssetWithNoEncodedVideo();
for (const asset of assets) {
await this.jobRepository.add({ name: JobName.VIDEO_CONVERSION, data: { asset } });
await this.jobRepository.queue({ name: JobName.VIDEO_CONVERSION, data: { asset } });
}
return assets.length;
}
case QueueName.CONFIG:
await this.jobRepository.add({ name: JobName.TEMPLATE_MIGRATION });
case QueueName.STORAGE_TEMPLATE_MIGRATION:
await this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION });
return 1;
case QueueName.MACHINE_LEARNING: {
@@ -68,8 +68,8 @@ export class JobService {
: await this._assetRepository.getAssetWithNoSmartInfo();
for (const asset of assets) {
await this.jobRepository.add({ name: JobName.IMAGE_TAGGING, data: { asset } });
await this.jobRepository.add({ name: JobName.OBJECT_DETECTION, data: { asset } });
await this.jobRepository.queue({ name: JobName.IMAGE_TAGGING, data: { asset } });
await this.jobRepository.queue({ name: JobName.OBJECT_DETECTION, data: { asset } });
}
return assets.length;
}
@@ -81,7 +81,7 @@ export class JobService {
for (const asset of assets) {
if (asset.type === AssetType.VIDEO) {
await this.jobRepository.add({
await this.jobRepository.queue({
name: JobName.EXTRACT_VIDEO_METADATA,
data: {
asset,
@@ -89,7 +89,7 @@ export class JobService {
},
});
} else {
await this.jobRepository.add({
await this.jobRepository.queue({
name: JobName.EXIF_EXTRACTION,
data: {
asset,
@@ -107,7 +107,7 @@ export class JobService {
: await this._assetRepository.getAssetWithNoThumbnail();
for (const asset of assets) {
await this.jobRepository.add({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { asset } });
await this.jobRepository.queue({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { asset } });
}
return assets.length;
}
@@ -129,7 +129,7 @@ export class JobService {
return QueueName.VIDEO_CONVERSION;
case JobId.STORAGE_TEMPLATE_MIGRATION:
return QueueName.CONFIG;
return QueueName.STORAGE_TEMPLATE_MIGRATION;
case JobId.MACHINE_LEARNING:
return QueueName.MACHINE_LEARNING;

View File

@@ -3,7 +3,6 @@ import { Module } from '@nestjs/common';
import { AssetModule } from './api-v1/asset/asset.module';
import { ConfigModule } from '@nestjs/config';
import { ServerInfoModule } from './api-v1/server-info/server-info.module';
import { CommunicationModule } from './api-v1/communication/communication.module';
import { AlbumModule } from './api-v1/album/album.module';
import { AppController } from './app.controller';
import { ScheduleModule } from '@nestjs/schedule';
@@ -36,8 +35,6 @@ import { AuthGuard } from './middlewares/auth.guard';
ServerInfoModule,
CommunicationModule,
AlbumModule,
ScheduleModule.forRoot(),

View File

@@ -1,26 +1,13 @@
import { Inject, Injectable } from '@nestjs/common';
import { UserService } from '@app/domain';
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm';
import { IsNull, Not, Repository } from 'typeorm';
import { UserEntity } from '@app/infra';
import { userUtils } from '@app/common';
import { IJobRepository, JobName } from '@app/domain';
@Injectable()
export class ScheduleTasksService {
constructor(
@InjectRepository(UserEntity)
private userRepository: Repository<UserEntity>,
constructor(private userService: UserService) {}
@Inject(IJobRepository) private jobRepository: IJobRepository,
) {}
@Cron(CronExpression.EVERY_DAY_AT_11PM)
async deleteUserAndRelatedAssets() {
const usersToDelete = await this.userRepository.find({ withDeleted: true, where: { deletedAt: Not(IsNull()) } });
for (const user of usersToDelete) {
if (userUtils.isReadyForDeletion(user)) {
await this.jobRepository.add({ name: JobName.USER_DELETION, data: { user } });
}
}
async onUserDeleteCheck() {
await this.userService.handleUserDeleteCheck();
}
}

View File

@@ -9,7 +9,6 @@
},
"moduleNameMapper": {
"^@app/common": "<rootDir>../../../libs/common/src",
"^@app/storage(|/.*)$": "<rootDir>../../../libs/storage/src/$1",
"^@app/infra(|/.*)$": "<rootDir>../../../libs/infra/src/$1",
"^@app/domain(|/.*)$": "<rootDir>../../../libs/domain/src/$1"
}