Selaa lähdekoodia

Set TypeScript to strict mode and fix issues related to server types (#261)

* Fix lint issues and some other TS issues

- set TypeScript in strict mode
- add npm commands to lint / check code
- fix all lint issues
- fix some TS issues
- rename User reponse DTO to make it consistent with the other ones
- override Express/User interface to use UserResponseDto interface
 This is for when the accessing the `user` from a Express Request,
 like in `asset-upload-config`

* Fix the rest of TS issues

- fix all the remaining TypeScript errors
- add missing `@types/mapbox__mapbox-sdk` package

* Move global.d.ts to server `src` folder

* Update AssetReponseDto duration type

This is now of type `string` that defaults to '0:00:00.00000' if not set
which is what the mobile app currently expects

* Set context when logging error in asset.service

Use `ServeFile` as the context for logging an error when
asset.resizePath is not set

* Fix wrong AppController merge conflict resolution

`redirectToWebpage` was removed in main as is no longer used.
Jaime Baez 3 vuotta sitten
vanhempi
commit
c918f5b001
64 muutettua tiedostoa jossa 415 lisäystä ja 273 poistoa
  1. 5 6
      server/apps/immich/src/api-v1/album/album-repository.ts
  2. 5 1
      server/apps/immich/src/api-v1/album/album.service.spec.ts
  3. 1 1
      server/apps/immich/src/api-v1/album/album.service.ts
  4. 1 1
      server/apps/immich/src/api-v1/album/dto/add-assets.dto.ts
  5. 1 1
      server/apps/immich/src/api-v1/album/dto/add-users.dto.ts
  6. 1 1
      server/apps/immich/src/api-v1/album/dto/create-album.dto.ts
  7. 1 1
      server/apps/immich/src/api-v1/album/dto/remove-assets.dto.ts
  8. 2 2
      server/apps/immich/src/api-v1/album/dto/update-album.dto.ts
  9. 2 2
      server/apps/immich/src/api-v1/album/response-dto/album-response.dto.ts
  10. 12 9
      server/apps/immich/src/api-v1/asset/asset.controller.ts
  11. 49 13
      server/apps/immich/src/api-v1/asset/asset.service.ts
  12. 8 8
      server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts
  13. 15 15
      server/apps/immich/src/api-v1/asset/dto/create-exif.dto.ts
  14. 1 1
      server/apps/immich/src/api-v1/asset/dto/delete-asset.dto.ts
  15. 2 2
      server/apps/immich/src/api-v1/asset/dto/get-all-asset-query.dto.ts
  16. 4 3
      server/apps/immich/src/api-v1/asset/dto/get-all-asset-response.dto.ts
  17. 1 1
      server/apps/immich/src/api-v1/asset/dto/get-asset.dto.ts
  18. 1 1
      server/apps/immich/src/api-v1/asset/dto/get-new-asset-query.dto.ts
  19. 1 1
      server/apps/immich/src/api-v1/asset/dto/search-asset.dto.ts
  20. 5 6
      server/apps/immich/src/api-v1/asset/dto/serve-file.dto.ts
  21. 2 2
      server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts
  22. 1 0
      server/apps/immich/src/api-v1/auth/auth.controller.ts
  23. 4 3
      server/apps/immich/src/api-v1/auth/auth.service.ts
  24. 2 2
      server/apps/immich/src/api-v1/auth/dto/login-credential.dto.ts
  25. 4 4
      server/apps/immich/src/api-v1/auth/dto/sign-up.dto.ts
  26. 7 7
      server/apps/immich/src/api-v1/communication/communication.gateway.ts
  27. 1 1
      server/apps/immich/src/api-v1/device-info/device-info.controller.ts
  28. 1 1
      server/apps/immich/src/api-v1/device-info/device-info.service.ts
  29. 3 3
      server/apps/immich/src/api-v1/device-info/dto/create-device-info.dto.ts
  30. 0 2
      server/apps/immich/src/api-v1/device-info/dto/update-device-info.dto.ts
  31. 8 7
      server/apps/immich/src/api-v1/server-info/dto/server-info.dto.ts
  32. 4 4
      server/apps/immich/src/api-v1/user/dto/create-user.dto.ts
  33. 2 2
      server/apps/immich/src/api-v1/user/response-dto/user-response.dto.ts
  34. 0 2
      server/apps/immich/src/api-v1/user/user.controller.ts
  35. 17 3
      server/apps/immich/src/api-v1/user/user.service.ts
  36. 2 5
      server/apps/immich/src/app.controller.ts
  37. 3 2
      server/apps/immich/src/app.module.ts
  38. 11 6
      server/apps/immich/src/config/asset-upload.config.ts
  39. 8 2
      server/apps/immich/src/config/profile-image-upload.config.ts
  40. 6 6
      server/apps/immich/src/decorators/auth-user.decorator.ts
  41. 8 0
      server/apps/immich/src/global.d.ts
  42. 7 1
      server/apps/immich/src/middlewares/admin-role-guard.middleware.ts
  43. 1 1
      server/apps/immich/src/middlewares/app-logger.middleware.ts
  44. 13 11
      server/apps/immich/src/modules/background-task/background-task.processor.ts
  45. 8 3
      server/apps/immich/src/modules/immich-jwt/immich-jwt.service.ts
  46. 0 1
      server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts
  47. 2 2
      server/apps/immich/test/user.e2e-spec.ts
  48. 0 2
      server/apps/microservices/src/microservices.module.ts
  49. 1 1
      server/apps/microservices/src/processors/asset-uploaded.processor.ts
  50. 5 6
      server/apps/microservices/src/processors/metadata-extraction.processor.ts
  51. 8 5
      server/apps/microservices/src/processors/thumbnail.processor.ts
  52. 1 1
      server/apps/microservices/src/processors/video-transcode.processor.ts
  53. 1 1
      server/apps/microservices/test/app.e2e-spec.ts
  54. 8 8
      server/libs/database/src/entities/album.entity.ts
  55. 5 5
      server/libs/database/src/entities/asset-album.entity.ts
  56. 22 22
      server/libs/database/src/entities/asset.entity.ts
  57. 8 8
      server/libs/database/src/entities/device-info.entity.ts
  58. 34 34
      server/libs/database/src/entities/exif.entity.ts
  59. 5 5
      server/libs/database/src/entities/smart-info.entity.ts
  60. 5 5
      server/libs/database/src/entities/user-album.entity.ts
  61. 10 10
      server/libs/database/src/entities/user.entity.ts
  62. 53 0
      server/package-lock.json
  63. 5 1
      server/package.json
  64. 1 0
      server/tsconfig.json

+ 5 - 6
server/apps/immich/src/api-v1/album/album-repository.ts

@@ -14,7 +14,7 @@ import { UpdateAlbumDto } from './dto/update-album.dto';
 export interface IAlbumRepository {
   create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>;
   getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]>;
-  get(albumId: string): Promise<AlbumEntity>;
+  get(albumId: string): Promise<AlbumEntity | undefined>;
   delete(album: AlbumEntity): Promise<void>;
   addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity>;
   removeUser(album: AlbumEntity, userId: string): Promise<void>;
@@ -39,7 +39,7 @@ export class AlbumRepository implements IAlbumRepository {
   ) {}
 
   async create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity> {
-    return await getConnection().transaction(async (transactionalEntityManager) => {
+    return getConnection().transaction(async (transactionalEntityManager) => {
       // Create album entity
       const newAlbum = new AlbumEntity();
       newAlbum.ownerId = ownerId;
@@ -80,7 +80,6 @@ export class AlbumRepository implements IAlbumRepository {
 
       return album;
     });
-    return;
   }
 
   getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]> {
@@ -155,7 +154,7 @@ export class AlbumRepository implements IAlbumRepository {
       return;
     }
     // TODO: sort in query
-    const sortedSharedAsset = album.assets.sort(
+    const sortedSharedAsset = album.assets?.sort(
       (a, b) => new Date(a.assetInfo.createdAt).valueOf() - new Date(b.assetInfo.createdAt).valueOf(),
     );
 
@@ -180,7 +179,7 @@ export class AlbumRepository implements IAlbumRepository {
     }
 
     await this.userAlbumRepository.save([...newRecords]);
-    return this.get(album.id);
+    return this.get(album.id) as Promise<AlbumEntity>; // There is an album for sure
   }
 
   async removeUser(album: AlbumEntity, userId: string): Promise<void> {
@@ -217,7 +216,7 @@ export class AlbumRepository implements IAlbumRepository {
     }
 
     await this.assetAlbumRepository.save([...newRecords]);
-    return this.get(album.id);
+    return this.get(album.id) as Promise<AlbumEntity>; // There is an album for sure
   }
 
   updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity> {

+ 5 - 1
server/apps/immich/src/api-v1/album/album.service.spec.ts

@@ -25,6 +25,7 @@ describe('Album service', () => {
     albumEntity.createdAt = 'date';
     albumEntity.sharedUsers = [];
     albumEntity.assets = [];
+    albumEntity.albumThumbnailAssetId = null;
 
     return albumEntity;
   };
@@ -36,6 +37,7 @@ describe('Album service', () => {
     albumEntity.albumName = 'name';
     albumEntity.createdAt = 'date';
     albumEntity.assets = [];
+    albumEntity.albumThumbnailAssetId = null;
     albumEntity.sharedUsers = [
       {
         id: '99',
@@ -60,6 +62,7 @@ describe('Album service', () => {
     albumEntity.albumName = 'name';
     albumEntity.createdAt = 'date';
     albumEntity.assets = [];
+    albumEntity.albumThumbnailAssetId = null;
     albumEntity.sharedUsers = [
       {
         id: '99',
@@ -96,6 +99,7 @@ describe('Album service', () => {
     albumEntity.createdAt = 'date';
     albumEntity.sharedUsers = [];
     albumEntity.assets = [];
+    albumEntity.albumThumbnailAssetId = null;
 
     return albumEntity;
   };
@@ -151,7 +155,7 @@ describe('Album service', () => {
 
     const expectedResult: AlbumResponseDto = {
       albumName: 'name',
-      albumThumbnailAssetId: undefined,
+      albumThumbnailAssetId: null,
       createdAt: 'date',
       id: '0001',
       ownerId,

+ 1 - 1
server/apps/immich/src/api-v1/album/album.service.ts

@@ -31,7 +31,7 @@ export class AlbumService {
 
     if (validateIsOwner && !isOwner) {
       throw new ForbiddenException('Unauthorized Album Access');
-    } else if (!isOwner && !album.sharedUsers.some((user) => user.sharedUserId == authUser.id)) {
+    } else if (!isOwner && !album.sharedUsers?.some((user) => user.sharedUserId == authUser.id)) {
       throw new ForbiddenException('Unauthorized Album Access');
     }
     return album;

+ 1 - 1
server/apps/immich/src/api-v1/album/dto/add-assets.dto.ts

@@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator';
 
 export class AddAssetsDto {
   @IsNotEmpty()
-  assetIds: string[];
+  assetIds!: string[];
 }

+ 1 - 1
server/apps/immich/src/api-v1/album/dto/add-users.dto.ts

@@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator';
 
 export class AddUsersDto {
   @IsNotEmpty()
-  sharedUserIds: string[];
+  sharedUserIds!: string[];
 }

+ 1 - 1
server/apps/immich/src/api-v1/album/dto/create-album.dto.ts

@@ -2,7 +2,7 @@ import { IsNotEmpty, IsOptional } from 'class-validator';
 
 export class CreateAlbumDto {
   @IsNotEmpty()
-  albumName: string;
+  albumName!: string;
 
   @IsOptional()
   sharedWithUserIds?: string[];

+ 1 - 1
server/apps/immich/src/api-v1/album/dto/remove-assets.dto.ts

@@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator';
 
 export class RemoveAssetsDto {
   @IsNotEmpty()
-  assetIds: string[];
+  assetIds!: string[];
 }

+ 2 - 2
server/apps/immich/src/api-v1/album/dto/update-album.dto.ts

@@ -2,8 +2,8 @@ import { IsNotEmpty } from 'class-validator';
 
 export class UpdateAlbumDto {
   @IsNotEmpty()
-  albumName: string;
+  albumName!: string;
 
   @IsNotEmpty()
-  ownerId: string;
+  ownerId!: string;
 }

+ 2 - 2
server/apps/immich/src/api-v1/album/response-dto/album-response.dto.ts

@@ -1,5 +1,5 @@
 import { AlbumEntity } from '../../../../../../libs/database/src/entities/album.entity';
-import { User, mapUser } from '../../user/response-dto/user';
+import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto';
 import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto';
 
 export interface AlbumResponseDto {
@@ -9,7 +9,7 @@ export interface AlbumResponseDto {
   createdAt: string;
   albumThumbnailAssetId: string | null;
   shared: boolean;
-  sharedUsers: User[];
+  sharedUsers: UserResponseDto[];
   assets: AssetResponseDto[];
 }
 

+ 12 - 9
server/apps/immich/src/api-v1/asset/asset.controller.ts

@@ -14,7 +14,6 @@ import {
   Headers,
   Delete,
   Logger,
-  Patch,
   HttpCode,
 } from '@nestjs/common';
 import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
@@ -25,9 +24,7 @@ import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
 import { CreateAssetDto } from './dto/create-asset.dto';
 import { ServeFileDto } from './dto/serve-file.dto';
 import { AssetEntity } from '@app/database/entities/asset.entity';
-import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
 import { Response as Res } from 'express';
-import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto';
 import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
 import { DeleteAssetDto } from './dto/delete-asset.dto';
 import { SearchAssetDto } from './dto/search-asset.dto';
@@ -58,15 +55,18 @@ export class AssetController {
     ),
   )
   async uploadFile(
-    @GetAuthUser() authUser,
+    @GetAuthUser() authUser: AuthUserDto,
     @UploadedFiles() uploadFiles: { assetData: Express.Multer.File[]; thumbnailData?: Express.Multer.File[] },
     @Body(ValidationPipe) assetInfo: CreateAssetDto,
-  ) {
+  ): Promise<'ok' | undefined> {
     for (const file of uploadFiles.assetData) {
       try {
         const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
 
-        if (uploadFiles.thumbnailData != null && savedAsset) {
+        if (!savedAsset) {
+          return;
+        }
+        if (uploadFiles.thumbnailData != null) {
           const assetWithThumbnail = await this.assetService.updateThumbnailInfo(
             savedAsset,
             uploadFiles.thumbnailData[0].path,
@@ -107,11 +107,11 @@ export class AssetController {
 
   @Get('/file')
   async serveFile(
-    @Headers() headers,
+    @Headers() headers: Record<string, string>,
     @GetAuthUser() authUser: AuthUserDto,
     @Response({ passthrough: true }) res: Res,
     @Query(ValidationPipe) query: ServeFileDto,
-  ): Promise<StreamableFile> {
+  ): Promise<StreamableFile | undefined> {
     return this.assetService.serveFile(authUser, query, res, headers);
   }
 
@@ -151,7 +151,7 @@ export class AssetController {
   }
 
   @Get('/assetById/:assetId')
-  async getAssetById(@GetAuthUser() authUser: AuthUserDto, @Param('assetId') assetId) {
+  async getAssetById(@GetAuthUser() authUser: AuthUserDto, @Param('assetId') assetId: string) {
     return await this.assetService.getAssetById(authUser, assetId);
   }
 
@@ -161,6 +161,9 @@ export class AssetController {
 
     for (const id of assetIds.ids) {
       const assets = await this.assetService.getAssetById(authUser, id);
+      if (!assets) {
+        continue;
+      }
       deleteAssetList.push(assets);
     }
 

+ 49 - 13
server/apps/immich/src/api-v1/asset/asset.service.ts

@@ -1,10 +1,16 @@
-import { BadRequestException, Injectable, InternalServerErrorException, Logger, StreamableFile } from '@nestjs/common';
+import {
+  BadRequestException,
+  Injectable,
+  InternalServerErrorException,
+  Logger,
+  NotFoundException,
+  StreamableFile,
+} from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import { IsNull, Not, Repository } from 'typeorm';
 import { AuthUserDto } from '../../decorators/auth-user.decorator';
 import { CreateAssetDto } from './dto/create-asset.dto';
 import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
-import _ from 'lodash';
 import { createReadStream, stat } from 'fs';
 import { ServeFileDto } from './dto/serve-file.dto';
 import { Response as Res } from 'express';
@@ -33,7 +39,12 @@ export class AssetService {
     return updatedAsset.raw[0];
   }
 
-  public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) {
+  public async createUserAsset(
+    authUser: AuthUserDto,
+    assetInfo: CreateAssetDto,
+    path: string,
+    mimeType: string,
+  ): Promise<AssetEntity | undefined> {
     const asset = new AssetEntity();
     asset.deviceAssetId = assetInfo.deviceAssetId;
     asset.userId = authUser.id;
@@ -44,10 +55,14 @@ export class AssetService {
     asset.modifiedAt = assetInfo.modifiedAt;
     asset.isFavorite = assetInfo.isFavorite;
     asset.mimeType = mimeType;
-    asset.duration = assetInfo.duration;
+    asset.duration = assetInfo.duration || null;
 
     try {
-      return await this.assetRepository.save(asset);
+      const createdAsset = await this.assetRepository.save(asset);
+      if (!createdAsset) {
+        throw new Error('Asset not created');
+      }
+      return createdAsset;
     } catch (e) {
       Logger.error(`Error Create New Asset ${e}`, 'createUserAsset');
     }
@@ -62,7 +77,7 @@ export class AssetService {
       select: ['deviceAssetId'],
     });
 
-    const res = [];
+    const res: string[] = [];
     rows.forEach((v) => res.push(v.deviceAssetId));
     return res;
   }
@@ -119,6 +134,9 @@ export class AssetService {
         });
         file = createReadStream(asset.originalPath);
       } else {
+        if (!asset.resizePath) {
+          throw new Error('resizePath not set');
+        }
         const { size } = await fileInfo(asset.resizePath);
         res.set({
           'Content-Type': 'image/jpeg',
@@ -134,16 +152,25 @@ export class AssetService {
     }
   }
 
-  public async getAssetThumbnail(assetId: string) {
+  public async getAssetThumbnail(assetId: string): Promise<StreamableFile> {
     try {
       const asset = await this.assetRepository.findOne({ id: assetId });
+      if (!asset) {
+        throw new NotFoundException('Asset not found');
+      }
 
       if (asset.webpPath && asset.webpPath.length > 0) {
         return new StreamableFile(createReadStream(asset.webpPath));
       } else {
+        if (!asset.resizePath) {
+          throw new Error('resizePath not set');
+        }
         return new StreamableFile(createReadStream(asset.resizePath));
       }
     } catch (e) {
+      if (e instanceof NotFoundException) {
+        throw e;
+      }
       Logger.error('Error serving asset thumbnail ', e);
       throw new InternalServerErrorException('Failed to serve asset thumbnail', 'GetAssetThumbnail');
     }
@@ -154,6 +181,7 @@ export class AssetService {
     const asset = await this.findOne(query.did, query.aid);
 
     if (!asset) {
+      // TODO: maybe this should be a NotFoundException?
       throw new BadRequestException('Asset does not exist');
     }
 
@@ -166,6 +194,10 @@ export class AssetService {
         res.set({
           'Content-Type': 'image/jpeg',
         });
+        if (!asset.resizePath) {
+          Logger.error('Error serving IMAGE asset for web', 'ServeFile');
+          throw new InternalServerErrorException(`Failed to serve image asset for web`, 'ServeFile');
+        }
         return new StreamableFile(createReadStream(asset.resizePath));
       }
 
@@ -189,6 +221,9 @@ export class AssetService {
             res.set({
               'Content-Type': 'image/jpeg',
             });
+            if (!asset.resizePath) {
+              throw new Error('resizePath not set');
+            }
             file = createReadStream(asset.resizePath);
           }
         }
@@ -297,6 +332,7 @@ export class AssetService {
 
   async getAssetSearchTerm(authUser: AuthUserDto): Promise<string[]> {
     const possibleSearchTerm = new Set<string>();
+    // TODO: should use query builder
     const rows = await this.assetRepository.query(
       `
       select distinct si.tags, si.objects, e.orientation, e."lensModel", e.make, e.model , a.type, e.city, e.state, e.country
@@ -308,12 +344,12 @@ export class AssetService {
       [authUser.id],
     );
 
-    rows.forEach((row) => {
+    rows.forEach((row: { [x: string]: any }) => {
       // tags
-      row['tags']?.map((tag) => possibleSearchTerm.add(tag?.toLowerCase()));
+      row['tags']?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase()));
 
       // objects
-      row['objects']?.map((object) => possibleSearchTerm.add(object?.toLowerCase()));
+      row['objects']?.map((object: string) => possibleSearchTerm.add(object?.toLowerCase()));
 
       // asset's tyoe
       possibleSearchTerm.add(row['type']?.toLowerCase());
@@ -345,7 +381,7 @@ export class AssetService {
              LEFT JOIN exif e ON a.id = e."assetId"
 
     WHERE a."userId" = $1
-       AND 
+       AND
        (
          TO_TSVECTOR('english', ARRAY_TO_STRING(si.tags, ',')) @@ PLAINTO_TSQUERY('english', $2) OR
          TO_TSVECTOR('english', ARRAY_TO_STRING(si.objects, ',')) @@ PLAINTO_TSQUERY('english', $2) OR
@@ -362,7 +398,7 @@ export class AssetService {
         select distinct on (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId"
         from assets a
         left join exif e on a.id = e."assetId"
-        where a."userId" = $1 
+        where a."userId" = $1
         and e.city is not null
         and a.type = 'IMAGE';
       `,
@@ -376,7 +412,7 @@ export class AssetService {
         select distinct on (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId"
         from assets a
         left join smart_info si on a.id = si."assetId"
-        where a."userId" = $1 
+        where a."userId" = $1
         and si.objects is not null
       `,
       [authUser.id],

+ 8 - 8
server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts

@@ -3,26 +3,26 @@ import { AssetType } from '@app/database/entities/asset.entity';
 
 export class CreateAssetDto {
   @IsNotEmpty()
-  deviceAssetId: string;
+  deviceAssetId!: string;
 
   @IsNotEmpty()
-  deviceId: string;
+  deviceId!: string;
 
   @IsNotEmpty()
-  assetType: AssetType;
+  assetType!: AssetType;
 
   @IsNotEmpty()
-  createdAt: string;
+  createdAt!: string;
 
   @IsNotEmpty()
-  modifiedAt: string;
+  modifiedAt!: string;
 
   @IsNotEmpty()
-  isFavorite: boolean;
+  isFavorite!: boolean;
 
   @IsNotEmpty()
-  fileExtension: string;
+  fileExtension!: string;
 
   @IsOptional()
-  duration: string;
+  duration?: string;
 }

+ 15 - 15
server/apps/immich/src/api-v1/asset/dto/create-exif.dto.ts

@@ -2,47 +2,47 @@ import { IsNotEmpty, IsOptional } from 'class-validator';
 
 export class CreateExifDto {
   @IsNotEmpty()
-  assetId: string;
+  assetId!: string;
 
   @IsOptional()
-  make: string;
+  make?: string;
 
   @IsOptional()
-  model: string;
+  model?: string;
 
   @IsOptional()
-  imageName: string;
+  imageName?: string;
 
   @IsOptional()
-  exifImageWidth: number;
+  exifImageWidth?: number;
 
   @IsOptional()
-  exifImageHeight: number;
+  exifImageHeight?: number;
 
   @IsOptional()
-  fileSizeInByte: number;
+  fileSizeInByte?: number;
 
   @IsOptional()
-  orientation: string;
+  orientation?: string;
 
   @IsOptional()
-  dateTimeOriginal: Date;
+  dateTimeOriginal?: Date;
 
   @IsOptional()
-  modifiedDate: Date;
+  modifiedDate?: Date;
 
   @IsOptional()
-  lensModel: string;
+  lensModel?: string;
 
   @IsOptional()
-  fNumber: number;
+  fNumber?: number;
 
   @IsOptional()
-  focalLenght: number;
+  focalLenght?: number;
 
   @IsOptional()
-  iso: number;
+  iso?: number;
 
   @IsOptional()
-  exposureTime: number;
+  exposureTime?: number;
 }

+ 1 - 1
server/apps/immich/src/api-v1/asset/dto/delete-asset.dto.ts

@@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator';
 
 export class DeleteAssetDto {
   @IsNotEmpty()
-  ids: string[];
+  ids!: string[];
 }

+ 2 - 2
server/apps/immich/src/api-v1/asset/dto/get-all-asset-query.dto.ts

@@ -1,6 +1,6 @@
-import { IsNotEmpty, IsOptional } from 'class-validator';
+import { IsOptional } from 'class-validator';
 
 export class GetAllAssetQueryDto {
   @IsOptional()
-  nextPageKey: string;
+  nextPageKey?: string;
 }

+ 4 - 3
server/apps/immich/src/api-v1/asset/dto/get-all-asset-response.dto.ts

@@ -1,7 +1,8 @@
 import { AssetEntity } from '@app/database/entities/asset.entity';
 
+// TODO: this doesn't seem to be used
 export class GetAllAssetReponseDto {
-  data: Array<{ date: string; assets: Array<AssetEntity> }>;
-  count: number;
-  nextPageKey: string;
+  data!: Array<{ date: string; assets: Array<AssetEntity> }>;
+  count!: number;
+  nextPageKey!: string;
 }

+ 1 - 1
server/apps/immich/src/api-v1/asset/dto/get-asset.dto.ts

@@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator';
 
 export class GetAssetDto {
   @IsNotEmpty()
-  deviceId: string;
+  deviceId!: string;
 }

+ 1 - 1
server/apps/immich/src/api-v1/asset/dto/get-new-asset-query.dto.ts

@@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator';
 
 export class GetNewAssetQueryDto {
   @IsNotEmpty()
-  latestDate: string;
+  latestDate!: string;
 }

+ 1 - 1
server/apps/immich/src/api-v1/asset/dto/search-asset.dto.ts

@@ -2,5 +2,5 @@ import { IsNotEmpty } from 'class-validator';
 
 export class SearchAssetDto {
   @IsNotEmpty()
-  searchTerm: string;
+  searchTerm!: string;
 }

+ 5 - 6
server/apps/immich/src/api-v1/asset/dto/serve-file.dto.ts

@@ -1,20 +1,19 @@
-import { Transform } from 'class-transformer';
-import { IsBoolean, IsBooleanString, IsNotEmpty, IsOptional } from 'class-validator';
+import { IsBooleanString, IsNotEmpty, IsOptional } from 'class-validator';
 
 export class ServeFileDto {
   //assetId
   @IsNotEmpty()
-  aid: string;
+  aid!: string;
 
   //deviceId
   @IsNotEmpty()
-  did: string;
+  did!: string;
 
   @IsOptional()
   @IsBooleanString()
-  isThumb: string;
+  isThumb?: string;
 
   @IsOptional()
   @IsBooleanString()
-  isWeb: string;
+  isWeb?: string;
 }

+ 2 - 2
server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts

@@ -14,7 +14,7 @@ export interface AssetResponseDto {
   modifiedAt: string;
   isFavorite: boolean;
   mimeType: string | null;
-  duration: string | null;
+  duration: string;
   exifInfo?: ExifResponseDto;
   smartInfo?: SmartInfoResponseDto;
 }
@@ -32,7 +32,7 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto {
     modifiedAt: entity.modifiedAt,
     isFavorite: entity.isFavorite,
     mimeType: entity.mimeType,
-    duration: entity.duration,
+    duration: entity.duration ?? '0:00:00.00000',
     exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
     smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
   };

+ 1 - 0
server/apps/immich/src/api-v1/auth/auth.controller.ts

@@ -21,6 +21,7 @@ export class AuthController {
 
   @UseGuards(JwtAuthGuard)
   @Post('/validateToken')
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
   async validateToken(@GetAuthUser() authUser: AuthUserDto) {
     return {
       authStatus: true,

+ 4 - 3
server/apps/immich/src/api-v1/auth/auth.service.ts

@@ -7,7 +7,7 @@ import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
 import { JwtPayloadDto } from './dto/jwt-payload.dto';
 import { SignUpDto } from './dto/sign-up.dto';
 import * as bcrypt from 'bcrypt';
-import { mapUser, User } from '../user/response-dto/user';
+import { mapUser, UserResponseDto } from '../user/response-dto/user-response.dto';
 
 @Injectable()
 export class AuthService {
@@ -39,7 +39,8 @@ export class AuthService {
       return null;
     }
 
-    const isAuthenticated = await this.validatePassword(user.password, loginCredential.password, user.salt);
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const isAuthenticated = await this.validatePassword(user.password!, loginCredential.password, user.salt!);
 
     if (isAuthenticated) {
       return user;
@@ -69,7 +70,7 @@ export class AuthService {
     };
   }
 
-  public async adminSignUp(signUpCredential: SignUpDto): Promise<User> {
+  public async adminSignUp(signUpCredential: SignUpDto): Promise<UserResponseDto> {
     const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } });
 
     if (adminUser) {

+ 2 - 2
server/apps/immich/src/api-v1/auth/dto/login-credential.dto.ts

@@ -2,8 +2,8 @@ import { IsNotEmpty } from 'class-validator';
 
 export class LoginCredentialDto {
   @IsNotEmpty()
-  email: string;
+  email!: string;
 
   @IsNotEmpty()
-  password: string;
+  password!: string;
 }

+ 4 - 4
server/apps/immich/src/api-v1/auth/dto/sign-up.dto.ts

@@ -2,14 +2,14 @@ import { IsNotEmpty } from 'class-validator';
 
 export class SignUpDto {
   @IsNotEmpty()
-  email: string;
+  email!: string;
 
   @IsNotEmpty()
-  password: string;
+  password!: string;
 
   @IsNotEmpty()
-  firstName: string;
+  firstName!: string;
 
   @IsNotEmpty()
-  lastName: string;
+  lastName!: string;
 }

+ 7 - 7
server/apps/immich/src/api-v1/communication/communication.gateway.ts

@@ -1,12 +1,10 @@
 import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
-import { CommunicationService } from './communication.service';
 import { Socket, Server } from 'socket.io';
-import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
+import { ImmichJwtService, JwtValidationResult } from '../../modules/immich-jwt/immich-jwt.service';
 import { Logger } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import { UserEntity } from '@app/database/entities/user.entity';
 import { Repository } from 'typeorm';
-import { query } from 'express';
 
 @WebSocketGateway({ cors: true })
 export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisconnect {
@@ -17,7 +15,7 @@ export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisco
     private userRepository: Repository<UserEntity>,
   ) {}
 
-  @WebSocketServer() server: Server;
+  @WebSocketServer() server!: Server;
 
   handleDisconnect(client: Socket) {
     client.leave(client.nsp.name);
@@ -25,13 +23,15 @@ export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisco
     Logger.log(`Client ${client.id} disconnected from Websocket`, 'WebsocketConnectionEvent');
   }
 
-  async handleConnection(client: Socket, ...args: any[]) {
+  async handleConnection(client: Socket) {
     try {
       Logger.log(`New websocket connection: ${client.id}`, 'WebsocketConnectionEvent');
 
-      const accessToken = client.handshake.headers.authorization.split(' ')[1];
+      const accessToken = client.handshake.headers.authorization?.split(' ')[1];
 
-      const res = await this.immichJwtService.validateToken(accessToken);
+      const res: JwtValidationResult = accessToken
+        ? await this.immichJwtService.validateToken(accessToken)
+        : { status: false, userId: null };
 
       if (!res.status) {
         client.emit('error', 'unauthorized');

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

@@ -1,4 +1,4 @@
-import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, ValidationPipe } from '@nestjs/common';
+import { Controller, Post, Body, Patch, UseGuards, ValidationPipe } from '@nestjs/common';
 import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
 import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
 import { DeviceInfoService } from './device-info.service';

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

@@ -1,4 +1,4 @@
-import { BadRequestException, HttpCode, Injectable, Logger, Res } from '@nestjs/common';
+import { BadRequestException, Injectable, Logger } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import { Repository } from 'typeorm';
 import { AuthUserDto } from '../../decorators/auth-user.decorator';

+ 3 - 3
server/apps/immich/src/api-v1/device-info/dto/create-device-info.dto.ts

@@ -3,11 +3,11 @@ import { DeviceType } from '@app/database/entities/device-info.entity';
 
 export class CreateDeviceInfoDto {
   @IsNotEmpty()
-  deviceId: string;
+  deviceId!: string;
 
   @IsNotEmpty()
-  deviceType: DeviceType;
+  deviceType!: DeviceType;
 
   @IsOptional()
-  isAutoBackup: boolean;
+  isAutoBackup?: boolean;
 }

+ 0 - 2
server/apps/immich/src/api-v1/device-info/dto/update-device-info.dto.ts

@@ -1,6 +1,4 @@
 import { PartialType } from '@nestjs/mapped-types';
-import { IsOptional } from 'class-validator';
-import { DeviceType } from '@app/database/entities/device-info.entity';
 import { CreateDeviceInfoDto } from './create-device-info.dto';
 
 export class UpdateDeviceInfoDto extends PartialType(CreateDeviceInfoDto) {}

+ 8 - 7
server/apps/immich/src/api-v1/server-info/dto/server-info.dto.ts

@@ -1,9 +1,10 @@
+// TODO: this is being used as a response DTO. Should be changed to interface
 export class ServerInfoDto {
-  diskSize: string;
-  diskUse: string;
-  diskAvailable: string;
-  diskSizeRaw: number;
-  diskUseRaw: number;
-  diskAvailableRaw: number;
-  diskUsagePercentage: number;
+  diskSize!: string;
+  diskUse!: string;
+  diskAvailable!: string;
+  diskSizeRaw!: number;
+  diskUseRaw!: number;
+  diskAvailableRaw!: number;
+  diskUsagePercentage!: number;
 }

+ 4 - 4
server/apps/immich/src/api-v1/user/dto/create-user.dto.ts

@@ -2,16 +2,16 @@ import { IsNotEmpty, IsOptional } from 'class-validator';
 
 export class CreateUserDto {
   @IsNotEmpty()
-  email: string;
+  email!: string;
 
   @IsNotEmpty()
-  password: string;
+  password!: string;
 
   @IsNotEmpty()
-  firstName: string;
+  firstName!: string;
 
   @IsNotEmpty()
-  lastName: string;
+  lastName!: string;
 
   @IsOptional()
   profileImagePath?: string;

+ 2 - 2
server/apps/immich/src/api-v1/user/response-dto/user.ts → server/apps/immich/src/api-v1/user/response-dto/user-response.dto.ts

@@ -1,6 +1,6 @@
 import { UserEntity } from '../../../../../../libs/database/src/entities/user.entity';
 
-export interface User {
+export interface UserResponseDto {
   id: string;
   email: string;
   firstName: string;
@@ -8,7 +8,7 @@ export interface User {
   createdAt: string;
 }
 
-export function mapUser(entity: UserEntity): User {
+export function mapUser(entity: UserEntity): UserResponseDto {
   return {
     id: entity.id,
     email: entity.email,

+ 0 - 2
server/apps/immich/src/api-v1/user/user.controller.ts

@@ -3,9 +3,7 @@ import {
   Get,
   Post,
   Body,
-  Patch,
   Param,
-  Delete,
   UseGuards,
   ValidationPipe,
   Put,

+ 17 - 3
server/apps/immich/src/api-v1/user/user.service.ts

@@ -1,4 +1,11 @@
-import { BadRequestException, Injectable, InternalServerErrorException, Logger, StreamableFile } from '@nestjs/common';
+import {
+  BadRequestException,
+  Injectable,
+  InternalServerErrorException,
+  Logger,
+  NotFoundException,
+  StreamableFile,
+} from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import { Not, Repository } from 'typeorm';
 import { AuthUserDto } from '../../decorators/auth-user.decorator';
@@ -8,7 +15,7 @@ import { UserEntity } from '@app/database/entities/user.entity';
 import * as bcrypt from 'bcrypt';
 import { createReadStream } from 'fs';
 import { Response as Res } from 'express';
-import { mapUser, User } from './response-dto/user';
+import { mapUser, UserResponseDto } from './response-dto/user-response.dto';
 
 @Injectable()
 export class UserService {
@@ -44,7 +51,7 @@ export class UserService {
     };
   }
 
-  async createUser(createUserDto: CreateUserDto): Promise<User> {
+  async createUser(createUserDto: CreateUserDto): Promise<UserResponseDto> {
     const user = await this.userRepository.findOne({ where: { email: createUserDto.email } });
 
     if (user) {
@@ -75,6 +82,9 @@ export class UserService {
 
   async updateUser(updateUserDto: UpdateUserDto) {
     const user = await this.userRepository.findOne(updateUserDto.id);
+    if (!user) {
+      throw new NotFoundException('User not found');
+    }
 
     user.lastName = updateUserDto.lastName || user.lastName;
     user.firstName = updateUserDto.firstName || user.firstName;
@@ -100,6 +110,7 @@ export class UserService {
     try {
       const updatedUser = await this.userRepository.save(user);
 
+      // TODO: this should probably retrun UserResponseDto
       return {
         id: updatedUser.id,
         email: updatedUser.email,
@@ -133,6 +144,9 @@ export class UserService {
   async getUserProfileImage(userId: string, res: Res) {
     try {
       const user = await this.userRepository.findOne({ id: userId });
+      if (!user) {
+        throw new NotFoundException('User not found');
+      }
 
       if (!user.profileImagePath) {
         // throw new BadRequestException('User does not have a profile image');

+ 2 - 5
server/apps/immich/src/app.controller.ts

@@ -1,6 +1,3 @@
-import { Controller, Get, Res, Headers } from '@nestjs/common';
-import { Response } from 'express';
+import { Controller } from '@nestjs/common';
 @Controller()
-export class AppController {
-  constructor() {}
-}
+export class AppController {}

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

@@ -4,8 +4,7 @@ import { AssetModule } from './api-v1/asset/asset.module';
 import { AuthModule } from './api-v1/auth/auth.module';
 import { ImmichJwtModule } from './modules/immich-jwt/immich-jwt.module';
 import { DeviceInfoModule } from './api-v1/device-info/device-info.module';
-import { AppLoggerMiddleware } from './middlewares/app-logger.middleware';
-import { ConfigModule, ConfigService } from '@nestjs/config';
+import { ConfigModule } from '@nestjs/config';
 import { immichAppConfig } from './config/app.config';
 import { BullModule } from '@nestjs/bull';
 import { ServerInfoModule } from './api-v1/server-info/server-info.module';
@@ -57,6 +56,8 @@ import { DatabaseModule } from '@app/database';
   providers: [],
 })
 export class AppModule implements NestModule {
+  // TODO: check if consumer is needed or remove
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
   configure(consumer: MiddlewareConsumer): void {
     if (process.env.NODE_ENV == 'development') {
       // consumer.apply(AppLoggerMiddleware).forRoutes('*');

+ 11 - 6
server/apps/immich/src/config/asset-upload.config.ts

@@ -6,7 +6,7 @@ import { extname } from 'path';
 import { Request } from 'express';
 import { APP_UPLOAD_LOCATION } from '../constants/upload_location.constant';
 import { randomUUID } from 'crypto';
-import { CreateAssetDto } from '../api-v1/asset/dto/create-asset.dto';
+// import { CreateAssetDto } from '../api-v1/asset/dto/create-asset.dto';
 
 export const assetUploadOption: MulterOptions = {
   fileFilter: (req: Request, file: any, cb: any) => {
@@ -20,13 +20,18 @@ export const assetUploadOption: MulterOptions = {
   storage: diskStorage({
     destination: (req: Request, file: Express.Multer.File, cb: any) => {
       const basePath = APP_UPLOAD_LOCATION;
-      const fileInfo = req.body as CreateAssetDto;
+      // TODO these are currently not used. Shall we remove them?
+      // const fileInfo = req.body as CreateAssetDto;
 
-      const yearInfo = new Date(fileInfo.createdAt).getFullYear();
-      const monthInfo = new Date(fileInfo.createdAt).getMonth();
+      // const yearInfo = new Date(fileInfo.createdAt).getFullYear();
+      // const monthInfo = new Date(fileInfo.createdAt).getMonth();
+
+      if (!req.user) {
+        return;
+      }
 
       if (file.fieldname == 'assetData') {
-        const originalUploadFolder = `${basePath}/${req.user['id']}/original/${req.body['deviceId']}`;
+        const originalUploadFolder = `${basePath}/${req.user.id}/original/${req.body['deviceId']}`;
 
         if (!existsSync(originalUploadFolder)) {
           mkdirSync(originalUploadFolder, { recursive: true });
@@ -35,7 +40,7 @@ export const assetUploadOption: MulterOptions = {
         // Save original to disk
         cb(null, originalUploadFolder);
       } else if (file.fieldname == 'thumbnailData') {
-        const thumbnailUploadFolder = `${basePath}/${req.user['id']}/thumb/${req.body['deviceId']}`;
+        const thumbnailUploadFolder = `${basePath}/${req.user.id}/thumb/${req.body['deviceId']}`;
 
         if (!existsSync(thumbnailUploadFolder)) {
           mkdirSync(thumbnailUploadFolder, { recursive: true });

+ 8 - 2
server/apps/immich/src/config/profile-image-upload.config.ts

@@ -17,8 +17,11 @@ export const profileImageUploadOption: MulterOptions = {
 
   storage: diskStorage({
     destination: (req: Request, file: Express.Multer.File, cb: any) => {
+      if (!req.user) {
+        return;
+      }
       const basePath = APP_UPLOAD_LOCATION;
-      const profileImageLocation = `${basePath}/${req.user['id']}/profile`;
+      const profileImageLocation = `${basePath}/${req.user.id}/profile`;
 
       if (!existsSync(profileImageLocation)) {
         mkdirSync(profileImageLocation, { recursive: true });
@@ -28,7 +31,10 @@ export const profileImageUploadOption: MulterOptions = {
     },
 
     filename: (req: Request, file: Express.Multer.File, cb: any) => {
-      const userId = req.user['id'];
+      if (!req.user) {
+        return;
+      }
+      const userId = req.user.id;
 
       cb(null, `${userId}${extname(file.originalname)}`);
     },

+ 6 - 6
server/apps/immich/src/decorators/auth-user.decorator.ts

@@ -1,18 +1,18 @@
-import { createParamDecorator, ExecutionContext, UnauthorizedException } from '@nestjs/common';
+import { createParamDecorator, ExecutionContext } from '@nestjs/common';
 import { UserEntity } from '@app/database/entities/user.entity';
 // import { AuthUserDto } from './dto/auth-user.dto';
 
 export class AuthUserDto {
-  id: string;
-  email: string;
+  id!: string;
+  email!: string;
 }
 
 export const GetAuthUser = createParamDecorator((data, ctx: ExecutionContext): AuthUserDto => {
-  const req = ctx.switchToHttp().getRequest();
+  const req = ctx.switchToHttp().getRequest<{ user: UserEntity }>();
 
-  const { id, email } = req.user as UserEntity;
+  const { id, email } = req.user;
 
-  const authUser: any = {
+  const authUser: AuthUserDto = {
     id: id.toString(),
     email,
   };

+ 8 - 0
server/apps/immich/src/global.d.ts

@@ -0,0 +1,8 @@
+import { UserResponseDto } from './api-v1/user/response-dto/user-response.dto';
+
+declare global {
+  namespace Express {
+    // eslint-disable-next-line @typescript-eslint/no-empty-interface
+    interface User extends UserResponseDto {}
+  }
+}

+ 7 - 1
server/apps/immich/src/middlewares/admin-role-guard.middleware.ts

@@ -1,6 +1,5 @@
 import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
 import { Reflector } from '@nestjs/core';
-import { JwtService } from '@nestjs/jwt';
 import { InjectRepository } from '@nestjs/typeorm';
 import { Repository } from 'typeorm';
 import { UserEntity } from '@app/database/entities/user.entity';
@@ -22,7 +21,14 @@ export class AdminRolesGuard implements CanActivate {
       const bearerToken = request.headers['authorization'].split(' ')[1];
       const { userId } = await this.jwtService.validateToken(bearerToken);
 
+      if (!userId) {
+        return false;
+      }
+
       const user = await this.userRepository.findOne(userId);
+      if (!user) {
+        return false;
+      }
 
       return user.isAdmin;
     }

+ 1 - 1
server/apps/immich/src/middlewares/app-logger.middleware.ts

@@ -7,7 +7,7 @@ export class AppLoggerMiddleware implements NestMiddleware {
   private logger = new Logger('HTTP');
 
   use(request: Request, response: Response, next: NextFunction): void {
-    const { ip, method, path: url, baseUrl } = request;
+    const { ip, method, baseUrl } = request;
     const userAgent = request.get('user-agent') || '';
 
     response.on('close', () => {

+ 13 - 11
server/apps/immich/src/modules/background-task/background-task.processor.ts

@@ -1,12 +1,10 @@
-import { InjectQueue, Process, Processor } from '@nestjs/bull';
+import { Process, Processor } from '@nestjs/bull';
 import { InjectRepository } from '@nestjs/typeorm';
-import { Job, Queue } from 'bull';
 import { Repository } from 'typeorm';
 import { AssetEntity } from '@app/database/entities/asset.entity';
 import fs from 'fs';
-import { Logger } from '@nestjs/common';
-import axios from 'axios';
 import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
+import { Job } from 'bull';
 
 @Processor('background-task')
 export class BackgroundTaskProcessor {
@@ -18,9 +16,10 @@ export class BackgroundTaskProcessor {
     private smartInfoRepository: Repository<SmartInfoEntity>,
   ) {}
 
+  // TODO: Should probably use constants / Interfaces for Queue names / data
   @Process('delete-file-on-disk')
-  async deleteFileOnDisk(job) {
-    const { assets }: { assets: AssetEntity[] } = job.data;
+  async deleteFileOnDisk(job: Job<{ assets: AssetEntity[] }>) {
+    const { assets } = job.data;
 
     for (const asset of assets) {
       fs.unlink(asset.originalPath, (err) => {
@@ -29,11 +28,14 @@ export class BackgroundTaskProcessor {
         }
       });
 
-      fs.unlink(asset.resizePath, (err) => {
-        if (err) {
-          console.log('error deleting ', asset.originalPath);
-        }
-      });
+      // TODO: what if there is no asset.resizePath. Should fail the Job?
+      if (asset.resizePath) {
+        fs.unlink(asset.resizePath, (err) => {
+          if (err) {
+            console.log('error deleting ', asset.originalPath);
+          }
+        });
+      }
     }
   }
 }

+ 8 - 3
server/apps/immich/src/modules/immich-jwt/immich-jwt.service.ts

@@ -3,6 +3,11 @@ import { JwtService } from '@nestjs/jwt';
 import { JwtPayloadDto } from '../../api-v1/auth/dto/jwt-payload.dto';
 import { jwtSecret } from '../../constants/jwt.constant';
 
+export type JwtValidationResult = {
+  status: boolean;
+  userId: string | null;
+};
+
 @Injectable()
 export class ImmichJwtService {
   constructor(private jwtService: JwtService) {}
@@ -13,11 +18,11 @@ export class ImmichJwtService {
     });
   }
 
-  public async validateToken(accessToken: string) {
+  public async validateToken(accessToken: string): Promise<JwtValidationResult> {
     try {
-      const payload = await this.jwtService.verify(accessToken, { secret: jwtSecret });
+      const payload = await this.jwtService.verifyAsync<JwtPayloadDto>(accessToken, { secret: jwtSecret });
       return {
-        userId: payload['userId'],
+        userId: payload.userId,
         status: true,
       };
     } catch (e) {

+ 0 - 1
server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts

@@ -3,7 +3,6 @@ import { Module } from '@nestjs/common';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { AssetEntity } from '@app/database/entities/asset.entity';
 import { ScheduleTasksService } from './schedule-tasks.service';
-import { MicroservicesModule } from '../../../../microservices/src/microservices.module';
 
 @Module({
   imports: [

+ 2 - 2
server/apps/immich/test/user.e2e-spec.ts

@@ -8,7 +8,7 @@ import { UserModule } from '../src/api-v1/user/user.module';
 import { ImmichJwtModule } from '../src/modules/immich-jwt/immich-jwt.module';
 import { UserService } from '../src/api-v1/user/user.service';
 import { CreateUserDto } from '../src/api-v1/user/dto/create-user.dto';
-import { User } from '../src/api-v1/user/response-dto/user';
+import { UserResponseDto } from '../src/api-v1/user/response-dto/user-response.dto';
 
 function _createUser(userService: UserService, data: CreateUserDto) {
   return userService.createUser(data);
@@ -44,7 +44,7 @@ describe('User', () => {
 
   describe('with auth', () => {
     let userService: UserService;
-    let authUser: User;
+    let authUser: UserResponseDto;
 
     beforeAll(async () => {
       const builder = Test.createTestingModule({

+ 0 - 2
server/apps/microservices/src/microservices.module.ts

@@ -11,8 +11,6 @@ import { AssetUploadedProcessor } from './processors/asset-uploaded.processor';
 import { ThumbnailGeneratorProcessor } from './processors/thumbnail.processor';
 import { MetadataExtractionProcessor } from './processors/metadata-extraction.processor';
 import { VideoTranscodeProcessor } from './processors/video-transcode.processor';
-import { AssetModule } from '../../immich/src/api-v1/asset/asset.module';
-import { CommunicationGateway } from '../../immich/src/api-v1/communication/communication.gateway';
 import { CommunicationModule } from '../../immich/src/api-v1/communication/communication.module';
 
 @Module({

+ 1 - 1
server/apps/microservices/src/processors/asset-uploaded.processor.ts

@@ -1,4 +1,4 @@
-import { InjectQueue, OnQueueActive, OnQueueCompleted, OnQueueWaiting, Process, Processor } from '@nestjs/bull';
+import { InjectQueue, Process, Processor } from '@nestjs/bull';
 import { Job, Queue } from 'bull';
 import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
 import { InjectRepository } from '@nestjs/typeorm';

+ 5 - 6
server/apps/microservices/src/processors/metadata-extraction.processor.ts

@@ -11,13 +11,12 @@ import { readFile } from 'fs/promises';
 import { Logger } from '@nestjs/common';
 import axios from 'axios';
 import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
-import { ConfigService } from '@nestjs/config';
 import ffmpeg from 'fluent-ffmpeg';
 // import moment from 'moment';
 
 @Processor('metadata-extraction-queue')
 export class MetadataExtractionProcessor {
-  private geocodingClient: GeocodeService;
+  private geocodingClient?: GeocodeService;
 
   constructor(
     @InjectRepository(AssetEntity)
@@ -29,7 +28,7 @@ export class MetadataExtractionProcessor {
     @InjectRepository(SmartInfoEntity)
     private smartInfoRepository: Repository<SmartInfoEntity>,
   ) {
-    if (process.env.ENABLE_MAPBOX == 'true') {
+    if (process.env.ENABLE_MAPBOX == 'true' && process.env.MAPBOX_KEY) {
       this.geocodingClient = mapboxGeocoding({
         accessToken: process.env.MAPBOX_KEY,
       });
@@ -65,7 +64,7 @@ export class MetadataExtractionProcessor {
       newExif.longitude = exifData['longitude'] || null;
 
       // Reverse GeoCoding
-      if (process.env.ENABLE_MAPBOX && exifData['longitude'] && exifData['latitude']) {
+      if (this.geocodingClient && exifData['longitude'] && exifData['latitude']) {
         const geoCodeInfo: MapiResponse = await this.geocodingClient
           .reverseGeocode({
             query: [exifData['longitude'], exifData['latitude']],
@@ -86,7 +85,7 @@ export class MetadataExtractionProcessor {
 
       await this.exifRepository.save(newExif);
     } catch (e) {
-      Logger.error(`Error extracting EXIF ${e.toString()}`, 'extractExif');
+      Logger.error(`Error extracting EXIF ${String(e)}`, 'extractExif');
     }
   }
 
@@ -128,7 +127,7 @@ export class MetadataExtractionProcessor {
         });
       }
     } catch (error) {
-      Logger.error(`Failed to trigger object detection pipe line ${error.toString()}`);
+      Logger.error(`Failed to trigger object detection pipe line ${String(error)}`);
     }
   }
 

+ 8 - 5
server/apps/microservices/src/processors/thumbnail.processor.ts

@@ -43,7 +43,7 @@ export class ThumbnailGeneratorProcessor {
       sharp(asset.originalPath)
         .resize(1440, 2560, { fit: 'inside' })
         .jpeg()
-        .toFile(jpegThumbnailPath, async (err, info) => {
+        .toFile(jpegThumbnailPath, async (err) => {
           if (!err) {
             await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
 
@@ -65,7 +65,7 @@ export class ThumbnailGeneratorProcessor {
         .on('start', () => {
           Logger.log('Start Generating Video Thumbnail', 'generateJPEGThumbnail');
         })
-        .on('error', (error, b, c) => {
+        .on('error', (error) => {
           Logger.error(`Cannot Generate Video Thumbnail ${error}`, 'generateJPEGThumbnail');
           // reject();
         })
@@ -87,15 +87,18 @@ export class ThumbnailGeneratorProcessor {
   }
 
   @Process({ name: 'generate-webp-thumbnail', concurrency: 2 })
-  async generateWepbThumbnail(job: Job) {
-    const { asset }: { asset: AssetEntity } = job.data;
+  async generateWepbThumbnail(job: Job<{ asset: AssetEntity }>) {
+    const { asset } = job.data;
 
+    if (!asset.resizePath) {
+      return;
+    }
     const webpPath = asset.resizePath.replace('jpeg', 'webp');
 
     sharp(asset.resizePath)
       .resize(250)
       .webp()
-      .toFile(webpPath, (err, info) => {
+      .toFile(webpPath, (err) => {
         if (!err) {
           this.assetRepository.update({ id: asset.id }, { webpPath: webpPath });
         }

+ 1 - 1
server/apps/microservices/src/processors/video-transcode.processor.ts

@@ -44,7 +44,7 @@ export class VideoTranscodeProcessor {
         .on('start', () => {
           Logger.log('Start Converting Video', 'mp4Conversion');
         })
-        .on('error', (error, b, c) => {
+        .on('error', (error) => {
           Logger.error(`Cannot Convert Video ${error}`, 'mp4Conversion');
           reject();
         })

+ 1 - 1
server/apps/microservices/test/app.e2e-spec.ts

@@ -1,6 +1,6 @@
 import { Test, TestingModule } from '@nestjs/testing';
 import { INestApplication } from '@nestjs/common';
-import * as request from 'supertest';
+import request from 'supertest';
 import { MicroservicesModule } from './../src/microservices.module';
 
 describe('MicroservicesController (e2e)', () => {

+ 8 - 8
server/libs/database/src/entities/album.entity.ts

@@ -5,23 +5,23 @@ import { UserAlbumEntity } from './user-album.entity';
 @Entity('albums')
 export class AlbumEntity {
   @PrimaryGeneratedColumn('uuid')
-  id: string;
+  id!: string;
 
   @Column()
-  ownerId: string;
+  ownerId!: string;
 
   @Column({ default: 'Untitled Album' })
-  albumName: string;
+  albumName!: string;
 
   @CreateDateColumn({ type: 'timestamptz' })
-  createdAt: string;
+  createdAt!: string;
 
-  @Column({ comment: 'Asset ID to be used as thumbnail', nullable: true })
-  albumThumbnailAssetId: string;
+  @Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true})
+  albumThumbnailAssetId!: string | null;
 
   @OneToMany(() => UserAlbumEntity, (userAlbums) => userAlbums.albumInfo)
-  sharedUsers: UserAlbumEntity[];
+  sharedUsers?: UserAlbumEntity[];
 
   @OneToMany(() => AssetAlbumEntity, (assetAlbumEntity) => assetAlbumEntity.albumInfo)
-  assets: AssetAlbumEntity[];
+  assets?: AssetAlbumEntity[];
 }

+ 5 - 5
server/libs/database/src/entities/asset-album.entity.ts

@@ -6,25 +6,25 @@ import { AssetEntity } from './asset.entity';
 @Unique('PK_unique_asset_in_album', ['albumId', 'assetId'])
 export class AssetAlbumEntity {
   @PrimaryGeneratedColumn()
-  id: string;
+  id!: string;
 
   @Column()
-  albumId: string;
+  albumId!: string;
 
   @Column()
-  assetId: string;
+  assetId!: string;
 
   @ManyToOne(() => AlbumEntity, (album) => album.assets, {
     onDelete: 'CASCADE',
     nullable: true,
   })
   @JoinColumn({ name: 'albumId' })
-  albumInfo: AlbumEntity;
+  albumInfo!: AlbumEntity;
 
   @ManyToOne(() => AssetEntity, {
     onDelete: 'CASCADE',
     nullable: true,
   })
   @JoinColumn({ name: 'assetId' })
-  assetInfo: AssetEntity;
+  assetInfo!: AssetEntity;
 }

+ 22 - 22
server/libs/database/src/entities/asset.entity.ts

@@ -1,4 +1,4 @@
-import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn, PrimaryGeneratedColumn, Unique } from 'typeorm';
+import { Column, Entity, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
 import { ExifEntity } from './exif.entity';
 import { SmartInfoEntity } from './smart-info.entity';
 
@@ -6,52 +6,52 @@ import { SmartInfoEntity } from './smart-info.entity';
 @Unique(['deviceAssetId', 'userId', 'deviceId'])
 export class AssetEntity {
   @PrimaryGeneratedColumn('uuid')
-  id: string;
+  id!: string;
 
   @Column()
-  deviceAssetId: string;
+  deviceAssetId!: string;
 
   @Column()
-  userId: string;
+  userId!: string;
 
   @Column()
-  deviceId: string;
+  deviceId!: string;
 
   @Column()
-  type: AssetType;
+  type!: AssetType;
 
   @Column()
-  originalPath: string;
+  originalPath!: string;
 
-  @Column({ nullable: true })
-  resizePath: string;
+  @Column({ type: 'varchar', nullable: true })
+  resizePath!: string | null;
 
-  @Column({ nullable: true })
-  webpPath: string;
+  @Column({ type: 'varchar', nullable: true })
+  webpPath!: string | null;
 
-  @Column({ nullable: true })
-  encodedVideoPath: string;
+  @Column({ type: 'varchar', nullable: true })
+  encodedVideoPath!: string;
 
   @Column()
-  createdAt: string;
+  createdAt!: string;
 
   @Column()
-  modifiedAt: string;
+  modifiedAt!: string;
 
   @Column({ type: 'boolean', default: false })
-  isFavorite: boolean;
+  isFavorite!: boolean;
 
-  @Column({ nullable: true })
-  mimeType: string;
+  @Column({ type: 'varchar', nullable: true })
+  mimeType!: string | null;
 
-  @Column({ nullable: true })
-  duration: string;
+  @Column({ type: 'varchar', nullable: true })
+  duration!: string | null;
 
   @OneToOne(() => ExifEntity, (exifEntity) => exifEntity.asset)
-  exifInfo: ExifEntity;
+  exifInfo?: ExifEntity;
 
   @OneToOne(() => SmartInfoEntity, (smartInfoEntity) => smartInfoEntity.asset)
-  smartInfo: SmartInfoEntity;
+  smartInfo?: SmartInfoEntity;
 }
 
 export enum AssetType {

+ 8 - 8
server/libs/database/src/entities/device-info.entity.ts

@@ -4,25 +4,25 @@ import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, Unique } from
 @Unique(['userId', 'deviceId'])
 export class DeviceInfoEntity {
   @PrimaryGeneratedColumn()
-  id: number;
+  id!: number;
 
   @Column()
-  userId: string;
+  userId!: string;
 
   @Column()
-  deviceId: string;
+  deviceId!: string;
 
   @Column()
-  deviceType: DeviceType;
+  deviceType!: DeviceType;
 
-  @Column({ nullable: true })
-  notificationToken: string;
+  @Column({ type: 'varchar', nullable: true })
+  notificationToken!: string | null;
 
   @CreateDateColumn()
-  createdAt: string;
+  createdAt!: string;
 
   @Column({ type: 'bool', default: false })
-  isAutoBackup: boolean;
+  isAutoBackup!: boolean;
 }
 
 export enum DeviceType {

+ 34 - 34
server/libs/database/src/entities/exif.entity.ts

@@ -7,70 +7,70 @@ import { AssetEntity } from './asset.entity';
 @Entity('exif')
 export class ExifEntity {
   @PrimaryGeneratedColumn()
-  id: string;
+  id!: string;
 
   @Index({ unique: true })
   @Column({ type: 'uuid' })
-  assetId: string;
+  assetId!: string;
 
-  @Column({ nullable: true })
-  make: string;
+  @Column({ type: 'varchar', nullable: true })
+  make!: string | null;
 
-  @Column({ nullable: true })
-  model: string;
+  @Column({ type: 'varchar', nullable: true })
+  model!: string | null;
 
-  @Column({ nullable: true })
-  imageName: string;
+  @Column({ type: 'varchar', nullable: true })
+  imageName!: string | null;
 
-  @Column({ nullable: true })
-  exifImageWidth: number;
+  @Column({ type: 'integer', nullable: true })
+  exifImageWidth!: number | null;
 
-  @Column({ nullable: true })
-  exifImageHeight: number;
+  @Column({ type: 'integer', nullable: true })
+  exifImageHeight!: number | null;
 
-  @Column({ nullable: true })
-  fileSizeInByte: number;
+  @Column({ type: 'integer', nullable: true })
+  fileSizeInByte!: number | null;
 
-  @Column({ nullable: true })
-  orientation: string;
+  @Column({ type: 'varchar', nullable: true })
+  orientation!: string | null;
 
   @Column({ type: 'timestamptz', nullable: true })
-  dateTimeOriginal: Date;
+  dateTimeOriginal!: Date | null;
 
   @Column({ type: 'timestamptz', nullable: true })
-  modifyDate: Date;
+  modifyDate!: Date | null;
 
-  @Column({ nullable: true })
-  lensModel: string;
+  @Column({ type: 'varchar', nullable: true })
+  lensModel!: string | null;
 
   @Column({ type: 'float8', nullable: true })
-  fNumber: number;
+  fNumber!: number | null;
 
   @Column({ type: 'float8', nullable: true })
-  focalLength: number;
+  focalLength!: number | null;
 
-  @Column({ nullable: true })
-  iso: number;
+  @Column({ type: 'integer', nullable: true })
+  iso!: number | null;
 
   @Column({ type: 'float', nullable: true })
-  exposureTime: number;
+  exposureTime!: number | null;
 
   @Column({ type: 'float', nullable: true })
-  latitude: number;
+  latitude!: number | null;
 
   @Column({ type: 'float', nullable: true })
-  longitude: number;
+  longitude!: number | null;
 
-  @Column({ nullable: true })
-  city: string;
+  @Column({ type: 'varchar', nullable: true })
+  city!: string | null;
 
-  @Column({ nullable: true })
-  state: string;
+  @Column({ type: 'varchar', nullable: true })
+  state!: string | null;
 
-  @Column({ nullable: true })
-  country: string;
+  @Column({ type: 'varchar', nullable: true })
+  country!: string | null;
 
   @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
   @JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
-  asset: ExifEntity;
+  asset?: ExifEntity;
 }

+ 5 - 5
server/libs/database/src/entities/smart-info.entity.ts

@@ -4,19 +4,19 @@ import { AssetEntity } from './asset.entity';
 @Entity('smart_info')
 export class SmartInfoEntity {
   @PrimaryGeneratedColumn()
-  id: string;
+  id!: string;
 
   @Index({ unique: true })
   @Column({ type: 'uuid' })
-  assetId: string;
+  assetId!: string;
 
   @Column({ type: 'text', array: true, nullable: true })
-  tags: string[];
+  tags!: string[] | null;
 
   @Column({ type: 'text', array: true, nullable: true })
-  objects: string[];
+  objects!: string[] | null;
 
   @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
   @JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
-  asset: SmartInfoEntity;
+  asset?: SmartInfoEntity;
 }

+ 5 - 5
server/libs/database/src/entities/user-album.entity.ts

@@ -6,22 +6,22 @@ import { AlbumEntity } from './album.entity';
 @Unique('PK_unique_user_in_album', ['albumId', 'sharedUserId'])
 export class UserAlbumEntity {
   @PrimaryGeneratedColumn()
-  id: string;
+  id!: string;
 
   @Column()
-  albumId: string;
+  albumId!: string;
 
   @Column()
-  sharedUserId: string;
+  sharedUserId!: string;
 
   @ManyToOne(() => AlbumEntity, (album) => album.sharedUsers, {
     onDelete: 'CASCADE',
     nullable: true,
   })
   @JoinColumn({ name: 'albumId' })
-  albumInfo: AlbumEntity;
+  albumInfo!: AlbumEntity;
 
   @ManyToOne(() => UserEntity)
   @JoinColumn({ name: 'sharedUserId' })
-  userInfo: UserEntity;
+  userInfo!: UserEntity;
 }

+ 10 - 10
server/libs/database/src/entities/user.entity.ts

@@ -3,32 +3,32 @@ import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeor
 @Entity('users')
 export class UserEntity {
   @PrimaryGeneratedColumn('uuid')
-  id: string;
+  id!: string;
 
   @Column()
-  firstName: string;
+  firstName!: string;
 
   @Column()
-  lastName: string;
+  lastName!: string;
 
   @Column()
-  isAdmin: boolean;
+  isAdmin!: boolean;
 
   @Column()
-  email: string;
+  email!: string;
 
   @Column({ select: false })
-  password: string;
+  password?: string;
 
   @Column({ select: false })
-  salt: string;
+  salt?: string;
 
   @Column()
-  profileImagePath: string;
+  profileImagePath!: string;
 
   @Column()
-  isFirstLoggedIn: boolean;
+  isFirstLoggedIn!: boolean;
 
   @CreateDateColumn()
-  createdAt: string;
+  createdAt!: string;
 }

+ 53 - 0
server/package-lock.json

@@ -59,6 +59,7 @@
         "@types/imagemin": "^8.0.0",
         "@types/jest": "27.0.2",
         "@types/lodash": "^4.14.178",
+        "@types/mapbox__mapbox-sdk": "^0.13.4",
         "@types/multer": "^1.4.7",
         "@types/node": "^16.0.0",
         "@types/passport-jwt": "^3.0.6",
@@ -2205,6 +2206,12 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/geojson": {
+      "version": "7946.0.8",
+      "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz",
+      "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==",
+      "dev": true
+    },
     "node_modules/@types/graceful-fs": {
       "version": "4.1.5",
       "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
@@ -2312,6 +2319,26 @@
       "integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==",
       "dev": true
     },
+    "node_modules/@types/mapbox__mapbox-sdk": {
+      "version": "0.13.4",
+      "resolved": "https://registry.npmjs.org/@types/mapbox__mapbox-sdk/-/mapbox__mapbox-sdk-0.13.4.tgz",
+      "integrity": "sha512-J4/7uKNo1uc4+xgjbOKFkZxNmlPbpsITcvhn3nXncTZtdGDOmJENfcDEpiRJRBIlnKMGeXy4fxVuEg+i0I3YWA==",
+      "dev": true,
+      "dependencies": {
+        "@types/geojson": "*",
+        "@types/mapbox-gl": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/mapbox-gl": {
+      "version": "2.7.3",
+      "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.7.3.tgz",
+      "integrity": "sha512-XdveeJptNNZw7ZoeiAJ2/dupNtWaV6qpBG/SOFEpQNQAc+oiO6qUznX85n+W1XbLeD8SVRVfVORKuR+I4CHDZw==",
+      "dev": true,
+      "dependencies": {
+        "@types/geojson": "*"
+      }
+    },
     "node_modules/@types/mime": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@@ -12822,6 +12849,12 @@
         "@types/node": "*"
       }
     },
+    "@types/geojson": {
+      "version": "7946.0.8",
+      "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz",
+      "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==",
+      "dev": true
+    },
     "@types/graceful-fs": {
       "version": "4.1.5",
       "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
@@ -12929,6 +12962,26 @@
       "integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==",
       "dev": true
     },
+    "@types/mapbox__mapbox-sdk": {
+      "version": "0.13.4",
+      "resolved": "https://registry.npmjs.org/@types/mapbox__mapbox-sdk/-/mapbox__mapbox-sdk-0.13.4.tgz",
+      "integrity": "sha512-J4/7uKNo1uc4+xgjbOKFkZxNmlPbpsITcvhn3nXncTZtdGDOmJENfcDEpiRJRBIlnKMGeXy4fxVuEg+i0I3YWA==",
+      "dev": true,
+      "requires": {
+        "@types/geojson": "*",
+        "@types/mapbox-gl": "*",
+        "@types/node": "*"
+      }
+    },
+    "@types/mapbox-gl": {
+      "version": "2.7.3",
+      "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.7.3.tgz",
+      "integrity": "sha512-XdveeJptNNZw7ZoeiAJ2/dupNtWaV6qpBG/SOFEpQNQAc+oiO6qUznX85n+W1XbLeD8SVRVfVORKuR+I4CHDZw==",
+      "dev": true,
+      "requires": {
+        "@types/geojson": "*"
+      }
+    },
     "@types/mime": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",

+ 5 - 1
server/package.json

@@ -13,7 +13,10 @@
     "start:dev": "nest start --watch",
     "start:debug": "nest start --debug --watch",
     "start:prod": "node dist/main",
-    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
+    "lint": "eslint \"{apps,libs}/**/*.ts\" --max-warnings 0",
+    "lint:fix": "npm run lint -- --fix",
+    "check:types": "tsc --noEmit",
+    "check:all": "npm run lint && npm run check:types && npm run test",
     "test": "jest",
     "test:watch": "jest --watch",
     "test:cov": "jest --coverage",
@@ -72,6 +75,7 @@
     "@types/imagemin": "^8.0.0",
     "@types/jest": "27.0.2",
     "@types/lodash": "^4.14.178",
+    "@types/mapbox__mapbox-sdk": "^0.13.4",
     "@types/multer": "^1.4.7",
     "@types/node": "^16.0.0",
     "@types/passport-jwt": "^3.0.6",

+ 1 - 0
server/tsconfig.json

@@ -1,6 +1,7 @@
 {
   "compilerOptions": {
     "module": "commonjs",
+    "strict": true,
     "declaration": true,
     "removeComments": true,
     "emitDecoratorMetadata": true,