refactor(server): device info service (#1071)

* refactor(server): device info service

* use upsertDeviceInfo in mobile app

* fix: return types and dedupe code

Co-authored-by: Fynn Petersen-Frey <zoodyy@users.noreply.github.com>
This commit is contained in:
Jason Rasmussen
2022-12-08 10:57:07 -05:00
committed by GitHub
parent b8e26a2112
commit cefdd86b7f
23 changed files with 468 additions and 443 deletions

View File

@@ -1,11 +1,10 @@
import { Controller, Post, Body, Patch, ValidationPipe } from '@nestjs/common';
import { Body, Controller, Patch, Post, 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 { CreateDeviceInfoDto } from './dto/create-device-info.dto';
import { UpdateDeviceInfoDto } from './dto/update-device-info.dto';
import { DeviceInfoResponseDto } from './response-dto/create-device-info-response.dto';
import { UpsertDeviceInfoDto } from './dto/upsert-device-info.dto';
import { DeviceInfoResponseDto, mapDeviceInfoResponse } from './response-dto/device-info-response.dto';
@Authenticated()
@ApiBearerAuth()
@@ -14,19 +13,30 @@ import { DeviceInfoResponseDto } from './response-dto/create-device-info-respons
export class DeviceInfoController {
constructor(private readonly deviceInfoService: DeviceInfoService) {}
/** @deprecated */
@Post()
async createDeviceInfo(
@Body(ValidationPipe) createDeviceInfoDto: CreateDeviceInfoDto,
@GetAuthUser() authUser: AuthUserDto,
public async createDeviceInfo(
@GetAuthUser() user: AuthUserDto,
@Body(ValidationPipe) dto: UpsertDeviceInfoDto,
): Promise<DeviceInfoResponseDto> {
return this.deviceInfoService.create(createDeviceInfoDto, authUser);
return this.upsertDeviceInfo(user, dto);
}
/** @deprecated */
@Patch()
async updateDeviceInfo(
@Body(ValidationPipe) updateDeviceInfoDto: UpdateDeviceInfoDto,
@GetAuthUser() authUser: AuthUserDto,
public async updateDeviceInfo(
@GetAuthUser() user: AuthUserDto,
@Body(ValidationPipe) dto: UpsertDeviceInfoDto,
): Promise<DeviceInfoResponseDto> {
return this.deviceInfoService.update(authUser.id, updateDeviceInfoDto);
return this.upsertDeviceInfo(user, dto);
}
@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

@@ -0,0 +1,65 @@
import { DeviceInfoEntity, DeviceType } from '@app/database/entities/device-info.entity';
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,70 +1,29 @@
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { DeviceInfoEntity } from '@app/database/entities/device-info.entity';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateDeviceInfoDto } from './dto/create-device-info.dto';
import { UpdateDeviceInfoDto } from './dto/update-device-info.dto';
import { DeviceInfoEntity } from '@app/database/entities/device-info.entity';
import { DeviceInfoResponseDto, mapDeviceInfoResponse } from './response-dto/create-device-info-response.dto';
type EntityKeys = Pick<DeviceInfoEntity, 'deviceId' | 'userId'>;
type Entity = EntityKeys & Partial<DeviceInfoEntity>;
@Injectable()
export class DeviceInfoService {
constructor(
@InjectRepository(DeviceInfoEntity)
private deviceRepository: Repository<DeviceInfoEntity>,
private repository: Repository<DeviceInfoEntity>,
) {}
async create(createDeviceInfoDto: CreateDeviceInfoDto, authUser: AuthUserDto): Promise<DeviceInfoResponseDto> {
const res = await this.deviceRepository.findOne({
where: {
deviceId: createDeviceInfoDto.deviceId,
userId: authUser.id,
},
});
public async upsert(entity: Entity): Promise<DeviceInfoEntity> {
const { deviceId, userId } = entity;
const exists = await this.repository.findOne({ where: { userId, deviceId } });
if (res) {
Logger.log('Device Info Exist', 'createDeviceInfo');
return mapDeviceInfoResponse(res);
if (!exists) {
return await this.repository.save(entity);
}
const deviceInfo = new DeviceInfoEntity();
deviceInfo.deviceId = createDeviceInfoDto.deviceId;
deviceInfo.deviceType = createDeviceInfoDto.deviceType;
deviceInfo.userId = authUser.id;
exists.isAutoBackup = entity.isAutoBackup ?? exists.isAutoBackup;
exists.deviceType = entity.deviceType ?? exists.deviceType;
const newDeviceInfo = await this.deviceRepository.save(deviceInfo);
return mapDeviceInfoResponse(newDeviceInfo);
}
async update(userId: string, updateDeviceInfoDto: UpdateDeviceInfoDto): Promise<DeviceInfoResponseDto> {
const deviceInfo = await this.deviceRepository.findOne({
where: { deviceId: updateDeviceInfoDto.deviceId, userId: userId },
});
if (!deviceInfo) {
throw new NotFoundException('Device Not Found');
}
const res = await this.deviceRepository.update(
{
id: deviceInfo.id,
},
updateDeviceInfoDto,
);
if (res.affected == 1) {
const updatedDeviceInfo = await this.deviceRepository.findOne({
where: { deviceId: updateDeviceInfoDto.deviceId, userId: userId },
});
if (!updatedDeviceInfo) {
throw new NotFoundException('Device Not Found');
}
return mapDeviceInfoResponse(updatedDeviceInfo);
} else {
throw new BadRequestException('Bad Request');
}
return await this.repository.save(exists);
}
}

View File

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

View File

@@ -2,7 +2,7 @@ import { IsNotEmpty, IsOptional } from 'class-validator';
import { DeviceType } from '@app/database/entities/device-info.entity';
import { ApiProperty } from '@nestjs/swagger';
export class CreateDeviceInfoDto {
export class UpsertDeviceInfoDto {
@IsNotEmpty()
deviceId!: string;

View File

@@ -1783,13 +1783,15 @@
"/device-info": {
"post": {
"operationId": "createDeviceInfo",
"summary": "",
"description": "@deprecated",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateDeviceInfoDto"
"$ref": "#/components/schemas/UpsertDeviceInfoDto"
}
}
}
@@ -1817,13 +1819,49 @@
},
"patch": {
"operationId": "updateDeviceInfo",
"summary": "",
"description": "@deprecated",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateDeviceInfoDto"
"$ref": "#/components/schemas/UpsertDeviceInfoDto"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeviceInfoResponseDto"
}
}
}
}
},
"tags": [
"Device Info"
],
"security": [
{
"bearer": []
}
]
},
"put": {
"operationId": "upsertDeviceInfo",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpsertDeviceInfoDto"
}
}
}
@@ -3227,7 +3265,7 @@
"WEB"
]
},
"CreateDeviceInfoDto": {
"UpsertDeviceInfoDto": {
"type": "object",
"properties": {
"deviceType": {
@@ -3276,24 +3314,6 @@
"isAutoBackup"
]
},
"UpdateDeviceInfoDto": {
"type": "object",
"properties": {
"deviceType": {
"$ref": "#/components/schemas/DeviceTypeEnum"
},
"deviceId": {
"type": "string"
},
"isAutoBackup": {
"type": "boolean"
}
},
"required": [
"deviceType",
"deviceId"
]
},
"ServerInfoResponseDto": {
"type": "object",
"properties": {