Ver Fonte

refactor(server): device info (#1490)

* refactor(server): device info

* fix: export device service

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
Jason Rasmussen há 2 anos atrás
pai
commit
bb84464216

+ 0 - 24
server/apps/immich/src/api-v1/device-info/device-info.controller.ts

@@ -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);
-  }
-}

+ 0 - 12
server/apps/immich/src/api-v1/device-info/device-info.module.ts

@@ -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 {}

+ 0 - 31
server/apps/immich/src/api-v1/device-info/device-info.service.ts

@@ -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);
-  }
-}

+ 2 - 3
server/apps/immich/src/app.module.ts

@@ -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,

+ 23 - 0
server/apps/immich/src/controllers/device-info.controller.ts

@@ -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);
+  }
+}

+ 1 - 0
server/apps/immich/src/controllers/index.ts

@@ -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';

+ 94 - 94
server/immich-openapi-specs.json

@@ -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": {
       "get": {
         "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": {
       "get": {
         "operationId": "getServerInfo",
@@ -2993,6 +2993,63 @@
           "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": {
         "type": "object",
         "properties": {
@@ -4265,63 +4322,6 @@
           "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": {
         "type": "object",
         "properties": {

+ 23 - 0
server/libs/domain/src/device-info/device-info.core.ts

@@ -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);
+  }
+}

+ 8 - 0
server/libs/domain/src/device-info/device-info.repository.ts

@@ -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>;
+}

+ 13 - 15
server/apps/immich/src/api-v1/device-info/device-info.service.spec.ts → server/libs/domain/src/device-info/device-info.service.spec.ts

@@ -1,5 +1,6 @@
 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';
 
 const deviceId = 'device-123';
@@ -7,13 +8,10 @@ const userId = 'user-123';
 
 describe('DeviceInfoService', () => {
   let sut: DeviceInfoService;
-  let repositoryMock: jest.Mocked<Repository<DeviceInfoEntity>>;
+  let repositoryMock: jest.Mocked<IDeviceInfoRepository>;
 
   beforeEach(async () => {
-    repositoryMock = {
-      findOne: jest.fn(),
-      save: jest.fn(),
-    } as unknown as jest.Mocked<Repository<DeviceInfoEntity>>;
+    repositoryMock = newDeviceInfoRepositoryMock();
 
     sut = new DeviceInfoService(repositoryMock);
   });
@@ -27,12 +25,12 @@ describe('DeviceInfoService', () => {
       const request = { deviceId, userId, deviceType: DeviceType.IOS } as DeviceInfoEntity;
       const response = { ...request, id: 1 } as DeviceInfoEntity;
 
-      repositoryMock.findOne.mockResolvedValue(null);
+      repositoryMock.get.mockResolvedValue(null);
       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);
     });
 
@@ -40,12 +38,12 @@ describe('DeviceInfoService', () => {
       const request = { deviceId, userId, deviceType: DeviceType.IOS, isAutoBackup: true } as DeviceInfoEntity;
       const response = { ...request, id: 1 } as DeviceInfoEntity;
 
-      repositoryMock.findOne.mockResolvedValue(response);
+      repositoryMock.get.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);
     });
 
@@ -53,12 +51,12 @@ describe('DeviceInfoService', () => {
       const request = { deviceId, userId } 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);
 
-      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);
     });
   });

+ 20 - 0
server/libs/domain/src/device-info/device-info.service.ts

@@ -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);
+  }
+}

+ 1 - 0
server/libs/domain/src/device-info/dto/index.ts

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

+ 1 - 1
server/apps/immich/src/api-v1/device-info/dto/upsert-device-info.dto.ts → server/libs/domain/src/device-info/dto/upsert-device-info.dto.ts

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

+ 4 - 0
server/libs/domain/src/device-info/index.ts

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

+ 1 - 1
server/apps/immich/src/api-v1/device-info/response-dto/device-info-response.dto.ts → server/libs/domain/src/device-info/response-dto/device-info-response.dto.ts

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

+ 1 - 0
server/libs/domain/src/device-info/response-dto/index.ts

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

+ 2 - 0
server/libs/domain/src/domain.module.ts

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

+ 1 - 0
server/libs/domain/src/index.ts

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

+ 8 - 0
server/libs/domain/test/device-info.repository.mock.ts

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

+ 1 - 0
server/libs/domain/test/index.ts

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

+ 16 - 0
server/libs/infra/src/db/repository/device-info.repository.ts

@@ -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);
+  }
+}

+ 3 - 1
server/libs/infra/src/db/repository/index.ts

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

+ 26 - 7
server/libs/infra/src/infra.module.ts

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