refactor(server): device info (#1490)

* refactor(server): device info

* fix: export device service

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen
2023-02-01 15:55:06 -05:00
committed by GitHub
parent 32b9e0bad4
commit bb84464216
23 changed files with 249 additions and 189 deletions

View File

@@ -1,24 +0,0 @@
import { Body, Controller, Put, ValidationPipe } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
import { Authenticated } from '../../decorators/authenticated.decorator';
import { DeviceInfoService } from './device-info.service';
import { UpsertDeviceInfoDto } from './dto/upsert-device-info.dto';
import { DeviceInfoResponseDto, mapDeviceInfoResponse } from './response-dto/device-info-response.dto';
@Authenticated()
@ApiBearerAuth()
@ApiTags('Device Info')
@Controller('device-info')
export class DeviceInfoController {
constructor(private readonly deviceInfoService: DeviceInfoService) {}
@Put()
public async upsertDeviceInfo(
@GetAuthUser() user: AuthUserDto,
@Body(ValidationPipe) dto: UpsertDeviceInfoDto,
): Promise<DeviceInfoResponseDto> {
const deviceInfo = await this.deviceInfoService.upsert({ ...dto, userId: user.id });
return mapDeviceInfoResponse(deviceInfo);
}
}

View File

@@ -1,12 +0,0 @@
import { Module } from '@nestjs/common';
import { DeviceInfoService } from './device-info.service';
import { DeviceInfoController } from './device-info.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DeviceInfoEntity } from '@app/infra';
@Module({
imports: [TypeOrmModule.forFeature([DeviceInfoEntity])],
controllers: [DeviceInfoController],
providers: [DeviceInfoService],
})
export class DeviceInfoModule {}

View File

@@ -1,65 +0,0 @@
import { DeviceInfoEntity, DeviceType } from '@app/infra';
import { Repository } from 'typeorm';
import { DeviceInfoService } from './device-info.service';
const deviceId = 'device-123';
const userId = 'user-123';
describe('DeviceInfoService', () => {
let sut: DeviceInfoService;
let repositoryMock: jest.Mocked<Repository<DeviceInfoEntity>>;
beforeEach(async () => {
repositoryMock = {
findOne: jest.fn(),
save: jest.fn(),
} as unknown as jest.Mocked<Repository<DeviceInfoEntity>>;
sut = new DeviceInfoService(repositoryMock);
});
it('should be defined', () => {
expect(sut).toBeDefined();
});
describe('upsert', () => {
it('should create a new record', async () => {
const request = { deviceId, userId, deviceType: DeviceType.IOS } as DeviceInfoEntity;
const response = { ...request, id: 1 } as DeviceInfoEntity;
repositoryMock.findOne.mockResolvedValue(null);
repositoryMock.save.mockResolvedValue(response);
await expect(sut.upsert(request)).resolves.toEqual(response);
expect(repositoryMock.findOne).toHaveBeenCalledTimes(1);
expect(repositoryMock.save).toHaveBeenCalledTimes(1);
});
it('should update an existing record', async () => {
const request = { deviceId, userId, deviceType: DeviceType.IOS, isAutoBackup: true } as DeviceInfoEntity;
const response = { ...request, id: 1 } as DeviceInfoEntity;
repositoryMock.findOne.mockResolvedValue(response);
repositoryMock.save.mockResolvedValue(response);
await expect(sut.upsert(request)).resolves.toEqual(response);
expect(repositoryMock.findOne).toHaveBeenCalledTimes(1);
expect(repositoryMock.save).toHaveBeenCalledTimes(1);
});
it('should keep properties that were not updated', async () => {
const request = { deviceId, userId } as DeviceInfoEntity;
const response = { id: 1, isAutoBackup: true, deviceId, userId, deviceType: DeviceType.WEB } as DeviceInfoEntity;
repositoryMock.findOne.mockResolvedValue(response);
repositoryMock.save.mockResolvedValue(response);
await expect(sut.upsert(request)).resolves.toEqual(response);
expect(repositoryMock.findOne).toHaveBeenCalledTimes(1);
expect(repositoryMock.save).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -1,31 +0,0 @@
import { DeviceInfoEntity } from '@app/infra';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
type EntityKeys = Pick<DeviceInfoEntity, 'deviceId' | 'userId'>;
type Entity = EntityKeys & Partial<DeviceInfoEntity>;
@Injectable()
export class DeviceInfoService {
constructor(
@InjectRepository(DeviceInfoEntity)
private repository: Repository<DeviceInfoEntity>,
) {}
public async upsert(entity: Entity): Promise<DeviceInfoEntity> {
const { deviceId, userId } = entity;
const exists = await this.repository.findOne({ where: { userId, deviceId } });
if (!exists) {
if (!entity.isAutoBackup) {
entity.isAutoBackup = false;
}
return await this.repository.save(entity);
}
exists.isAutoBackup = entity.isAutoBackup ?? exists.isAutoBackup;
exists.deviceType = entity.deviceType ?? exists.deviceType;
return await this.repository.save(exists);
}
}

View File

@@ -1,15 +0,0 @@
import { IsNotEmpty, IsOptional } from 'class-validator';
import { DeviceType } from '@app/infra';
import { ApiProperty } from '@nestjs/swagger';
export class UpsertDeviceInfoDto {
@IsNotEmpty()
deviceId!: string;
@IsNotEmpty()
@ApiProperty({ enumName: 'DeviceTypeEnum', enum: DeviceType })
deviceType!: DeviceType;
@IsOptional()
isAutoBackup?: boolean;
}

View File

@@ -1,26 +0,0 @@
import { DeviceInfoEntity, DeviceType } from '@app/infra';
import { ApiProperty } from '@nestjs/swagger';
export class DeviceInfoResponseDto {
@ApiProperty({ type: 'integer' })
id!: number;
userId!: string;
deviceId!: string;
@ApiProperty({ enumName: 'DeviceTypeEnum', enum: DeviceType })
deviceType!: DeviceType;
createdAt!: string;
isAutoBackup!: boolean;
}
export function mapDeviceInfoResponse(entity: DeviceInfoEntity): DeviceInfoResponseDto {
return {
id: entity.id,
userId: entity.userId,
deviceId: entity.deviceId,
deviceType: entity.deviceType,
createdAt: entity.createdAt,
isAutoBackup: entity.isAutoBackup,
};
}

View File

@@ -1,7 +1,6 @@
import { immichAppConfig } from '@app/common/config';
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AssetModule } from './api-v1/asset/asset.module';
import { DeviceInfoModule } from './api-v1/device-info/device-info.module';
import { ConfigModule } from '@nestjs/config';
import { ServerInfoModule } from './api-v1/server-info/server-info.module';
import { CommunicationModule } from './api-v1/communication/communication.module';
@@ -16,6 +15,7 @@ import { InfraModule } from '@app/infra';
import {
APIKeyController,
AuthController,
DeviceInfoController,
OAuthController,
ShareController,
SystemConfigController,
@@ -34,8 +34,6 @@ import { AuthGuard } from './middlewares/auth.guard';
AssetModule,
DeviceInfoModule,
ServerInfoModule,
CommunicationModule,
@@ -55,6 +53,7 @@ import { AuthGuard } from './middlewares/auth.guard';
AppController,
APIKeyController,
AuthController,
DeviceInfoController,
OAuthController,
ShareController,
SystemConfigController,

View File

@@ -0,0 +1,23 @@
import {
AuthUserDto,
DeviceInfoResponseDto as ResponseDto,
DeviceInfoService,
UpsertDeviceInfoDto as UpsertDto,
} from '@app/domain';
import { Body, Controller, Put, ValidationPipe } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { GetAuthUser } from '../decorators/auth-user.decorator';
import { Authenticated } from '../decorators/authenticated.decorator';
@Authenticated()
@ApiBearerAuth()
@ApiTags('Device Info')
@Controller('device-info')
export class DeviceInfoController {
constructor(private readonly service: DeviceInfoService) {}
@Put()
upsertDeviceInfo(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) dto: UpsertDto): Promise<ResponseDto> {
return this.service.upsert(authUser, dto);
}
}

View File

@@ -1,5 +1,6 @@
export * from './api-key.controller';
export * from './auth.controller';
export * from './device-info.controller';
export * from './oauth.controller';
export * from './share.controller';
export * from './system-config.controller';