1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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,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,7 +1,6 @@
import { immichAppConfig } from '@app/common/config'; import { immichAppConfig } from '@app/common/config';
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AssetModule } from './api-v1/asset/asset.module'; import { AssetModule } from './api-v1/asset/asset.module';
import { DeviceInfoModule } from './api-v1/device-info/device-info.module';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { ServerInfoModule } from './api-v1/server-info/server-info.module'; import { ServerInfoModule } from './api-v1/server-info/server-info.module';
import { CommunicationModule } from './api-v1/communication/communication.module'; import { CommunicationModule } from './api-v1/communication/communication.module';
@ -16,6 +15,7 @@ import { InfraModule } from '@app/infra';
import { import {
APIKeyController, APIKeyController,
AuthController, AuthController,
DeviceInfoController,
OAuthController, OAuthController,
ShareController, ShareController,
SystemConfigController, SystemConfigController,
@ -34,8 +34,6 @@ import { AuthGuard } from './middlewares/auth.guard';
AssetModule, AssetModule,
DeviceInfoModule,
ServerInfoModule, ServerInfoModule,
CommunicationModule, CommunicationModule,
@ -55,6 +53,7 @@ import { AuthGuard } from './middlewares/auth.guard';
AppController, AppController,
APIKeyController, APIKeyController,
AuthController, AuthController,
DeviceInfoController,
OAuthController, OAuthController,
ShareController, ShareController,
SystemConfigController, 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 './api-key.controller';
export * from './auth.controller'; export * from './auth.controller';
export * from './device-info.controller';
export * from './oauth.controller'; export * from './oauth.controller';
export * from './share.controller'; export * from './share.controller';
export * from './system-config.controller'; export * from './system-config.controller';

View File

@ -301,6 +301,43 @@
] ]
} }
}, },
"/device-info": {
"put": {
"operationId": "upsertDeviceInfo",
"description": "",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpsertDeviceInfoDto"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeviceInfoResponseDto"
}
}
}
}
},
"tags": [
"Device Info"
],
"security": [
{
"bearer": []
}
]
}
},
"/oauth/mobile-redirect": { "/oauth/mobile-redirect": {
"get": { "get": {
"operationId": "mobileRedirect", "operationId": "mobileRedirect",
@ -2505,43 +2542,6 @@
] ]
} }
}, },
"/device-info": {
"put": {
"operationId": "upsertDeviceInfo",
"description": "",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpsertDeviceInfoDto"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeviceInfoResponseDto"
}
}
}
}
},
"tags": [
"Device Info"
],
"security": [
{
"bearer": []
}
]
}
},
"/server-info": { "/server-info": {
"get": { "get": {
"operationId": "getServerInfo", "operationId": "getServerInfo",
@ -2993,6 +2993,63 @@
"redirectUri" "redirectUri"
] ]
}, },
"DeviceTypeEnum": {
"type": "string",
"enum": [
"IOS",
"ANDROID",
"WEB"
]
},
"UpsertDeviceInfoDto": {
"type": "object",
"properties": {
"deviceType": {
"$ref": "#/components/schemas/DeviceTypeEnum"
},
"deviceId": {
"type": "string"
},
"isAutoBackup": {
"type": "boolean"
}
},
"required": [
"deviceType",
"deviceId"
]
},
"DeviceInfoResponseDto": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"deviceType": {
"$ref": "#/components/schemas/DeviceTypeEnum"
},
"userId": {
"type": "string"
},
"deviceId": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"isAutoBackup": {
"type": "boolean"
}
},
"required": [
"id",
"deviceType",
"userId",
"deviceId",
"createdAt",
"isAutoBackup"
]
},
"OAuthConfigDto": { "OAuthConfigDto": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4265,63 +4322,6 @@
"albumId" "albumId"
] ]
}, },
"DeviceTypeEnum": {
"type": "string",
"enum": [
"IOS",
"ANDROID",
"WEB"
]
},
"UpsertDeviceInfoDto": {
"type": "object",
"properties": {
"deviceType": {
"$ref": "#/components/schemas/DeviceTypeEnum"
},
"deviceId": {
"type": "string"
},
"isAutoBackup": {
"type": "boolean"
}
},
"required": [
"deviceType",
"deviceId"
]
},
"DeviceInfoResponseDto": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"deviceType": {
"$ref": "#/components/schemas/DeviceTypeEnum"
},
"userId": {
"type": "string"
},
"deviceId": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"isAutoBackup": {
"type": "boolean"
}
},
"required": [
"id",
"deviceType",
"userId",
"deviceId",
"createdAt",
"isAutoBackup"
]
},
"ServerInfoResponseDto": { "ServerInfoResponseDto": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -0,0 +1,23 @@
import { DeviceInfoEntity } from '@app/infra/db/entities';
import { IDeviceInfoRepository } from './device-info.repository';
type UpsertKeys = Pick<DeviceInfoEntity, 'deviceId' | 'userId'>;
type UpsertEntity = UpsertKeys & Partial<DeviceInfoEntity>;
export class DeviceInfoCore {
constructor(private repository: IDeviceInfoRepository) {}
async upsert(entity: UpsertEntity) {
const exists = await this.repository.get(entity.userId, entity.deviceId);
if (!exists) {
if (!entity.isAutoBackup) {
entity.isAutoBackup = false;
}
return this.repository.save(entity);
}
exists.isAutoBackup = entity.isAutoBackup ?? exists.isAutoBackup;
exists.deviceType = entity.deviceType ?? exists.deviceType;
return this.repository.save(exists);
}
}

View File

@ -0,0 +1,8 @@
import { DeviceInfoEntity } from '@app/infra/db/entities';
export const IDeviceInfoRepository = 'IDeviceInfoRepository';
export interface IDeviceInfoRepository {
get(userId: string, deviceId: string): Promise<DeviceInfoEntity | null>;
save(entity: Partial<DeviceInfoEntity>): Promise<DeviceInfoEntity>;
}

View File

@ -1,5 +1,6 @@
import { DeviceInfoEntity, DeviceType } from '@app/infra'; import { DeviceInfoEntity, DeviceType } from '@app/infra';
import { Repository } from 'typeorm'; import { authStub, newDeviceInfoRepositoryMock } from '../../test';
import { IDeviceInfoRepository } from './device-info.repository';
import { DeviceInfoService } from './device-info.service'; import { DeviceInfoService } from './device-info.service';
const deviceId = 'device-123'; const deviceId = 'device-123';
@ -7,13 +8,10 @@ const userId = 'user-123';
describe('DeviceInfoService', () => { describe('DeviceInfoService', () => {
let sut: DeviceInfoService; let sut: DeviceInfoService;
let repositoryMock: jest.Mocked<Repository<DeviceInfoEntity>>; let repositoryMock: jest.Mocked<IDeviceInfoRepository>;
beforeEach(async () => { beforeEach(async () => {
repositoryMock = { repositoryMock = newDeviceInfoRepositoryMock();
findOne: jest.fn(),
save: jest.fn(),
} as unknown as jest.Mocked<Repository<DeviceInfoEntity>>;
sut = new DeviceInfoService(repositoryMock); sut = new DeviceInfoService(repositoryMock);
}); });
@ -27,12 +25,12 @@ describe('DeviceInfoService', () => {
const request = { deviceId, userId, deviceType: DeviceType.IOS } as DeviceInfoEntity; const request = { deviceId, userId, deviceType: DeviceType.IOS } as DeviceInfoEntity;
const response = { ...request, id: 1 } as DeviceInfoEntity; const response = { ...request, id: 1 } as DeviceInfoEntity;
repositoryMock.findOne.mockResolvedValue(null); repositoryMock.get.mockResolvedValue(null);
repositoryMock.save.mockResolvedValue(response); repositoryMock.save.mockResolvedValue(response);
await expect(sut.upsert(request)).resolves.toEqual(response); await expect(sut.upsert(authStub.user1, request)).resolves.toEqual(response);
expect(repositoryMock.findOne).toHaveBeenCalledTimes(1); expect(repositoryMock.get).toHaveBeenCalledTimes(1);
expect(repositoryMock.save).toHaveBeenCalledTimes(1); expect(repositoryMock.save).toHaveBeenCalledTimes(1);
}); });
@ -40,12 +38,12 @@ describe('DeviceInfoService', () => {
const request = { deviceId, userId, deviceType: DeviceType.IOS, isAutoBackup: true } as DeviceInfoEntity; const request = { deviceId, userId, deviceType: DeviceType.IOS, isAutoBackup: true } as DeviceInfoEntity;
const response = { ...request, id: 1 } as DeviceInfoEntity; const response = { ...request, id: 1 } as DeviceInfoEntity;
repositoryMock.findOne.mockResolvedValue(response); repositoryMock.get.mockResolvedValue(response);
repositoryMock.save.mockResolvedValue(response); repositoryMock.save.mockResolvedValue(response);
await expect(sut.upsert(request)).resolves.toEqual(response); await expect(sut.upsert(authStub.user1, request)).resolves.toEqual(response);
expect(repositoryMock.findOne).toHaveBeenCalledTimes(1); expect(repositoryMock.get).toHaveBeenCalledTimes(1);
expect(repositoryMock.save).toHaveBeenCalledTimes(1); expect(repositoryMock.save).toHaveBeenCalledTimes(1);
}); });
@ -53,12 +51,12 @@ describe('DeviceInfoService', () => {
const request = { deviceId, userId } as DeviceInfoEntity; const request = { deviceId, userId } as DeviceInfoEntity;
const response = { id: 1, isAutoBackup: true, deviceId, userId, deviceType: DeviceType.WEB } as DeviceInfoEntity; const response = { id: 1, isAutoBackup: true, deviceId, userId, deviceType: DeviceType.WEB } as DeviceInfoEntity;
repositoryMock.findOne.mockResolvedValue(response); repositoryMock.get.mockResolvedValue(response);
repositoryMock.save.mockResolvedValue(response); repositoryMock.save.mockResolvedValue(response);
await expect(sut.upsert(request)).resolves.toEqual(response); await expect(sut.upsert(authStub.user1, request)).resolves.toEqual(response);
expect(repositoryMock.findOne).toHaveBeenCalledTimes(1); expect(repositoryMock.get).toHaveBeenCalledTimes(1);
expect(repositoryMock.save).toHaveBeenCalledTimes(1); expect(repositoryMock.save).toHaveBeenCalledTimes(1);
}); });
}); });

View File

@ -0,0 +1,20 @@
import { Inject, Injectable } from '@nestjs/common';
import { AuthUserDto } from '../auth';
import { DeviceInfoCore } from './device-info.core';
import { IDeviceInfoRepository } from './device-info.repository';
import { UpsertDeviceInfoDto } from './dto';
import { DeviceInfoResponseDto, mapDeviceInfoResponse } from './response-dto';
@Injectable()
export class DeviceInfoService {
private core: DeviceInfoCore;
constructor(@Inject(IDeviceInfoRepository) repository: IDeviceInfoRepository) {
this.core = new DeviceInfoCore(repository);
}
public async upsert(authUser: AuthUserDto, dto: UpsertDeviceInfoDto): Promise<DeviceInfoResponseDto> {
const deviceInfo = await this.core.upsert({ ...dto, userId: authUser.id });
return mapDeviceInfoResponse(deviceInfo);
}
}

View File

@ -0,0 +1 @@
export * from './upsert-device-info.dto';

View File

@ -1,5 +1,5 @@
import { IsNotEmpty, IsOptional } from 'class-validator'; import { IsNotEmpty, IsOptional } from 'class-validator';
import { DeviceType } from '@app/infra'; import { DeviceType } from '@app/infra/db/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export class UpsertDeviceInfoDto { export class UpsertDeviceInfoDto {

View File

@ -0,0 +1,4 @@
export * from './device-info.repository';
export * from './device-info.service';
export * from './dto';
export * from './response-dto';

View File

@ -1,4 +1,4 @@
import { DeviceInfoEntity, DeviceType } from '@app/infra'; import { DeviceInfoEntity, DeviceType } from '@app/infra/db/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export class DeviceInfoResponseDto { export class DeviceInfoResponseDto {

View File

@ -0,0 +1 @@
export * from './device-info-response.dto';

View File

@ -1,6 +1,7 @@
import { DynamicModule, Global, Module, ModuleMetadata, Provider } from '@nestjs/common'; import { DynamicModule, Global, Module, ModuleMetadata, Provider } from '@nestjs/common';
import { APIKeyService } from './api-key'; import { APIKeyService } from './api-key';
import { AuthService } from './auth'; import { AuthService } from './auth';
import { DeviceInfoService } from './device-info';
import { JobService } from './job'; import { JobService } from './job';
import { OAuthService } from './oauth'; import { OAuthService } from './oauth';
import { ShareService } from './share'; import { ShareService } from './share';
@ -10,6 +11,7 @@ import { UserService } from './user';
const providers: Provider[] = [ const providers: Provider[] = [
APIKeyService, APIKeyService,
AuthService, AuthService,
DeviceInfoService,
JobService, JobService,
OAuthService, OAuthService,
SystemConfigService, SystemConfigService,

View File

@ -3,6 +3,7 @@ export * from './api-key';
export * from './asset'; export * from './asset';
export * from './auth'; export * from './auth';
export * from './crypto'; export * from './crypto';
export * from './device-info';
export * from './domain.module'; export * from './domain.module';
export * from './job'; export * from './job';
export * from './oauth'; export * from './oauth';

View File

@ -0,0 +1,8 @@
import { IDeviceInfoRepository } from '../src';
export const newDeviceInfoRepositoryMock = (): jest.Mocked<IDeviceInfoRepository> => {
return {
get: jest.fn(),
save: jest.fn(),
};
};

View File

@ -1,5 +1,6 @@
export * from './api-key.repository.mock'; export * from './api-key.repository.mock';
export * from './crypto.repository.mock'; export * from './crypto.repository.mock';
export * from './device-info.repository.mock';
export * from './fixtures'; export * from './fixtures';
export * from './job.repository.mock'; export * from './job.repository.mock';
export * from './shared-link.repository.mock'; export * from './shared-link.repository.mock';

View File

@ -0,0 +1,16 @@
import { IDeviceInfoRepository } from '@app/domain';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DeviceInfoEntity } from '../entities';
export class DeviceInfoRepository implements IDeviceInfoRepository {
constructor(@InjectRepository(DeviceInfoEntity) private repository: Repository<DeviceInfoEntity>) {}
get(userId: string, deviceId: string): Promise<DeviceInfoEntity | null> {
return this.repository.findOne({ where: { userId, deviceId } });
}
save(entity: Partial<DeviceInfoEntity>): Promise<DeviceInfoEntity> {
return this.repository.save(entity);
}
}

View File

@ -1,4 +1,6 @@
export * from './api-key.repository'; export * from './api-key.repository';
export * from './device-info.repository';
export * from './shared-link.repository'; export * from './shared-link.repository';
export * from './user.repository'; export * from './system-config.repository';
export * from './user-token.repository'; export * from './user-token.repository';
export * from './user.repository';

View File

@ -1,5 +1,6 @@
import { import {
ICryptoRepository, ICryptoRepository,
IDeviceInfoRepository,
IJobRepository, IJobRepository,
IKeyRepository, IKeyRepository,
ISharedLinkRepository, ISharedLinkRepository,
@ -7,20 +8,31 @@ import {
IUserRepository, IUserRepository,
QueueName, QueueName,
} from '@app/domain'; } from '@app/domain';
import { databaseConfig, UserEntity, UserTokenEntity } from './db'; import { IUserTokenRepository } from '@app/domain/user-token';
import { UserTokenRepository } from '@app/infra/db/repository/user-token.repository';
import { BullModule } from '@nestjs/bull'; import { BullModule } from '@nestjs/bull';
import { Global, Module, Provider } from '@nestjs/common'; import { Global, Module, Provider } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { APIKeyEntity, SharedLinkEntity, SystemConfigEntity, UserRepository } from './db';
import { APIKeyRepository, SharedLinkRepository } from './db/repository';
import { CryptoRepository } from './auth/crypto.repository'; import { CryptoRepository } from './auth/crypto.repository';
import { SystemConfigRepository } from './db/repository/system-config.repository'; import {
APIKeyEntity,
APIKeyRepository,
databaseConfig,
DeviceInfoEntity,
DeviceInfoRepository,
SharedLinkEntity,
SharedLinkRepository,
SystemConfigEntity,
SystemConfigRepository,
UserEntity,
UserRepository,
UserTokenEntity,
} from './db';
import { JobRepository } from './job'; import { JobRepository } from './job';
import { IUserTokenRepository } from '@app/domain/user-token';
import { UserTokenRepository } from '@app/infra/db/repository/user-token.repository';
const providers: Provider[] = [ const providers: Provider[] = [
{ provide: ICryptoRepository, useClass: CryptoRepository }, { provide: ICryptoRepository, useClass: CryptoRepository },
{ provide: IDeviceInfoRepository, useClass: DeviceInfoRepository },
{ provide: IKeyRepository, useClass: APIKeyRepository }, { provide: IKeyRepository, useClass: APIKeyRepository },
{ provide: IJobRepository, useClass: JobRepository }, { provide: IJobRepository, useClass: JobRepository },
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository }, { provide: ISharedLinkRepository, useClass: SharedLinkRepository },
@ -33,7 +45,14 @@ const providers: Provider[] = [
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forRoot(databaseConfig), TypeOrmModule.forRoot(databaseConfig),
TypeOrmModule.forFeature([APIKeyEntity, UserEntity, SharedLinkEntity, SystemConfigEntity, UserTokenEntity]), TypeOrmModule.forFeature([
APIKeyEntity,
DeviceInfoEntity,
UserEntity,
SharedLinkEntity,
SystemConfigEntity,
UserTokenEntity,
]),
BullModule.forRootAsync({ BullModule.forRootAsync({
useFactory: async () => ({ useFactory: async () => ({
prefix: 'immich_bull', prefix: 'immich_bull',