From f44fa45aa0d9874725b57fad244d5aa7d3480e19 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors <jonathan@jogenfors.se> Date: Fri, 2 Feb 2024 04:18:00 +0100 Subject: [PATCH] chore(server,cli,web): housekeeping and stricter code style (#6751) * add unicorn to eslint * fix lint errors for cli * fix merge * fix album name extraction * Update cli/src/commands/upload.command.ts Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * es2k23 * use lowercase os * return undefined album name * fix bug in asset response dto * auto fix issues * fix server code style * es2022 and formatting * fix compilation error * fix test * fix config load * fix last lint errors * set string type * bump ts * start work on web * web formatting * Fix UUIDParamDto as UUIDParamDto * fix library service lint * fix web errors * fix errors * formatting * wip * lints fixed * web can now start * alphabetical package json * rename error * chore: clean up --------- Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> --- cli/.eslintrc.cjs | 10 +- cli/package-lock.json | 30 +- cli/package.json | 14 +- cli/src/commands/upload.command.ts | 92 ++- cli/src/index.ts | 8 +- cli/src/services/crawl.service.ts | 10 +- cli/src/services/session.service.ts | 16 +- cli/src/version.ts | 6 +- cli/test/e2e/login-key.e2e-spec.ts | 6 +- cli/test/e2e/setup.ts | 14 +- cli/test/e2e/vitest.config.ts | 2 +- cli/tsconfig.json | 4 +- server/.eslintrc.js | 8 +- server/package-lock.json | 700 +++++++++++++++++- server/package.json | 9 +- server/src/domain/access/access.core.ts | 91 ++- .../src/domain/activity/activity.service.ts | 2 +- server/src/domain/album/album-response.dto.ts | 13 +- server/src/domain/album/album.service.ts | 30 +- server/src/domain/api-key/api-key.service.ts | 2 +- server/src/domain/asset/asset.service.spec.ts | 4 +- server/src/domain/asset/asset.service.ts | 57 +- .../asset/response-dto/asset-response.dto.ts | 5 +- .../asset/response-dto/exif-response.dto.ts | 4 +- server/src/domain/audit/audit.service.ts | 32 +- server/src/domain/auth/auth.service.spec.ts | 2 +- server/src/domain/auth/auth.service.ts | 19 +- .../src/domain/database/database.service.ts | 4 +- server/src/domain/domain.constant.spec.ts | 24 +- server/src/domain/domain.constant.ts | 13 +- server/src/domain/domain.util.ts | 25 +- .../src/domain/download/download.service.ts | 18 +- server/src/domain/job/job.service.spec.ts | 2 +- server/src/domain/job/job.service.ts | 66 +- .../domain/library/library.service.spec.ts | 22 +- server/src/domain/library/library.service.ts | 36 +- server/src/domain/media/media.service.ts | 66 +- server/src/domain/media/media.util.ts | 40 +- .../domain/metadata/metadata.service.spec.ts | 12 +- .../src/domain/metadata/metadata.service.ts | 34 +- server/src/domain/partner/partner.service.ts | 8 +- .../src/domain/person/person.service.spec.ts | 8 +- server/src/domain/person/person.service.ts | 6 +- .../repositories/database.repository.ts | 2 +- .../domain/repositories/media.repository.ts | 2 +- .../domain/repositories/storage.repository.ts | 6 +- server/src/domain/search/search.service.ts | 11 +- .../server-info/server-info.service.spec.ts | 44 +- .../domain/server-info/server-info.service.ts | 2 +- .../domain/shared-link/shared-link.service.ts | 20 +- .../domain/smart-info/smart-info.constant.ts | 8 +- .../storage-template.service.spec.ts | 8 +- .../storage-template.service.ts | 38 +- server/src/domain/storage/storage.core.ts | 69 +- .../system-config/system-config.core.ts | 43 +- .../system-config.service.spec.ts | 2 +- .../system-config/system-config.service.ts | 2 +- server/src/domain/tag/tag.service.ts | 8 +- .../user/response-dto/user-response.dto.ts | 5 +- server/src/domain/user/user.core.ts | 4 +- server/src/domain/user/user.service.spec.ts | 2 +- server/src/domain/user/user.service.ts | 11 +- .../commands/reset-admin-password.command.ts | 18 +- server/src/immich/api-v1/asset/asset.core.ts | 2 +- .../immich/api-v1/asset/asset.service.spec.ts | 4 +- .../src/immich/api-v1/asset/asset.service.ts | 16 +- server/src/immich/app.guard.ts | 28 +- server/src/immich/app.service.ts | 14 +- server/src/immich/app.utils.ts | 38 +- .../src/immich/controllers/auth.controller.ts | 4 +- .../immich/controllers/oauth.controller.ts | 4 +- .../controllers/shared-link.controller.ts | 4 +- .../interceptors/file-upload.interceptor.ts | 51 +- server/src/infra/database.config.ts | 5 +- server/src/infra/infra.config.ts | 4 +- server/src/infra/infra.util.ts | 2 +- server/src/infra/infra.utils.ts | 20 +- .../1688392120838-AddLibraryTable.ts | 3 +- .../migrations/1700713871511-UsePgVectors.ts | 2 +- .../infra/repositories/access.repository.ts | 2 +- .../infra/repositories/album.repository.ts | 2 +- .../infra/repositories/asset.repository.ts | 38 +- .../repositories/communication.repository.ts | 6 +- .../infra/repositories/crypto.repository.ts | 6 +- .../infra/repositories/filesystem.provider.ts | 14 +- .../src/infra/repositories/job.repository.ts | 20 +- .../machine-learning.repository.ts | 2 +- .../infra/repositories/media.repository.ts | 14 +- .../infra/repositories/metadata.repository.ts | 21 +- .../infra/repositories/person.repository.ts | 7 +- .../repositories/smart-info.repository.ts | 18 +- .../repositories/system-config.repository.ts | 4 +- .../src/infra/repositories/user.repository.ts | 6 +- server/src/infra/sql-generator/index.ts | 7 +- server/src/infra/sql-generator/sql.logger.ts | 4 +- .../src/infra/subscribers/audit.subscriber.ts | 6 +- server/src/main.ts | 15 +- .../utils/exif/coordinates.spec.ts | 12 +- .../src/microservices/utils/numbers.spec.ts | 13 +- server/src/microservices/utils/numbers.ts | 12 +- server/src/test-utils/utils.ts | 12 +- server/test/fixtures/asset.stub.ts | 12 +- server/test/fixtures/media.stub.ts | 2 +- .../repositories/database.repository.mock.ts | 2 +- server/tsconfig.json | 10 +- web/.eslintrc.cjs | 15 +- web/package-lock.json | 418 ++++++++++- web/package.json | 9 +- web/src/api/api.ts | 24 +- web/src/api/types.ts | 2 +- .../admin-page/delete-confirm-dialoge.svelte | 6 +- .../admin-page/jobs/job-tile.svelte | 2 +- .../admin-page/jobs/jobs-panel.svelte | 3 +- .../admin-page/restore-dialoge.svelte | 2 +- .../admin-page/settings/admin-settings.svelte | 15 +- .../settings/setting-checkboxes.svelte | 6 +- .../admin-page/settings/setting-select.svelte | 2 +- .../storage-template-settings.svelte | 2 +- .../album-page/__tests__/album-card.spec.ts | 6 +- .../components/album-page/album-card.svelte | 2 +- .../components/album-page/album-viewer.svelte | 3 +- .../album-page/share-info-modal.svelte | 8 +- .../album-page/thumbnail-selection.svelte | 6 +- .../album-page/user-selection-modal.svelte | 14 +- .../asset-viewer/activity-viewer.svelte | 6 +- .../asset-viewer/asset-viewer.svelte | 48 +- .../asset-viewer/detail-panel.svelte | 5 +- .../asset-viewer/panorama-viewer.svelte | 4 +- .../asset-viewer/photo-viewer.svelte | 6 +- .../assets/thumbnail/image-thumbnail.svelte | 1 + .../assets/thumbnail/thumbnail.svelte | 2 +- .../lib/components/elements/dropdown.svelte | 8 +- .../faces-page/assign-face-side-panel.svelte | 31 +- .../faces-page/merge-suggestion-modal.svelte | 2 +- .../components/faces-page/people-list.svelte | 6 +- .../faces-page/person-side-panel.svelte | 16 +- .../components/forms/create-user-form.svelte | 7 +- .../components/forms/edit-user-form.svelte | 4 +- .../forms/library-import-paths-form.svelte | 2 +- .../forms/library-scan-settings-form.svelte | 2 +- .../lib/components/forms/login-form.svelte | 6 +- .../memory-page/memory-viewer.svelte | 12 +- .../photos-page/actions/add-to-album.svelte | 4 +- .../photos-page/actions/archive-action.svelte | 2 +- .../actions/asset-job-actions.svelte | 4 +- .../actions/create-shared-link.svelte | 2 +- .../photos-page/actions/delete-assets.svelte | 4 +- .../actions/download-action.svelte | 2 +- .../actions/favorite-action.svelte | 2 +- .../actions/remove-from-album.svelte | 8 +- .../actions/remove-from-shared-link.svelte | 2 +- .../photos-page/actions/restore-assets.svelte | 8 +- .../actions/select-all-assets.svelte | 4 +- .../photos-page/actions/stack-action.svelte | 6 +- .../photos-page/asset-date-group.svelte | 17 +- .../components/photos-page/asset-grid.svelte | 31 +- .../asset-select-control-bar.svelte | 2 +- .../components/photos-page/memory-lane.svelte | 4 +- .../individual-shared-viewer.svelte | 12 +- .../album-selection-modal.svelte | 13 +- .../shared-components/base-modal.svelte | 6 +- .../shared-components/change-location.svelte | 6 +- .../create-shared-link-modal.svelte | 33 +- .../empty-placeholder.svelte | 2 +- .../asset-selection-viewer.svelte | 8 +- .../gallery-viewer/gallery-viewer.svelte | 16 +- .../shared-components/map/map.svelte | 3 +- .../navigation-bar/navigation-bar.svelte | 6 +- .../notification/notification-card.svelte | 2 +- .../notification/notification.ts | 2 +- .../shared-components/portal/portal.svelte | 22 +- .../profile-image-cropper.svelte | 16 +- .../scrollbar/scrollbar.svelte | 8 +- .../search-bar/search-bar.svelte | 6 +- .../version-announcement-box.svelte | 4 +- .../sharedlinks-page/shared-link-card.svelte | 2 +- .../user-settings-page/device-card.svelte | 4 +- .../user-settings-page/device-list.svelte | 4 +- .../user-settings-page/library-list.svelte | 10 +- .../partner-selection-modal.svelte | 12 +- .../user-api-key-list.svelte | 6 +- web/src/lib/stores/assets.store.ts | 43 +- web/src/lib/stores/download.ts | 2 +- web/src/lib/stores/preferences.store.ts | 16 +- web/src/lib/stores/upload.ts | 4 +- web/src/lib/stores/websocket.ts | 4 +- web/src/lib/utils/actions.ts | 4 +- web/src/lib/utils/asset-utils.spec.ts | 6 +- web/src/lib/utils/asset-utils.ts | 47 +- web/src/lib/utils/byte-converter.ts | 4 +- web/src/lib/utils/byte-units.ts | 2 +- web/src/lib/utils/context-menu.ts | 9 +- web/src/lib/utils/executor-queue.ts | 8 +- web/src/lib/utils/file-uploader.ts | 14 +- web/src/lib/utils/person.ts | 22 +- web/src/lib/utils/time-to-seconds.spec.ts | 2 +- web/src/routes/(user)/albums/+page.svelte | 13 +- .../(user)/albums/[albumId]/+page.svelte | 23 +- .../albums/__tests__/albums.bloc.spec.ts | 4 +- web/src/routes/(user)/albums/albums.bloc.ts | 6 +- web/src/routes/(user)/archive/+page.svelte | 2 +- web/src/routes/(user)/favorites/+page.svelte | 2 +- .../(user)/partners/[userId]/+page.svelte | 4 +- web/src/routes/(user)/people/+page.svelte | 19 +- .../(user)/people/[personId]/+page.svelte | 10 +- web/src/routes/(user)/photos/+page.svelte | 2 +- web/src/routes/(user)/search/+page.svelte | 13 +- .../routes/(user)/share/[key]/+page.svelte | 4 +- web/src/routes/(user)/share/[key]/+page.ts | 8 +- web/src/routes/(user)/sharing/+page.svelte | 8 +- web/src/routes/(user)/trash/+page.svelte | 8 +- web/src/routes/admin/jobs-status/+page.svelte | 2 +- web/src/routes/admin/repair/+page.svelte | 16 +- .../routes/admin/system-settings/+page.svelte | 2 +- .../routes/admin/user-management/+page.svelte | 8 +- web/src/routes/auth/onboarding/+page.svelte | 4 +- web/src/test-data/factories/album-factory.ts | 2 +- web/vite.config.js | 3 +- 218 files changed, 2471 insertions(+), 1244 deletions(-) diff --git a/cli/.eslintrc.cjs b/cli/.eslintrc.cjs index 17a0a2dd6c..ca36d31bf7 100644 --- a/cli/.eslintrc.cjs +++ b/cli/.eslintrc.cjs @@ -6,7 +6,7 @@ module.exports = { tsconfigRootDir: __dirname, }, plugins: ['@typescript-eslint/eslint-plugin'], - extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], + extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'], root: true, env: { node: true, @@ -19,6 +19,14 @@ module.exports = { '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-floating-promises': 'error', + 'unicorn/prefer-module': 'off', + curly: 2, 'prettier/prettier': 0, + 'unicorn/prevent-abbreviations': [ + 'error', + { + ignore: ['\\.e2e-spec$', /^ignore/i], + }, + ], }, }; diff --git a/cli/package-lock.json b/cli/package-lock.json index c9afe6e467..9c218f78b9 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -30,15 +30,15 @@ "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitest/coverage-v8": "^1.2.2", - "eslint": "^8.43.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", - "eslint-plugin-unicorn": "^50.0.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-unicorn": "^50.0.1", "immich": "file:../server", "mock-fs": "^5.2.0", "ts-node": "^10.9.1", "tslib": "^2.5.3", - "typescript": "^5.0.0", + "typescript": "^5.3.3", "vitest": "^1.2.1" } }, @@ -73,6 +73,7 @@ "@nestjs/typeorm": "^10.0.0", "@nestjs/websockets": "^10.2.2", "@socket.io/postgres-adapter": "^0.3.1", + "@types/picomatch": "^2.3.3", "archiver": "^6.0.0", "async-lock": "^1.4.0", "axios": "^1.5.0", @@ -96,6 +97,7 @@ "node-addon-api": "^7.0.0", "openid-client": "^5.4.3", "pg": "^8.11.3", + "picomatch": "^3.0.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "sanitize-filename": "^1.6.3", @@ -127,10 +129,12 @@ "@types/ua-parser-js": "^0.7.36", "@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/parser": "^6.4.1", + "chokidar": "^3.5.3", "dotenv": "^16.3.1", - "eslint": "^8.48.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-unicorn": "^50.0.1", "jest": "^29.6.4", "jest-when": "^3.6.0", "mock-fs": "^5.2.0", @@ -8768,6 +8772,7 @@ "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", "@types/node": "^20.5.7", + "@types/picomatch": "^2.3.3", "@types/sharp": "^0.31.1", "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", @@ -8778,13 +8783,15 @@ "axios": "^1.5.0", "bcrypt": "^5.1.1", "bullmq": "^4.8.0", + "chokidar": "^3.5.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", "dotenv": "^16.3.1", - "eslint": "^8.48.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-unicorn": "^50.0.1", "exiftool-vendored": "~24.4.0", "exiftool-vendored.pl": "12.73", "fluent-ffmpeg": "^2.1.2", @@ -8803,6 +8810,7 @@ "node-addon-api": "^7.0.0", "openid-client": "^5.4.3", "pg": "^8.11.3", + "picomatch": "^3.0.1", "prettier": "^3.0.2", "prettier-plugin-organize-imports": "^3.2.3", "reflect-metadata": "^0.1.13", diff --git a/cli/package.json b/cli/package.json index 6c18605661..9e32061e3d 100644 --- a/cli/package.json +++ b/cli/package.json @@ -28,18 +28,18 @@ "@types/cli-progress": "^3.11.0", "@types/mock-fs": "^4.13.1", "@types/node": "^20.3.1", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^6.4.1", + "@typescript-eslint/parser": "^6.4.1", "@vitest/coverage-v8": "^1.2.2", - "eslint": "^8.43.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", - "eslint-plugin-unicorn": "^50.0.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-unicorn": "^50.0.1", "immich": "file:../server", "mock-fs": "^5.2.0", "ts-node": "^10.9.1", "tslib": "^2.5.3", - "typescript": "^5.0.0", + "typescript": "^5.3.3", "vitest": "^1.2.1" }, "scripts": { diff --git a/cli/src/commands/upload.command.ts b/cli/src/commands/upload.command.ts index 17fc6541bc..f026e374df 100644 --- a/cli/src/commands/upload.command.ts +++ b/cli/src/commands/upload.command.ts @@ -8,7 +8,7 @@ import { BaseCommand } from './base-command'; import { basename } from 'node:path'; import { access, constants, stat, unlink } from 'node:fs/promises'; import { createHash } from 'node:crypto'; -import Os from 'os'; +import os from 'node:os'; class Asset { readonly path: string; @@ -27,7 +27,7 @@ class Asset { async prepare() { const stats = await stat(this.path); - this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replace(/\s+/g, ''); + this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replaceAll(/\s+/g, ''); this.fileCreatedAt = stats.mtime.toISOString(); this.fileModifiedAt = stats.mtime.toISOString(); this.fileSize = stats.size; @@ -35,9 +35,15 @@ class Asset { } async getUploadFormData(): Promise<FormData> { - if (!this.deviceAssetId) throw new Error('Device asset id not set'); - if (!this.fileCreatedAt) throw new Error('File created at not set'); - if (!this.fileModifiedAt) throw new Error('File modified at not set'); + if (!this.deviceAssetId) { + throw new Error('Device asset id not set'); + } + if (!this.fileCreatedAt) { + throw new Error('File created at not set'); + } + if (!this.fileModifiedAt) { + throw new Error('File modified at not set'); + } // TODO: doesn't xmp replace the file extension? Will need investigation const sideCarPath = `${this.path}.xmp`; @@ -45,7 +51,7 @@ class Asset { try { await access(sideCarPath, constants.R_OK); sidecarData = createReadStream(sideCarPath); - } catch (error) {} + } catch {} const data: any = { assetData: createReadStream(this.path), @@ -57,8 +63,8 @@ class Asset { }; const formData = new FormData(); - for (const prop in data) { - formData.append(prop, data[prop]); + for (const property in data) { + formData.append(property, data[property]); } if (sidecarData) { @@ -86,12 +92,8 @@ class Asset { return await sha1(this.path); } - private extractAlbumName(): string { - if (Os.platform() === 'win32') { - return this.path.split('\\').slice(-2)[0]; - } else { - return this.path.split('/').slice(-2)[0]; - } + private extractAlbumName(): string | undefined { + return os.platform() === 'win32' ? this.path.split('\\').at(-2) : this.path.split('/').at(-2); } } @@ -162,7 +164,7 @@ export class UploadCommand extends BaseCommand { } } - const existingAlbums = (await this.immichApi.albumApi.getAllAlbums()).data; + const { data: existingAlbums } = await this.immichApi.albumApi.getAllAlbums(); uploadProgress.start(totalSize, 0); uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) }); @@ -195,32 +197,30 @@ export class UploadCommand extends BaseCommand { skipAsset = skipUpload && !isDuplicate; } - if (!skipAsset) { - if (!options.dryRun) { - if (!skipUpload) { - const formData = await asset.getUploadFormData(); - const res = await this.uploadAsset(formData); - existingAssetId = res.data.id; - uploadCounter++; - totalSizeUploaded += asset.fileSize; + if (!skipAsset && !options.dryRun) { + if (!skipUpload) { + const formData = await asset.getUploadFormData(); + const { data } = await this.uploadAsset(formData); + existingAssetId = data.id; + uploadCounter++; + totalSizeUploaded += asset.fileSize; + } + + if ((options.album || options.albumName) && asset.albumName !== undefined) { + let album = existingAlbums.find((album) => album.albumName === asset.albumName); + if (!album) { + const { data } = await this.immichApi.albumApi.createAlbum({ + createAlbumDto: { albumName: asset.albumName }, + }); + album = data; + existingAlbums.push(album); } - if ((options.album || options.albumName) && asset.albumName !== undefined) { - let album = existingAlbums.find((album) => album.albumName === asset.albumName); - if (!album) { - const res = await this.immichApi.albumApi.createAlbum({ - createAlbumDto: { albumName: asset.albumName }, - }); - album = res.data; - existingAlbums.push(album); - } - - if (existingAssetId) { - await this.immichApi.albumApi.addAssetsToAlbum({ - id: album.id, - bulkIdsDto: { ids: [existingAssetId] }, - }); - } + if (existingAssetId) { + await this.immichApi.albumApi.addAssetsToAlbum({ + id: album.id, + bulkIdsDto: { ids: [existingAssetId] }, + }); } } } @@ -233,12 +233,7 @@ export class UploadCommand extends BaseCommand { uploadProgress.stop(); } - let messageStart; - if (options.dryRun) { - messageStart = 'Would have'; - } else { - messageStart = 'Successfully'; - } + const messageStart = options.dryRun ? 'Would have' : 'Successfully'; if (uploadCounter === 0) { console.log('All assets were already uploaded, nothing to do.'); @@ -276,12 +271,11 @@ export class UploadCommand extends BaseCommand { 'x-api-key': this.immichApi.apiKey, ...data.getHeaders(), }, - maxContentLength: Infinity, - maxBodyLength: Infinity, + maxContentLength: Number.POSITIVE_INFINITY, + maxBodyLength: Number.POSITIVE_INFINITY, data, }; - const res = await axios(config); - return res; + return axios(config); } } diff --git a/cli/src/index.ts b/cli/src/index.ts index 8369bff934..6582b37956 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -1,21 +1,21 @@ #! /usr/bin/env node import { Command, Option } from 'commander'; import path from 'node:path'; -import os from 'os'; +import os from 'node:os'; import { version } from '../package.json'; import { LoginCommand } from './commands/login'; import { LogoutCommand } from './commands/logout.command'; import { ServerInfoCommand } from './commands/server-info.command'; import { UploadCommand } from './commands/upload.command'; -const userHomeDir = os.homedir(); -const configDir = path.join(userHomeDir, '.config/immich/'); +const homeDirectory = os.homedir(); +const configDirectory = path.join(homeDirectory, '.config/immich/'); const program = new Command() .name('immich') .version(version) .description('Command line interface for Immich') - .addOption(new Option('-d, --config', 'Configuration directory').env('IMMICH_CONFIG_DIR').default(configDir)); + .addOption(new Option('-d, --config', 'Configuration directory').env('IMMICH_CONFIG_DIR').default(configDirectory)); program .command('upload') diff --git a/cli/src/services/crawl.service.ts b/cli/src/services/crawl.service.ts index bfe94a8992..3ad0fcf3b8 100644 --- a/cli/src/services/crawl.service.ts +++ b/cli/src/services/crawl.service.ts @@ -1,5 +1,5 @@ import { glob } from 'glob'; -import * as fs from 'fs'; +import * as fs from 'node:fs'; export class CrawlOptions { pathsToCrawl!: string[]; @@ -12,14 +12,14 @@ export class CrawlService { private readonly extensions!: string[]; constructor(image: string[], video: string[]) { - this.extensions = image.concat(video).map((extension) => extension.replace('.', '')); + this.extensions = [...image, ...video].map((extension) => extension.replace('.', '')); } async crawl(options: CrawlOptions): Promise<string[]> { const { recursive, pathsToCrawl, exclusionPatterns, includeHidden } = options; if (!pathsToCrawl) { - return Promise.resolve([]); + return []; } const patterns: string[] = []; @@ -65,8 +65,6 @@ export class CrawlService { ignore: exclusionPatterns, }); - const returnedFiles = crawledFiles.concat(globbedFiles); - returnedFiles.sort(); - return returnedFiles; + return [...crawledFiles, ...globbedFiles].sort(); } } diff --git a/cli/src/services/session.service.ts b/cli/src/services/session.service.ts index ee49a7074a..9276a47210 100644 --- a/cli/src/services/session.service.ts +++ b/cli/src/services/session.service.ts @@ -1,4 +1,4 @@ -import { existsSync } from 'fs'; +import { existsSync } from 'node:fs'; import { access, constants, mkdir, readFile, unlink, writeFile } from 'node:fs/promises'; import path from 'node:path'; import yaml from 'yaml'; @@ -15,12 +15,12 @@ class LoginError extends Error { } export class SessionService { - readonly configDir!: string; + readonly configDirectory!: string; readonly authPath!: string; - constructor(configDir: string) { - this.configDir = configDir; - this.authPath = path.join(configDir, '/auth.yml'); + constructor(configDirectory: string) { + this.configDirectory = configDirectory; + this.authPath = path.join(configDirectory, '/auth.yml'); } async connect(): Promise<ImmichApi> { @@ -74,11 +74,11 @@ export class SessionService { console.log(`Logged in as ${userInfo.email}`); - if (!existsSync(this.configDir)) { + if (!existsSync(this.configDirectory)) { // Create config folder if it doesn't exist - const created = await mkdir(this.configDir, { recursive: true }); + const created = await mkdir(this.configDirectory, { recursive: true }); if (!created) { - throw new Error(`Failed to create config folder ${this.configDir}`); + throw new Error(`Failed to create config folder ${this.configDirectory}`); } } diff --git a/cli/src/version.ts b/cli/src/version.ts index e33764693a..6899251eea 100644 --- a/cli/src/version.ts +++ b/cli/src/version.ts @@ -1,4 +1,4 @@ -import pkg from '../package.json'; +import { version } from '../package.json'; export interface ICliVersion { major: number; @@ -23,7 +23,7 @@ export class CliVersion implements ICliVersion { } static fromString(version: string): CliVersion { - const regex = /(?:v)?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/i; + const regex = /v?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/i; const matchResult = version.match(regex); if (matchResult) { const [, major, minor, patch] = matchResult.map(Number); @@ -34,4 +34,4 @@ export class CliVersion implements ICliVersion { } } -export const cliVersion = CliVersion.fromString(pkg.version); +export const cliVersion = CliVersion.fromString(version); diff --git a/cli/test/e2e/login-key.e2e-spec.ts b/cli/test/e2e/login-key.e2e-spec.ts index b9761c6bd3..d1e7f780e3 100644 --- a/cli/test/e2e/login-key.e2e-spec.ts +++ b/cli/test/e2e/login-key.e2e-spec.ts @@ -10,10 +10,10 @@ describe(`login-key (e2e)`, () => { beforeAll(async () => { await testApp.create(); - if (!process.env.IMMICH_INSTANCE_URL) { - throw new Error('IMMICH_INSTANCE_URL environment variable not set'); - } else { + if (process.env.IMMICH_INSTANCE_URL) { instanceUrl = process.env.IMMICH_INSTANCE_URL; + } else { + throw new Error('IMMICH_INSTANCE_URL environment variable not set'); } }); diff --git a/cli/test/e2e/setup.ts b/cli/test/e2e/setup.ts index 09872b3adf..52b2ae082c 100644 --- a/cli/test/e2e/setup.ts +++ b/cli/test/e2e/setup.ts @@ -1,6 +1,11 @@ -import path from 'path'; +import path from 'node:path'; import { PostgreSqlContainer } from '@testcontainers/postgresql'; -import { access } from 'fs/promises'; +import { access } from 'node:fs/promises'; + +export const directoryExists = (directory: string) => + access(directory) + .then(() => true) + .catch(() => false); export default async () => { let IMMICH_TEST_ASSET_PATH: string = ''; @@ -12,11 +17,6 @@ export default async () => { IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH; } - const directoryExists = async (dirPath: string) => - await access(dirPath) - .then(() => true) - .catch(() => false); - if (!(await directoryExists(`${IMMICH_TEST_ASSET_PATH}/albums`))) { throw new Error( `Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${IMMICH_TEST_ASSET_PATH} before testing`, diff --git a/cli/test/e2e/vitest.config.ts b/cli/test/e2e/vitest.config.ts index ddb161c7ec..1657938765 100644 --- a/cli/test/e2e/vitest.config.ts +++ b/cli/test/e2e/vitest.config.ts @@ -17,6 +17,6 @@ export default defineConfig({ minForks: 1, }, }, - testTimeout: 10000, + testTimeout: 10_000, }, }); diff --git a/cli/tsconfig.json b/cli/tsconfig.json index 4576ca4b6f..3742f4c192 100644 --- a/cli/tsconfig.json +++ b/cli/tsconfig.json @@ -9,7 +9,7 @@ "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true, - "target": "es2021", + "target": "es2022", "sourceMap": true, "outDir": "./dist", "incremental": true, @@ -30,5 +30,5 @@ }, "types": ["vitest/globals"] }, - "exclude": ["dist", "node_modules", "upload"] + "exclude": ["dist", "node_modules"] } diff --git a/server/.eslintrc.js b/server/.eslintrc.js index 2e46281fe6..f1e6564d87 100644 --- a/server/.eslintrc.js +++ b/server/.eslintrc.js @@ -6,7 +6,7 @@ module.exports = { tsconfigRootDir: __dirname, }, plugins: ['@typescript-eslint/eslint-plugin'], - extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], + extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'], root: true, env: { node: true, @@ -19,6 +19,12 @@ module.exports = { '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-floating-promises': 'error', + 'unicorn/prevent-abbreviations': 'off', + 'unicorn/filename-case': 'off', + 'unicorn/no-null': 'off', + 'unicorn/prefer-top-level-await': 'off', + 'unicorn/prefer-event-target': 'off', + 'unicorn/no-thenable': 'off', curly: 2, 'prettier/prettier': 0, }, diff --git a/server/package-lock.json b/server/package-lock.json index 8470fb9c68..c01dc1567d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -80,9 +80,10 @@ "@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/parser": "^6.4.1", "dotenv": "^16.3.1", - "eslint": "^8.48.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-unicorn": "^50.0.1", "jest": "^29.6.4", "jest-when": "^3.6.0", "mock-fs": "^5.2.0", @@ -97,7 +98,7 @@ "ts-loader": "^9.4.4", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", - "typescript": "^5.2.2", + "typescript": "^5.3.3", "utimes": "^5.2.1" } }, @@ -3233,6 +3234,12 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, "node_modules/@types/pg": { "version": "8.10.9", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.9.tgz", @@ -4439,9 +4446,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", + "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", "dev": true, "funding": [ { @@ -4458,9 +4465,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001580", + "electron-to-chromium": "^1.4.648", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -4545,6 +4552,18 @@ "node": ">=10.0.0" } }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bullmq": { "version": "4.17.0", "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.17.0.tgz", @@ -4664,9 +4683,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001542", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001542.tgz", - "integrity": "sha512-UrtAXVcj1mvPBFQ4sKd38daP8dEcXXr5sQe6QNNinaPd0iA/cxg9/l3VrSdL73jgw5sKyuQ6jNgiKO12W3SsVA==", + "version": "1.0.30001581", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz", + "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==", "dev": true, "funding": [ { @@ -4791,6 +4810,27 @@ "validator": "^13.9.0" } }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -5111,6 +5151,19 @@ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, + "node_modules/core-js-compat": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", + "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==", + "dev": true, + "dependencies": { + "browserslist": "^4.22.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -5535,9 +5588,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.538", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.538.tgz", - "integrity": "sha512-1a2m63NEookb1beNFTGDihgF3CKL7ksZ7PSA0VloON5DpTEhnOVgaDes8xkrDhkXRxlcN8JymQDGnv+Nn+uvhg==", + "version": "1.4.650", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.650.tgz", + "integrity": "sha512-sYSQhJCJa4aGA1wYol5cMQgekDBlbVfTRavlGZVr3WZpDdOPcp6a6xUnFfrt8TqZhsBYYbDxJZCjGfHuGupCRQ==", "dev": true }, "node_modules/emittery": { @@ -5751,6 +5804,66 @@ } } }, + "node_modules/eslint-plugin-unicorn": { + "version": "50.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz", + "integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "@eslint-community/eslint-utils": "^4.4.0", + "@eslint/eslintrc": "^2.1.4", + "ci-info": "^4.0.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.34.0", + "esquery": "^1.5.0", + "indent-string": "^4.0.0", + "is-builtin-module": "^3.2.1", + "jsesc": "^3.0.2", + "pluralize": "^8.0.0", + "read-pkg-up": "^7.0.1", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.10.0", + "semver": "^7.5.4", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=8.56.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/ci-info": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", + "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -6884,6 +6997,12 @@ "node": "*" } }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -7019,6 +7138,15 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -7114,6 +7242,21 @@ "node": ">=8" } }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-core-module": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", @@ -8475,6 +8618,15 @@ "node": ">=6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -8818,9 +8970,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "node_modules/nopt": { @@ -8837,6 +8989,27 @@ "node": ">=6" } }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -9735,6 +9908,108 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -9838,6 +10113,36 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/regjsparser": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz", + "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, "node_modules/repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", @@ -10465,6 +10770,38 @@ "node": ">=0.10.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz", + "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "dev": true + }, "node_modules/split-ca": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", @@ -10672,6 +11009,18 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -11901,6 +12250,16 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/validator": { "version": "13.11.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", @@ -14439,6 +14798,12 @@ "undici-types": "~5.26.4" } }, + "@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, "@types/pg": { "version": "8.10.9", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.9.tgz", @@ -15402,14 +15767,14 @@ } }, "browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", + "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001580", + "electron-to-chromium": "^1.4.648", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" } }, @@ -15462,6 +15827,12 @@ "dev": true, "optional": true }, + "builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true + }, "bullmq": { "version": "4.17.0", "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.17.0.tgz", @@ -15553,9 +15924,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001542", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001542.tgz", - "integrity": "sha512-UrtAXVcj1mvPBFQ4sKd38daP8dEcXXr5sQe6QNNinaPd0iA/cxg9/l3VrSdL73jgw5sKyuQ6jNgiKO12W3SsVA==", + "version": "1.0.30001581", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz", + "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==", "dev": true }, "chalk": { @@ -15631,6 +16002,23 @@ "validator": "^13.9.0" } }, + "clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + } + } + }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -15874,6 +16262,15 @@ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, + "core-js-compat": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", + "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==", + "dev": true, + "requires": { + "browserslist": "^4.22.2" + } + }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -16185,9 +16582,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { - "version": "1.4.538", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.538.tgz", - "integrity": "sha512-1a2m63NEookb1beNFTGDihgF3CKL7ksZ7PSA0VloON5DpTEhnOVgaDes8xkrDhkXRxlcN8JymQDGnv+Nn+uvhg==", + "version": "1.4.650", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.650.tgz", + "integrity": "sha512-sYSQhJCJa4aGA1wYol5cMQgekDBlbVfTRavlGZVr3WZpDdOPcp6a6xUnFfrt8TqZhsBYYbDxJZCjGfHuGupCRQ==", "dev": true }, "emittery": { @@ -16369,6 +16766,44 @@ "synckit": "^0.8.6" } }, + "eslint-plugin-unicorn": { + "version": "50.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz", + "integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "@eslint-community/eslint-utils": "^4.4.0", + "@eslint/eslintrc": "^2.1.4", + "ci-info": "^4.0.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.34.0", + "esquery": "^1.5.0", + "indent-string": "^4.0.0", + "is-builtin-module": "^3.2.1", + "jsesc": "^3.0.2", + "pluralize": "^8.0.0", + "read-pkg-up": "^7.0.1", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.10.0", + "semver": "^7.5.4", + "strip-indent": "^3.0.0" + }, + "dependencies": { + "ci-info": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", + "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", + "dev": true + }, + "jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true + } + } + }, "eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -17192,6 +17627,12 @@ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -17277,6 +17718,12 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -17353,6 +17800,15 @@ "binary-extensions": "^2.0.0" } }, + "is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "requires": { + "builtin-modules": "^3.3.0" + } + }, "is-core-module": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", @@ -18401,6 +18857,12 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -18676,9 +19138,9 @@ "dev": true }, "node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "nopt": { @@ -18689,6 +19151,26 @@ "abbrev": "1" } }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -19338,6 +19820,82 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, "readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -19421,6 +19979,29 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, + "regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true + }, + "regjsparser": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz", + "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true + } + } + }, "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", @@ -19901,6 +20482,38 @@ } } }, + "spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz", + "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "dev": true + }, "split-ca": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", @@ -20063,6 +20676,15 @@ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -20892,6 +21514,16 @@ } } }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "validator": { "version": "13.11.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", diff --git a/server/package.json b/server/package.json index 12c36e3462..481c878481 100644 --- a/server/package.json +++ b/server/package.json @@ -105,9 +105,10 @@ "@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/parser": "^6.4.1", "dotenv": "^16.3.1", - "eslint": "^8.48.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-unicorn": "^50.0.1", "jest": "^29.6.4", "jest-when": "^3.6.0", "mock-fs": "^5.2.0", @@ -122,7 +123,7 @@ "ts-loader": "^9.4.4", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", - "typescript": "^5.2.2", + "typescript": "^5.3.3", "utimes": "^5.2.1" }, "jest": { diff --git a/server/src/domain/access/access.core.ts b/server/src/domain/access/access.core.ts index fe9d972239..8602701072 100644 --- a/server/src/domain/access/access.core.ts +++ b/server/src/domain/access/access.core.ts @@ -107,42 +107,51 @@ export class AccessCore { const sharedLinkId = sharedLink.id; switch (permission) { - case Permission.ASSET_READ: + case Permission.ASSET_READ: { return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids); + } - case Permission.ASSET_VIEW: + case Permission.ASSET_VIEW: { return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids); + } - case Permission.ASSET_DOWNLOAD: - return !!sharedLink.allowDownload + case Permission.ASSET_DOWNLOAD: { + return sharedLink.allowDownload ? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); + } - case Permission.ASSET_UPLOAD: + case Permission.ASSET_UPLOAD: { return sharedLink.allowUpload ? ids : new Set(); + } - case Permission.ASSET_SHARE: + case Permission.ASSET_SHARE: { // TODO: fix this to not use sharedLink.userId for access control return await this.repository.asset.checkOwnerAccess(sharedLink.userId, ids); + } - case Permission.ALBUM_READ: + case Permission.ALBUM_READ: { return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids); + } - case Permission.ALBUM_DOWNLOAD: - return !!sharedLink.allowDownload + case Permission.ALBUM_DOWNLOAD: { + return sharedLink.allowDownload ? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); + } - default: + default: { return new Set(); + } } } private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set<string>) { switch (permission) { // uses album id - case Permission.ACTIVITY_CREATE: + case Permission.ACTIVITY_CREATE: { return await this.repository.activity.checkCreateAccess(auth.user.id, ids); + } // uses activity id case Permission.ACTIVITY_DELETE: { @@ -190,14 +199,17 @@ export class AccessCore { return setUnion(isOwner, isAlbum, isPartner); } - case Permission.ASSET_UPDATE: + case Permission.ASSET_UPDATE: { return await this.repository.asset.checkOwnerAccess(auth.user.id, ids); + } - case Permission.ASSET_DELETE: + case Permission.ASSET_DELETE: { return await this.repository.asset.checkOwnerAccess(auth.user.id, ids); + } - case Permission.ASSET_RESTORE: + case Permission.ASSET_RESTORE: { return await this.repository.asset.checkOwnerAccess(auth.user.id, ids); + } case Permission.ALBUM_READ: { const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); @@ -205,14 +217,17 @@ export class AccessCore { return setUnion(isOwner, isShared); } - case Permission.ALBUM_UPDATE: + case Permission.ALBUM_UPDATE: { return await this.repository.album.checkOwnerAccess(auth.user.id, ids); + } - case Permission.ALBUM_DELETE: + case Permission.ALBUM_DELETE: { return await this.repository.album.checkOwnerAccess(auth.user.id, ids); + } - case Permission.ALBUM_SHARE: + case Permission.ALBUM_SHARE: { return await this.repository.album.checkOwnerAccess(auth.user.id, ids); + } case Permission.ALBUM_DOWNLOAD: { const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); @@ -220,17 +235,21 @@ export class AccessCore { return setUnion(isOwner, isShared); } - case Permission.ALBUM_REMOVE_ASSET: + case Permission.ALBUM_REMOVE_ASSET: { return await this.repository.album.checkOwnerAccess(auth.user.id, ids); + } - case Permission.ASSET_UPLOAD: + case Permission.ASSET_UPLOAD: { return await this.repository.library.checkOwnerAccess(auth.user.id, ids); + } - case Permission.ARCHIVE_READ: + case Permission.ARCHIVE_READ: { return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + } - case Permission.AUTH_DEVICE_DELETE: + case Permission.AUTH_DEVICE_DELETE: { return await this.repository.authDevice.checkOwnerAccess(auth.user.id, ids); + } case Permission.TIMELINE_READ: { const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>(); @@ -238,8 +257,9 @@ export class AccessCore { return setUnion(isOwner, isPartner); } - case Permission.TIMELINE_DOWNLOAD: + case Permission.TIMELINE_DOWNLOAD: { return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + } case Permission.LIBRARY_READ: { const isOwner = await this.repository.library.checkOwnerAccess(auth.user.id, ids); @@ -247,32 +267,41 @@ export class AccessCore { return setUnion(isOwner, isPartner); } - case Permission.LIBRARY_UPDATE: + case Permission.LIBRARY_UPDATE: { return await this.repository.library.checkOwnerAccess(auth.user.id, ids); + } - case Permission.LIBRARY_DELETE: + case Permission.LIBRARY_DELETE: { return await this.repository.library.checkOwnerAccess(auth.user.id, ids); + } - case Permission.PERSON_READ: + case Permission.PERSON_READ: { return await this.repository.person.checkOwnerAccess(auth.user.id, ids); + } - case Permission.PERSON_WRITE: + case Permission.PERSON_WRITE: { return await this.repository.person.checkOwnerAccess(auth.user.id, ids); + } - case Permission.PERSON_MERGE: + case Permission.PERSON_MERGE: { return await this.repository.person.checkOwnerAccess(auth.user.id, ids); + } - case Permission.PERSON_CREATE: + case Permission.PERSON_CREATE: { return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids); + } - case Permission.PERSON_REASSIGN: + case Permission.PERSON_REASSIGN: { return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids); + } - case Permission.PARTNER_UPDATE: + case Permission.PARTNER_UPDATE: { return await this.repository.partner.checkUpdateAccess(auth.user.id, ids); + } - default: + default: { return new Set(); + } } } } diff --git a/server/src/domain/activity/activity.service.ts b/server/src/domain/activity/activity.service.ts index 15482acaeb..69386f561e 100644 --- a/server/src/domain/activity/activity.service.ts +++ b/server/src/domain/activity/activity.service.ts @@ -35,7 +35,7 @@ export class ActivityService { isLiked: dto.type && dto.type === ReactionType.LIKE, }); - return activities.map(mapActivity); + return activities.map((activity) => mapActivity(activity)); } async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> { diff --git a/server/src/domain/album/album-response.dto.ts b/server/src/domain/album/album-response.dto.ts index 671922408e..1a266abac4 100644 --- a/server/src/domain/album/album-response.dto.ts +++ b/server/src/domain/album/album-response.dto.ts @@ -27,10 +27,11 @@ export class AlbumResponseDto { export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumResponseDto => { const sharedUsers: UserResponseDto[] = []; - entity.sharedUsers?.forEach((user) => { - const userDto = mapUser(user); - sharedUsers.push(userDto); - }); + if (entity.sharedUsers) { + for (const user of entity.sharedUsers) { + sharedUsers.push(mapUser(user)); + } + } const assets = entity.assets || []; @@ -41,9 +42,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons let endDate = assets.at(-1)?.fileCreatedAt || undefined; // Swap dates if start date is greater than end date. if (startDate && endDate && startDate > endDate) { - const temp = startDate; - startDate = endDate; - endDate = temp; + [startDate, endDate] = [endDate, startDate]; } return { diff --git a/server/src/domain/album/album.service.ts b/server/src/domain/album/album.service.ts index 898e3f5263..b14779a802 100644 --- a/server/src/domain/album/album.service.ts +++ b/server/src/domain/album/album.service.ts @@ -69,19 +69,17 @@ export class AlbumService { // Get asset count for each album. Then map the result to an object: // { [albumId]: assetCount } - const albumMetadataForIds = await this.albumRepository.getMetadataForIds(albums.map((album) => album.id)); - const albumMetadataForIdsObj: Record<string, AlbumAssetCount> = albumMetadataForIds.reduce( - (obj: Record<string, AlbumAssetCount>, { albumId, assetCount, startDate, endDate }) => { - obj[albumId] = { - albumId, - assetCount, - startDate, - endDate, - }; - return obj; - }, - {}, - ); + const results = await this.albumRepository.getMetadataForIds(albums.map((album) => album.id)); + const albumMetadata: Record<string, AlbumAssetCount> = {}; + for (const metadata of results) { + const { albumId, assetCount, startDate, endDate } = metadata; + albumMetadata[albumId] = { + albumId, + assetCount, + startDate, + endDate, + }; + } return Promise.all( albums.map(async (album) => { @@ -89,9 +87,9 @@ export class AlbumService { return { ...mapAlbumWithoutAssets(album), sharedLinks: undefined, - startDate: albumMetadataForIdsObj[album.id].startDate, - endDate: albumMetadataForIdsObj[album.id].endDate, - assetCount: albumMetadataForIdsObj[album.id].assetCount, + startDate: albumMetadata[album.id].startDate, + endDate: albumMetadata[album.id].endDate, + assetCount: albumMetadata[album.id].assetCount, lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt, }; }), diff --git a/server/src/domain/api-key/api-key.service.ts b/server/src/domain/api-key/api-key.service.ts index 2c4b6147af..0eef1981ce 100644 --- a/server/src/domain/api-key/api-key.service.ts +++ b/server/src/domain/api-key/api-key.service.ts @@ -12,7 +12,7 @@ export class APIKeyService { ) {} async create(auth: AuthDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> { - const secret = this.crypto.randomBytes(32).toString('base64').replace(/\W/g, ''); + const secret = this.crypto.randomBytes(32).toString('base64').replaceAll(/\W/g, ''); const entity = await this.repository.create({ key: this.crypto.hashSha256(secret), name: dto.name || 'API Key', diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/domain/asset/asset.service.spec.ts index 3cbe2068b6..a6b2cde3e8 100644 --- a/server/src/domain/asset/asset.service.spec.ts +++ b/server/src/domain/asset/asset.service.spec.ts @@ -1009,9 +1009,7 @@ describe(AssetService.name, () => { it('get assets by device id', async () => { const assets = [assetStub.image, assetStub.image1]; - assetMock.getAllByDeviceId.mockImplementation(() => - Promise.resolve<string[]>(Array.from(assets.map((asset) => asset.deviceAssetId))), - ); + assetMock.getAllByDeviceId.mockResolvedValue(assets.map((asset) => asset.deviceAssetId)); const deviceId = 'device-id'; const result = await sut.getUserAssetsByDeviceId(authStub.user1, deviceId); diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index 087a5ebcf2..e73858c311 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -3,7 +3,7 @@ import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException, Inject } from '@nestjs/common'; import _ from 'lodash'; import { DateTime, Duration } from 'luxon'; -import { extname } from 'path'; +import { extname } from 'node:path'; import sanitize from 'sanitize-filename'; import { AccessCore, Permission } from '../access'; import { AuthDto } from '../auth'; @@ -93,7 +93,7 @@ export class AssetService { } search(auth: AuthDto, dto: AssetSearchDto) { - let checksum: Buffer | undefined = undefined; + let checksum: Buffer | undefined; if (dto.checksum) { const encoding = dto.checksum.length === 28 ? 'base64' : 'hex'; @@ -126,29 +126,33 @@ export class AssetService { const filename = file.originalName; switch (fieldName) { - case UploadFieldName.ASSET_DATA: + case UploadFieldName.ASSET_DATA: { if (mimeTypes.isAsset(filename)) { return true; } break; + } - case UploadFieldName.LIVE_PHOTO_DATA: + case UploadFieldName.LIVE_PHOTO_DATA: { if (mimeTypes.isVideo(filename)) { return true; } break; + } - case UploadFieldName.SIDECAR_DATA: + case UploadFieldName.SIDECAR_DATA: { if (mimeTypes.isSidecar(filename)) { return true; } break; + } - case UploadFieldName.PROFILE_DATA: + case UploadFieldName.PROFILE_DATA: { if (mimeTypes.isProfile(filename)) { return true; } break; + } } this.logger.error(`Unsupported file type ${filename}`); @@ -158,13 +162,13 @@ export class AssetService { getUploadFilename({ auth, fieldName, file }: UploadRequest): string { this.access.requireUploadAccess(auth); - const originalExt = extname(file.originalName); + const originalExtension = extname(file.originalName); const lookup = { - [UploadFieldName.ASSET_DATA]: originalExt, + [UploadFieldName.ASSET_DATA]: originalExtension, [UploadFieldName.LIVE_PHOTO_DATA]: '.mov', [UploadFieldName.SIDECAR_DATA]: '.xmp', - [UploadFieldName.PROFILE_DATA]: originalExt, + [UploadFieldName.PROFILE_DATA]: originalExtension, }; return sanitize(`${file.uuid}${lookup[fieldName]}`); @@ -247,11 +251,9 @@ export class AssetService { await this.timeBucketChecks(auth, dto); const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto); const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions); - if (!auth.sharedLink || auth.sharedLink?.showExif) { - return assets.map((asset) => mapAsset(asset, { withStack: true })); - } else { - return assets.map((asset) => mapAsset(asset, { stripMetadata: true })); - } + return !auth.sharedLink || auth.sharedLink?.showExif + ? assets.map((asset) => mapAsset(asset, { withStack: true })) + : assets.map((asset) => mapAsset(asset, { stripMetadata: true })); } async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> { @@ -371,14 +373,14 @@ export class AssetService { const assetsWithChildren = assets.filter((a) => a.stack && a.stack.assets.length > 0); ids.push(...assetsWithChildren.flatMap((child) => child.stack!.assets.map((gChild) => gChild.id))); - if (!stack) { - stack = await this.assetStackRepository.create({ + if (stack) { + await this.assetStackRepository.update({ + id: stack.id, primaryAssetId: primaryAsset.id, assets: ids.map((id) => ({ id }) as AssetEntity), }); } else { - await this.assetStackRepository.update({ - id: stack.id, + stack = await this.assetStackRepository.create({ primaryAssetId: primaryAsset.id, assets: ids.map((id) => ({ id }) as AssetEntity), }); @@ -394,9 +396,10 @@ export class AssetService { } await this.assetRepository.updateAll(ids, options); - const stacksToDelete = ( - await Promise.all(stackIdsToCheckForDelete.map((id) => this.assetStackRepository.getById(id))) - ) + const stackIdsToDelete = await Promise.all( + stackIdsToCheckForDelete.map((id) => this.assetStackRepository.getById(id)), + ); + const stacksToDelete = stackIdsToDelete .flatMap((stack) => (stack ? [stack] : [])) .filter((stack) => stack.assets.length < 2); await Promise.all(stacksToDelete.map((as) => this.assetStackRepository.delete(as.id))); @@ -510,9 +513,8 @@ export class AssetService { throw new Error('Asset not found or not in a stack'); } if (oldParent != null) { - childIds.push(oldParent.id); // Get all children of old parent - childIds.push(...(oldParent.stack?.assets.map((a) => a.id) ?? [])); + childIds.push(oldParent.id, ...(oldParent.stack?.assets.map((a) => a.id) ?? [])); } await this.assetStackRepository.update({ id: oldParent.stackId, @@ -530,17 +532,20 @@ export class AssetService { for (const id of dto.assetIds) { switch (dto.name) { - case AssetJobName.REFRESH_METADATA: + case AssetJobName.REFRESH_METADATA: { jobs.push({ name: JobName.METADATA_EXTRACTION, data: { id } }); break; + } - case AssetJobName.REGENERATE_THUMBNAIL: + case AssetJobName.REGENERATE_THUMBNAIL: { jobs.push({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id } }); break; + } - case AssetJobName.TRANSCODE_VIDEO: + case AssetJobName.TRANSCODE_VIDEO: { jobs.push({ name: JobName.VIDEO_CONVERSION, data: { id } }); break; + } } } diff --git a/server/src/domain/asset/response-dto/asset-response.dto.ts b/server/src/domain/asset/response-dto/asset-response.dto.ts index d70e5963c4..94a9f8a42d 100644 --- a/server/src/domain/asset/response-dto/asset-response.dto.ts +++ b/server/src/domain/asset/response-dto/asset-response.dto.ts @@ -26,7 +26,6 @@ export class AssetResponseDto extends SanitizedAssetResponseDto { libraryId!: string; originalPath!: string; originalFileName!: string; - resized!: boolean; fileCreatedAt!: Date; fileModifiedAt!: Date; updatedAt!: Date; @@ -56,7 +55,7 @@ export type AssetMapOptions = { const peopleWithFaces = (faces: AssetFaceEntity[]): PersonWithFacesResponseDto[] => { const result: PersonWithFacesResponseDto[] = []; if (faces) { - faces.forEach((face) => { + for (const face of faces) { if (face.person) { const existingPersonEntry = result.find((item) => item.id === face.person!.id); if (existingPersonEntry) { @@ -65,7 +64,7 @@ const peopleWithFaces = (faces: AssetFaceEntity[]): PersonWithFacesResponseDto[] result.push({ ...mapPerson(face.person!), faces: [mapFacesWithoutPerson(face)] }); } } - }); + } } return result; diff --git a/server/src/domain/asset/response-dto/exif-response.dto.ts b/server/src/domain/asset/response-dto/exif-response.dto.ts index cb0f8399a1..f4d0226b47 100644 --- a/server/src/domain/asset/response-dto/exif-response.dto.ts +++ b/server/src/domain/asset/response-dto/exif-response.dto.ts @@ -33,7 +33,7 @@ export function mapExif(entity: ExifEntity): ExifResponseDto { model: entity.model, exifImageWidth: entity.exifImageWidth, exifImageHeight: entity.exifImageHeight, - fileSizeInByte: entity.fileSizeInByte ? parseInt(entity.fileSizeInByte.toString()) : null, + fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null, orientation: entity.orientation, dateTimeOriginal: entity.dateTimeOriginal, modifyDate: entity.modifyDate, @@ -55,7 +55,7 @@ export function mapExif(entity: ExifEntity): ExifResponseDto { export function mapSanitizedExif(entity: ExifEntity): ExifResponseDto { return { - fileSizeInByte: entity.fileSizeInByte ? parseInt(entity.fileSizeInByte.toString()) : null, + fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null, orientation: entity.orientation, dateTimeOriginal: entity.dateTimeOriginal, timeZone: entity.timeZone, diff --git a/server/src/domain/audit/audit.service.ts b/server/src/domain/audit/audit.service.ts index 1466d5fdb2..887b72e2cd 100644 --- a/server/src/domain/audit/audit.service.ts +++ b/server/src/domain/audit/audit.service.ts @@ -91,40 +91,50 @@ export class AuditService { } switch (pathType) { - case AssetPathType.ENCODED_VIDEO: + case AssetPathType.ENCODED_VIDEO: { await this.assetRepository.save({ id, encodedVideoPath: pathValue }); break; + } - case AssetPathType.JPEG_THUMBNAIL: + case AssetPathType.JPEG_THUMBNAIL: { await this.assetRepository.save({ id, resizePath: pathValue }); break; + } - case AssetPathType.WEBP_THUMBNAIL: + case AssetPathType.WEBP_THUMBNAIL: { await this.assetRepository.save({ id, webpPath: pathValue }); break; + } - case AssetPathType.ORIGINAL: + case AssetPathType.ORIGINAL: { await this.assetRepository.save({ id, originalPath: pathValue }); break; + } - case AssetPathType.SIDECAR: + case AssetPathType.SIDECAR: { await this.assetRepository.save({ id, sidecarPath: pathValue }); break; + } - case PersonPathType.FACE: + case PersonPathType.FACE: { await this.personRepository.update({ id, thumbnailPath: pathValue }); break; + } - case UserPathType.PROFILE: + case UserPathType.PROFILE: { await this.userRepository.update(id, { profileImagePath: pathValue }); break; + } } } } + private fullPath(filename: string) { + return resolve(filename); + } + async getFileReport() { - const fullPath = (filename: string) => resolve(filename); - const hasFile = (items: Set<string>, filename: string) => items.has(filename) || items.has(fullPath(filename)); + const hasFile = (items: Set<string>, filename: string) => items.has(filename) || items.has(this.fullPath(filename)); const crawl = async (folder: StorageFolder) => new Set( await this.storageRepository.crawl({ @@ -150,7 +160,7 @@ export class AuditService { return; } allFiles.delete(filename); - allFiles.delete(fullPath(filename)); + allFiles.delete(this.fullPath(filename)); }; this.logger.log( @@ -226,7 +236,7 @@ export class AuditService { // send as absolute paths for (const orphan of orphans) { - orphan.pathValue = fullPath(orphan.pathValue); + orphan.pathValue = this.fullPath(orphan.pathValue); } return { orphans, extras }; diff --git a/server/src/domain/auth/auth.service.spec.ts b/server/src/domain/auth/auth.service.spec.ts index ef3ee64b98..c04bbc2630 100644 --- a/server/src/domain/auth/auth.service.spec.ts +++ b/server/src/domain/auth/auth.service.spec.ts @@ -18,7 +18,7 @@ import { userStub, userTokenStub, } from '@test'; -import { IncomingHttpHeaders } from 'http'; +import { IncomingHttpHeaders } from 'node:http'; import { Issuer, generators } from 'openid-client'; import { Socket } from 'socket.io'; import { diff --git a/server/src/domain/auth/auth.service.ts b/server/src/domain/auth/auth.service.ts index fd527ee0d9..ff4ea43032 100644 --- a/server/src/domain/auth/auth.service.ts +++ b/server/src/domain/auth/auth.service.ts @@ -8,8 +8,8 @@ import { UnauthorizedException, } from '@nestjs/common'; import cookieParser from 'cookie'; -import { IncomingHttpHeaders } from 'http'; import { DateTime } from 'luxon'; +import { IncomingHttpHeaders } from 'node:http'; import { ClientMetadata, Issuer, UserinfoResponse, custom, generators } from 'openid-client'; import { AccessCore, Permission } from '../access'; import { @@ -85,7 +85,7 @@ export class AuthService { this.configCore = SystemConfigCore.create(configRepository); this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository); - custom.setHttpOptionsDefaults({ timeout: 30000 }); + custom.setHttpOptionsDefaults({ timeout: 30_000 }); } async login(dto: LoginCredentialDto, details: LoginDetails): Promise<LoginResponse> { @@ -213,7 +213,8 @@ export class AuthService { } const { scope, buttonText, autoLaunch } = config.oauth; - const url = (await this.getOAuthClient(config)).authorizationUrl({ + const oauthClient = await this.getOAuthClient(config); + const url = oauthClient.authorizationUrl({ redirect_uri: this.normalize(config, dto.redirectUri), scope, state: generators.state(), @@ -376,12 +377,10 @@ export class AuthService { const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url'); const sharedLink = await this.sharedLinkRepository.getByKey(bytes); - if (sharedLink) { - if (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date()) { - const user = sharedLink.user; - if (user) { - return { user, sharedLink }; - } + if (sharedLink && (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date())) { + const user = sharedLink.user; + if (user) { + return { user, sharedLink }; } } throw new UnauthorizedException('Invalid share key'); @@ -423,7 +422,7 @@ export class AuthService { } private async createLoginResponse(user: UserEntity, authType: AuthType, loginDetails: LoginDetails) { - const key = this.cryptoRepository.randomBytes(32).toString('base64').replace(/\W/g, ''); + const key = this.cryptoRepository.randomBytes(32).toString('base64').replaceAll(/\W/g, ''); const token = this.cryptoRepository.hashSha256(key); await this.userTokenRepository.create({ diff --git a/server/src/domain/database/database.service.ts b/server/src/domain/database/database.service.ts index 4c490ffc55..5af576a73b 100644 --- a/server/src/domain/database/database.service.ts +++ b/server/src/domain/database/database.service.ts @@ -50,14 +50,14 @@ export class DatabaseService { } private async createVectors() { - await this.databaseRepository.createExtension(DatabaseExtension.VECTORS).catch(async (err: QueryFailedError) => { + await this.databaseRepository.createExtension(DatabaseExtension.VECTORS).catch(async (error: QueryFailedError) => { const image = await this.getVectorsImage(); this.logger.fatal(` Failed to create pgvecto.rs extension. If you have not updated your Postgres instance to a docker image that supports pgvecto.rs (such as '${image}'), please do so. See the v1.91.0 release notes for more info: https://github.com/immich-app/immich/releases/tag/v1.91.0' `); - throw err; + throw error; }); } diff --git a/server/src/domain/domain.constant.spec.ts b/server/src/domain/domain.constant.spec.ts index 84cd4d8ee7..4ec4b1124c 100644 --- a/server/src/domain/domain.constant.spec.ts +++ b/server/src/domain/domain.constant.spec.ts @@ -108,9 +108,9 @@ describe('mimeTypes', () => { expect(keys).toEqual([...keys].sort()); }); - for (const [ext, v] of Object.entries(mimeTypes.profile)) { - it(`should lookup ${ext}`, () => { - expect(mimeTypes.lookup(`test.${ext}`)).toEqual(v[0]); + for (const [extension, v] of Object.entries(mimeTypes.profile)) { + it(`should lookup ${extension}`, () => { + expect(mimeTypes.lookup(`test.${extension}`)).toEqual(v[0]); }); } }); @@ -135,9 +135,9 @@ describe('mimeTypes', () => { expect(values).toEqual(values.filter((mimeType) => mimeType.startsWith('image/'))); }); - for (const [ext, v] of Object.entries(mimeTypes.image)) { - it(`should lookup ${ext}`, () => { - expect(mimeTypes.lookup(`test.${ext}`)).toEqual(v[0]); + for (const [extension, v] of Object.entries(mimeTypes.image)) { + it(`should lookup ${extension}`, () => { + expect(mimeTypes.lookup(`test.${extension}`)).toEqual(v[0]); }); } }); @@ -162,9 +162,9 @@ describe('mimeTypes', () => { expect(values).toEqual(values.filter((mimeType) => mimeType.startsWith('video/'))); }); - for (const [ext, v] of Object.entries(mimeTypes.video)) { - it(`should lookup ${ext}`, () => { - expect(mimeTypes.lookup(`test.${ext}`)).toEqual(v[0]); + for (const [extension, v] of Object.entries(mimeTypes.video)) { + it(`should lookup ${extension}`, () => { + expect(mimeTypes.lookup(`test.${extension}`)).toEqual(v[0]); }); } }); @@ -188,9 +188,9 @@ describe('mimeTypes', () => { expect(Object.values(mimeTypes.sidecar).flat()).toEqual(['application/xml', 'text/xml']); }); - for (const [ext, v] of Object.entries(mimeTypes.sidecar)) { - it(`should lookup ${ext}`, () => { - expect(mimeTypes.lookup(`it.${ext}`)).toEqual(v[0]); + for (const [extension, v] of Object.entries(mimeTypes.sidecar)) { + it(`should lookup ${extension}`, () => { + expect(mimeTypes.lookup(`it.${extension}`)).toEqual(v[0]); }); } }); diff --git a/server/src/domain/domain.constant.ts b/server/src/domain/domain.constant.ts index 5589eb15a6..227595e04f 100644 --- a/server/src/domain/domain.constant.ts +++ b/server/src/domain/domain.constant.ts @@ -3,8 +3,6 @@ import { Duration } from 'luxon'; import { readFileSync } from 'node:fs'; import { extname, join } from 'node:path'; -const pkg = JSON.parse(readFileSync('./package.json', 'utf-8')); - export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 }); export const ONE_HOUR = Duration.fromObject({ hours: 1 }); @@ -31,7 +29,7 @@ export class Version implements IVersion { } static fromString(version: string): Version { - const regex = /(?:v)?(?<major>\d+)(?:\.(?<minor>\d+))?(?:[\.-](?<patch>\d+))?/i; + const regex = /v?(?<major>\d+)(?:\.(?<minor>\d+))?(?:[.-](?<patch>\d+))?/i; const matchResult = version.match(regex); if (matchResult) { const { major, minor = '0', patch = '0' } = matchResult.groups as { [K in keyof IVersion]: string }; @@ -68,7 +66,8 @@ export class Version implements IVersion { export const envName = (process.env.NODE_ENV || 'development').toUpperCase(); export const isDev = process.env.NODE_ENV === 'development'; -export const serverVersion = Version.fromString(pkg.version); +const { version } = JSON.parse(readFileSync('./package.json', 'utf8')); +export const serverVersion = Version.fromString(version); export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload'; @@ -129,9 +128,9 @@ const image: Record<string, string[]> = { '.x3f': ['image/x3f', 'image/x-sigma-x3f'], }; -const profileExtensions = ['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp']; +const profileExtensions = new Set(['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp']); const profile: Record<string, string[]> = Object.fromEntries( - Object.entries(image).filter(([key]) => profileExtensions.includes(key)), + Object.entries(image).filter(([key]) => profileExtensions.has(key)), ); const video: Record<string, string[]> = { @@ -180,5 +179,5 @@ export const mimeTypes = { } return AssetType.OTHER; }, - getSupportedFileExtensions: () => Object.keys(image).concat(Object.keys(video)), + getSupportedFileExtensions: () => [...Object.keys(image), ...Object.keys(video)], }; diff --git a/server/src/domain/domain.util.ts b/server/src/domain/domain.util.ts index 79a1913b8e..ba5942cea4 100644 --- a/server/src/domain/domain.util.ts +++ b/server/src/domain/domain.util.ts @@ -46,7 +46,8 @@ export type Options = { export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED'; -export function ValidateUUID({ optional, each }: Options = { optional: false, each: false }) { +export function ValidateUUID(options?: Options) { + const { optional, each } = { optional: false, each: false, ...options }; return applyDecorators( IsUUID('4', { each }), ApiProperty({ format: 'uuid' }), @@ -58,7 +59,7 @@ export function ValidateUUID({ optional, each }: Options = { optional: false, ea export function validateCronExpression(expression: string) { try { new CronJob(expression, () => {}); - } catch (error) { + } catch { return false; } @@ -96,7 +97,7 @@ export const toBoolean = ({ value }: IValue) => { export const toEmail = ({ value }: IValue) => value?.toLowerCase(); -export const toSanitized = ({ value }: IValue) => sanitize((value || '').replace(/\./g, '')); +export const toSanitized = ({ value }: IValue) => sanitize((value || '').replaceAll('.', '')); export function getFileNameWithoutExtension(path: string): string { return basename(path, extname(path)); @@ -173,7 +174,7 @@ export function Optional({ nullable, ...validationOptions }: OptionalOptions = { return IsOptional(validationOptions); } - return ValidateIf((obj: any, v: any) => v !== undefined, validationOptions); + return ValidateIf((object: any, v: any) => v !== undefined, validationOptions); } /** @@ -186,8 +187,8 @@ export function chunks<T>(collection: Array<T> | Set<T>, size: number): T[][] { if (collection instanceof Set) { const result = []; let chunk = []; - for (const elem of collection) { - chunk.push(elem); + for (const element of collection) { + chunk.push(element); if (chunk.length === size) { result.push(chunk); chunk = []; @@ -209,8 +210,8 @@ export function chunks<T>(collection: Array<T> | Set<T>, size: number): T[][] { export const setUnion = <T>(...sets: Set<T>[]): Set<T> => { const union = new Set(sets[0]); for (const set of sets.slice(1)) { - for (const elem of set) { - union.add(elem); + for (const element of set) { + union.add(element); } } return union; @@ -219,16 +220,16 @@ export const setUnion = <T>(...sets: Set<T>[]): Set<T> => { export const setDifference = <T>(setA: Set<T>, ...sets: Set<T>[]): Set<T> => { const difference = new Set(setA); for (const set of sets) { - for (const elem of set) { - difference.delete(elem); + for (const element of set) { + difference.delete(element); } } return difference; }; export const setIsSuperset = <T>(set: Set<T>, subset: Set<T>): boolean => { - for (const elem of subset) { - if (!set.has(elem)) { + for (const element of subset) { + if (!set.has(element)) { return false; } } diff --git a/server/src/domain/download/download.service.ts b/server/src/domain/download/download.service.ts index 0b28942705..03bd6fee60 100644 --- a/server/src/domain/download/download.service.ts +++ b/server/src/domain/download/download.service.ts @@ -1,6 +1,6 @@ import { AssetEntity } from '@app/infra/entities'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { extname } from 'path'; +import { extname } from 'node:path'; import { AccessCore, Permission } from '../access'; import { AssetIdsDto } from '../asset'; import { AuthDto } from '../auth'; @@ -68,10 +68,12 @@ export class DownloadService { } } - return { - totalSize: archives.reduce((total, item) => (total += item.size), 0), - archives, - }; + let totalSize = 0; + for (const archive of archives) { + totalSize += archive.size; + } + + return { totalSize, archives }; } async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> { @@ -82,12 +84,12 @@ export class DownloadService { const paths: Record<string, number> = {}; for (const { originalPath, originalFileName } of assets) { - const ext = extname(originalPath); - let filename = `${originalFileName}${ext}`; + const extension = extname(originalPath); + let filename = `${originalFileName}${extension}`; const count = paths[filename] || 0; paths[filename] = count + 1; if (count !== 0) { - filename = `${originalFileName}+${count}${ext}`; + filename = `${originalFileName}+${count}${extension}`; } zip.addFile(originalPath, filename); diff --git a/server/src/domain/job/job.service.spec.ts b/server/src/domain/job/job.service.spec.ts index 4abeb309b6..58cda724f8 100644 --- a/server/src/domain/job/job.service.spec.ts +++ b/server/src/domain/job/job.service.spec.ts @@ -23,7 +23,7 @@ import { JobService } from './job.service'; const makeMockHandlers = (success: boolean) => { const mock = jest.fn().mockResolvedValue(success); - return Object.values(JobName).reduce((map, jobName) => ({ ...map, [jobName]: mock }), {}) as Record< + return Object.fromEntries(Object.values(JobName).map((jobName) => [jobName, mock])) as unknown as Record< JobName, JobHandler >; diff --git a/server/src/domain/job/job.service.ts b/server/src/domain/job/job.service.ts index a804cf658b..25055f1f31 100644 --- a/server/src/domain/job/job.service.ts +++ b/server/src/domain/job/job.service.ts @@ -36,26 +36,31 @@ export class JobService { this.logger.debug(`Handling command: queue=${queueName},force=${dto.force}`); switch (dto.command) { - case JobCommand.START: + case JobCommand.START: { await this.start(queueName, dto); break; + } - case JobCommand.PAUSE: + case JobCommand.PAUSE: { await this.jobRepository.pause(queueName); break; + } - case JobCommand.RESUME: + case JobCommand.RESUME: { await this.jobRepository.resume(queueName); break; + } - case JobCommand.EMPTY: + case JobCommand.EMPTY: { await this.jobRepository.empty(queueName); break; + } - case JobCommand.CLEAR_FAILED: + case JobCommand.CLEAR_FAILED: { const failedJobs = await this.jobRepository.clear(queueName, QueueCleanType.FAILED); this.logger.debug(`Cleared failed jobs: ${failedJobs}`); break; + } } return this.getJobStatus(queueName); @@ -85,42 +90,53 @@ export class JobService { } switch (name) { - case QueueName.VIDEO_CONVERSION: + case QueueName.VIDEO_CONVERSION: { return this.jobRepository.queue({ name: JobName.QUEUE_VIDEO_CONVERSION, data: { force } }); + } - case QueueName.STORAGE_TEMPLATE_MIGRATION: + case QueueName.STORAGE_TEMPLATE_MIGRATION: { return this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION }); + } - case QueueName.MIGRATION: + case QueueName.MIGRATION: { return this.jobRepository.queue({ name: JobName.QUEUE_MIGRATION }); + } - case QueueName.SMART_SEARCH: + case QueueName.SMART_SEARCH: { await this.configCore.requireFeature(FeatureFlag.SMART_SEARCH); return this.jobRepository.queue({ name: JobName.QUEUE_SMART_SEARCH, data: { force } }); + } - case QueueName.METADATA_EXTRACTION: + case QueueName.METADATA_EXTRACTION: { return this.jobRepository.queue({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force } }); + } - case QueueName.SIDECAR: + case QueueName.SIDECAR: { await this.configCore.requireFeature(FeatureFlag.SIDECAR); return this.jobRepository.queue({ name: JobName.QUEUE_SIDECAR, data: { force } }); + } - case QueueName.THUMBNAIL_GENERATION: + case QueueName.THUMBNAIL_GENERATION: { return this.jobRepository.queue({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force } }); + } - case QueueName.FACE_DETECTION: + case QueueName.FACE_DETECTION: { await this.configCore.requireFeature(FeatureFlag.FACIAL_RECOGNITION); return this.jobRepository.queue({ name: JobName.QUEUE_FACE_DETECTION, data: { force } }); + } - case QueueName.FACIAL_RECOGNITION: + case QueueName.FACIAL_RECOGNITION: { await this.configCore.requireFeature(FeatureFlag.FACIAL_RECOGNITION); return this.jobRepository.queue({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force } }); + } - case QueueName.LIBRARY: + case QueueName.LIBRARY: { return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force } }); + } - default: + default: { throw new BadRequestException(`Invalid job name: ${name}`); + } } } @@ -184,17 +200,19 @@ export class JobService { private async onDone(item: JobItem) { switch (item.name) { case JobName.SIDECAR_SYNC: - case JobName.SIDECAR_DISCOVERY: + case JobName.SIDECAR_DISCOVERY: { await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: item.data }); break; + } - case JobName.SIDECAR_WRITE: + case JobName.SIDECAR_WRITE: { await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: item.data.id, source: 'sidecar-write' }, }); + } - case JobName.METADATA_EXTRACTION: + case JobName.METADATA_EXTRACTION: { if (item.data.source === 'sidecar-write') { const [asset] = await this.assetRepository.getByIds([item.data.id]); if (asset) { @@ -203,24 +221,28 @@ export class JobService { } await this.jobRepository.queue({ name: JobName.LINK_LIVE_PHOTOS, data: item.data }); break; + } - case JobName.LINK_LIVE_PHOTOS: + case JobName.LINK_LIVE_PHOTOS: { await this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: item.data }); break; + } - case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: + case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: { if (item.data.source === 'upload') { await this.jobRepository.queue({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: item.data }); } break; + } - case JobName.GENERATE_PERSON_THUMBNAIL: + case JobName.GENERATE_PERSON_THUMBNAIL: { const { id } = item.data; const person = await this.personRepository.getById(id); if (person) { this.communicationRepository.send(ClientEvent.PERSON_THUMBNAIL, person.ownerId, person.id); } break; + } case JobName.GENERATE_JPEG_THUMBNAIL: { const jobs: JobItem[] = [ diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts index 4117f4129e..1af343318b 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/domain/library/library.service.spec.ts @@ -19,7 +19,7 @@ import { } from '@test'; import { newFSWatcherMock } from '@test/mocks'; -import { Stats } from 'fs'; +import { Stats } from 'node:fs'; import { ILibraryFileJob, ILibraryRefreshJob, JobName } from '../job'; import { IAssetRepository, @@ -116,12 +116,15 @@ describe(LibraryService.name, () => { libraryMock.get.mockImplementation(async (id) => { switch (id) { - case libraryStub.externalLibraryWithImportPaths1.id: + case libraryStub.externalLibraryWithImportPaths1.id: { return libraryStub.externalLibraryWithImportPaths1; - case libraryStub.externalLibraryWithImportPaths2.id: + } + case libraryStub.externalLibraryWithImportPaths2.id: { return libraryStub.externalLibraryWithImportPaths2; - default: + } + default: { return null; + } } }); @@ -532,7 +535,7 @@ describe(LibraryService.name, () => { }); it('should set a missing asset to offline', async () => { - storageMock.stat.mockRejectedValue(new Error()); + storageMock.stat.mockRejectedValue(new Error('Path not found')); const mockLibraryJob: ILibraryFileJob = { id: assetStub.image.id, @@ -1430,12 +1433,15 @@ describe(LibraryService.name, () => { libraryMock.get.mockImplementation(async (id) => { switch (id) { - case libraryStub.externalLibraryWithImportPaths1.id: + case libraryStub.externalLibraryWithImportPaths1.id: { return libraryStub.externalLibraryWithImportPaths1; - case libraryStub.externalLibraryWithImportPaths2.id: + } + case libraryStub.externalLibraryWithImportPaths2.id: { return libraryStub.externalLibraryWithImportPaths2; - default: + } + default: { return null; + } } }); diff --git a/server/src/domain/library/library.service.ts b/server/src/domain/library/library.service.ts index c064cd1b13..dc36c9d23b 100644 --- a/server/src/domain/library/library.service.ts +++ b/server/src/domain/library/library.service.ts @@ -1,17 +1,17 @@ import { AssetType, LibraryType } from '@app/infra/entities'; import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { EventEmitter } from 'events'; import { R_OK } from 'node:constants'; +import { EventEmitter } from 'node:events'; import { Stats } from 'node:fs'; -import path from 'node:path'; -import { basename, parse } from 'path'; +import path, { basename, parse } from 'node:path'; import picomatch from 'picomatch'; import { AccessCore, Permission } from '../access'; import { AuthDto } from '../auth'; import { mimeTypes } from '../domain.constant'; import { usePagination, validateCronExpression } from '../domain.util'; import { IBaseJob, IEntityJob, ILibraryFileJob, ILibraryRefreshJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; + import { IAccessRepository, IAssetRepository, @@ -84,11 +84,7 @@ export class LibraryService extends EventEmitter { if (library.watch.enabled !== this.watchLibraries) { this.watchLibraries = library.watch.enabled; - if (this.watchLibraries) { - await this.watchAll(); - } else { - await this.unwatchAll(); - } + await (this.watchLibraries ? this.watchAll() : this.unwatchAll()); } }); } @@ -227,12 +223,13 @@ export class LibraryService extends EventEmitter { async create(auth: AuthDto, dto: CreateLibraryDto): Promise<LibraryResponseDto> { switch (dto.type) { - case LibraryType.EXTERNAL: + case LibraryType.EXTERNAL: { if (!dto.name) { dto.name = 'New External Library'; } break; - case LibraryType.UPLOAD: + } + case LibraryType.UPLOAD: { if (!dto.name) { dto.name = 'New Upload Library'; } @@ -246,6 +243,7 @@ export class LibraryService extends EventEmitter { throw new BadRequestException('Upload libraries cannot be watched'); } break; + } } const library = await this.repository.create({ @@ -401,7 +399,7 @@ export class LibraryService extends EventEmitter { sidecarPath = `${assetPath}.xmp`; } - const deviceAssetId = `${basename(assetPath)}`.replace(/\s+/g, ''); + const deviceAssetId = `${basename(assetPath)}`.replaceAll(/\s+/g, ''); let assetId; if (doImport) { @@ -533,17 +531,17 @@ export class LibraryService extends EventEmitter { } this.logger.verbose(`Refreshing library: ${job.id}`); - const crawledAssetPaths = ( - await this.storageRepository.crawl({ - pathsToCrawl: library.importPaths, - exclusionPatterns: library.exclusionPatterns, - }) - ) - .map(path.normalize) + const rawPaths = await this.storageRepository.crawl({ + pathsToCrawl: library.importPaths, + exclusionPatterns: library.exclusionPatterns, + }); + + const crawledAssetPaths = rawPaths + .map((filePath) => path.normalize(filePath)) .filter((assetPath) => // Filter out paths that are not within the user's external path assetPath.match(new RegExp(`^${user.externalPath}`)), - ); + ) as string[]; this.logger.debug(`Found ${crawledAssetPaths.length} asset(s) when crawling import paths ${library.importPaths}`); const assetsInLibrary = await this.assetRepository.getByLibraryId([job.id]); diff --git a/server/src/domain/media/media.service.ts b/server/src/domain/media/media.service.ts index 6108ebf8da..68f861d7e2 100644 --- a/server/src/domain/media/media.service.ts +++ b/server/src/domain/media/media.service.ts @@ -181,13 +181,14 @@ export class MediaService { this.storageCore.ensureFolders(path); switch (asset.type) { - case AssetType.IMAGE: + case AssetType.IMAGE: { const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : thumbnail.colorspace; const thumbnailOptions = { format, size, colorspace, quality: thumbnail.quality }; await this.mediaRepository.resize(asset.originalPath, path, thumbnailOptions); break; + } - case AssetType.VIDEO: + case AssetType.VIDEO: { const { audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath); const mainVideoStream = this.getMainStream(videoStreams); if (!mainVideoStream) { @@ -199,9 +200,11 @@ export class MediaService { const options = new ThumbnailConfig(config).getOptions(mainVideoStream, mainAudioStream); await this.mediaRepository.transcode(asset.originalPath, path, options); break; + } - default: + default: { throw new UnsupportedMediaTypeException(`Unsupported asset type for thumbnail generation: ${asset.type}`); + } } this.logger.log( `Successfully generated ${format.toUpperCase()} ${asset.type.toLowerCase()} thumbnail for asset ${asset.id}`, @@ -297,16 +300,16 @@ export class MediaService { let transcodeOptions; try { transcodeOptions = await this.getCodecConfig(config).then((c) => c.getOptions(mainVideoStream, mainAudioStream)); - } catch (err) { - this.logger.error(`An error occurred while configuring transcoding options: ${err}`); + } catch (error) { + this.logger.error(`An error occurred while configuring transcoding options: ${error}`); return false; } this.logger.log(`Start encoding video ${asset.id} ${JSON.stringify(transcodeOptions)}`); try { await this.mediaRepository.transcode(input, output, transcodeOptions); - } catch (err) { - this.logger.error(err); + } catch (error) { + this.logger.error(error); if (config.accel !== TranscodeHWAccel.DISABLED) { this.logger.error( `Error occurred during transcoding. Retrying with ${config.accel.toUpperCase()} acceleration disabled.`, @@ -354,23 +357,29 @@ export class MediaService { const isLargerThanTargetBitrate = bitrate > this.parseBitrateToBps(ffmpegConfig.maxBitrate); switch (ffmpegConfig.transcode) { - case TranscodePolicy.DISABLED: + case TranscodePolicy.DISABLED: { return false; + } - case TranscodePolicy.ALL: + case TranscodePolicy.ALL: { return true; + } - case TranscodePolicy.REQUIRED: + case TranscodePolicy.REQUIRED: { return !allTargetsMatching || videoStream.isHDR; + } - case TranscodePolicy.OPTIMAL: + case TranscodePolicy.OPTIMAL: { return !allTargetsMatching || isLargerThanTargetRes || videoStream.isHDR; + } - case TranscodePolicy.BITRATE: + case TranscodePolicy.BITRATE: { return !allTargetsMatching || isLargerThanTargetBitrate || videoStream.isHDR; + } - default: + default: { return false; + } } } @@ -383,14 +392,18 @@ export class MediaService { private getSWCodecConfig(config: SystemConfigFFmpegDto) { switch (config.targetVideoCodec) { - case VideoCodec.H264: + case VideoCodec.H264: { return new H264Config(config); - case VideoCodec.HEVC: + } + case VideoCodec.HEVC: { return new HEVCConfig(config); - case VideoCodec.VP9: + } + case VideoCodec.VP9: { return new VP9Config(config); - default: + } + default: { throw new UnsupportedMediaTypeException(`Codec '${config.targetVideoCodec}' is unsupported`); + } } } @@ -398,23 +411,28 @@ export class MediaService { let handler: VideoCodecHWConfig; let devices: string[]; switch (config.accel) { - case TranscodeHWAccel.NVENC: + case TranscodeHWAccel.NVENC: { handler = new NVENCConfig(config); break; - case TranscodeHWAccel.QSV: + } + case TranscodeHWAccel.QSV: { devices = await this.storageRepository.readdir('/dev/dri'); handler = new QSVConfig(config, devices); break; - case TranscodeHWAccel.VAAPI: + } + case TranscodeHWAccel.VAAPI: { devices = await this.storageRepository.readdir('/dev/dri'); handler = new VAAPIConfig(config, devices); break; - case TranscodeHWAccel.RKMPP: + } + case TranscodeHWAccel.RKMPP: { devices = await this.storageRepository.readdir('/dev/dri'); handler = new RKMPPConfig(config, devices); break; - default: + } + default: { throw new UnsupportedMediaTypeException(`${config.accel.toUpperCase()} acceleration is unsupported`); + } } if (!handler.getSupportedCodecs().includes(config.targetVideoCodec)) { throw new UnsupportedMediaTypeException( @@ -441,14 +459,14 @@ export class MediaService { parseBitrateToBps(bitrateString: string) { const bitrateValue = Number.parseInt(bitrateString); - if (isNaN(bitrateValue)) { + if (Number.isNaN(bitrateValue)) { return 0; } if (bitrateString.toLowerCase().endsWith('k')) { return bitrateValue * 1000; // Kilobits per second to bits per second } else if (bitrateString.toLowerCase().endsWith('m')) { - return bitrateValue * 1000000; // Megabits per second to bits per second + return bitrateValue * 1_000_000; // Megabits per second to bits per second } else { return bitrateValue; } diff --git a/server/src/domain/media/media.util.ts b/server/src/domain/media/media.util.ts index 6166a6d5cf..ab3e43ec9f 100644 --- a/server/src/domain/media/media.util.ts +++ b/server/src/domain/media/media.util.ts @@ -15,16 +15,14 @@ class BaseConfig implements VideoCodecSWConfig { getOptions(videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) { const options = { inputOptions: this.getBaseInputOptions(), - outputOptions: this.getBaseOutputOptions(videoStream, audioStream).concat('-v verbose'), + outputOptions: [...this.getBaseOutputOptions(videoStream, audioStream), '-v verbose'], twoPass: this.eligibleForTwoPass(), } as TranscodeOptions; const filters = this.getFilterOptions(videoStream); if (filters.length > 0) { options.outputOptions.push(`-vf ${filters.join(',')}`); } - options.outputOptions.push(...this.getPresetOptions()); - options.outputOptions.push(...this.getThreadOptions()); - options.outputOptions.push(...this.getBitrateOptions()); + options.outputOptions.push(...this.getPresetOptions(), ...this.getThreadOptions(), ...this.getBitrateOptions()); return options; } @@ -129,11 +127,10 @@ class BaseConfig implements VideoCodecSWConfig { getTargetResolution(videoStream: VideoStreamInfo) { let target; - if (this.config.targetResolution === 'original') { - target = Math.min(videoStream.height, videoStream.width); - } else { - target = Number.parseInt(this.config.targetResolution); - } + target = + this.config.targetResolution === 'original' + ? Math.min(videoStream.height, videoStream.width) + : Number.parseInt(this.config.targetResolution); if (target % 2 !== 0) { target -= 1; @@ -182,7 +179,7 @@ class BaseConfig implements VideoCodecSWConfig { getBitrateUnit() { const maxBitrate = this.getMaxBitrateValue(); - return this.config.maxBitrate.trim().substring(maxBitrate.toString().length); // use inputted unit if provided + return this.config.maxBitrate.trim().slice(maxBitrate.toString().length); // use inputted unit if provided } getMaxBitrateValue() { @@ -411,8 +408,7 @@ export class NVENCConfig extends BaseHWConfig { ...super.getBaseOutputOptions(videoStream, audioStream), ]; if (this.getBFrames() > 0) { - options.push('-b_ref_mode middle'); - options.push('-b_qfactor 1.1'); + options.push('-b_ref_mode middle', '-b_qfactor 1.1'); } if (this.config.temporalAQ) { options.push('-temporal-aq 1'); @@ -474,8 +470,8 @@ export class NVENCConfig extends BaseHWConfig { export class QSVConfig extends BaseHWConfig { getBaseInputOptions() { - if (!this.devices.length) { - throw Error('No QSV device found'); + if (this.devices.length === 0) { + throw new Error('No QSV device found'); } let qsvString = ''; @@ -519,8 +515,7 @@ export class QSVConfig extends BaseHWConfig { options.push(`-${this.useCQP() ? 'q:v' : 'global_quality'} ${this.config.crf}`); const bitrates = this.getBitrateDistribution(); if (bitrates.max > 0) { - options.push(`-maxrate ${bitrates.max}${bitrates.unit}`); - options.push(`-bufsize ${bitrates.max * 2}${bitrates.unit}`); + options.push(`-maxrate ${bitrates.max}${bitrates.unit}`, `-bufsize ${bitrates.max * 2}${bitrates.unit}`); } return options; } @@ -623,7 +618,7 @@ export class RKMPPConfig extends BaseHWConfig { getBaseInputOptions() { if (this.devices.length === 0) { - throw Error('No RKMPP device found'); + throw new Error('No RKMPP device found'); } return []; } @@ -642,14 +637,17 @@ export class RKMPPConfig extends BaseHWConfig { getPresetOptions() { switch (this.config.targetVideoCodec) { - case VideoCodec.H264: + case VideoCodec.H264: { // from ffmpeg_mpp help, commonly referred to as H264 level 5.1 return ['-level 51']; - case VideoCodec.HEVC: + } + case VideoCodec.HEVC: { // from ffmpeg_mpp help, commonly referred to as HEVC level 5.1 return ['-level 153']; - default: - throw Error(`Incompatible video codec for RKMPP: ${this.config.targetVideoCodec}`); + } + default: { + throw new Error(`Incompatible video codec for RKMPP: ${this.config.targetVideoCodec}`); + } } } diff --git a/server/src/domain/metadata/metadata.service.spec.ts b/server/src/domain/metadata/metadata.service.spec.ts index 9a1e11893f..9a3a4bfed1 100644 --- a/server/src/domain/metadata/metadata.service.spec.ts +++ b/server/src/domain/metadata/metadata.service.spec.ts @@ -16,11 +16,11 @@ import { newSystemConfigRepositoryMock, probeStub, } from '@test'; -import { randomBytes } from 'crypto'; import { BinaryField } from 'exiftool-vendored'; -import { Stats } from 'fs'; -import { constants } from 'fs/promises'; import { when } from 'jest-when'; +import { randomBytes } from 'node:crypto'; +import { Stats } from 'node:fs'; +import { constants } from 'node:fs/promises'; import { JobName } from '../job'; import { ClientEvent, @@ -234,7 +234,7 @@ describe(MetadataService.name, () => { describe('handleMetadataExtraction', () => { beforeEach(() => { - storageMock.stat.mockResolvedValue({ size: 123456 } as Stats); + storageMock.stat.mockResolvedValue({ size: 123_456 } as Stats); }); it('should handle an asset that could not be found', async () => { @@ -507,7 +507,7 @@ describe(MetadataService.name, () => { exifImageWidth: null, exposureTime: tags.ExposureTime, fNumber: null, - fileSizeInByte: 123456, + fileSizeInByte: 123_456, focalLength: tags.FocalLength, fps: null, iso: tags.ISO, @@ -565,7 +565,7 @@ describe(MetadataService.name, () => { it('should handle duration with scale', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.readTags.mockResolvedValue({ Duration: { Scale: 1.11111111111111e-5, Value: 558720 } }); + metadataMock.readTags.mockResolvedValue({ Duration: { Scale: 1.111_111_111_111_11e-5, Value: 558_720 } }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts index 00d8412c8d..12ad57006d 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/domain/metadata/metadata.service.ts @@ -3,9 +3,9 @@ import { ImmichLogger } from '@app/infra/logger'; import { Inject, Injectable } from '@nestjs/common'; import { ExifDateTime, Tags } from 'exiftool-vendored'; import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime'; -import { constants } from 'fs/promises'; import _ from 'lodash'; import { Duration } from 'luxon'; +import { constants } from 'node:fs/promises'; import { Subscription } from 'rxjs'; import { usePagination } from '../domain.util'; import { IBaseJob, IEntityJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job'; @@ -85,7 +85,7 @@ const validate = <T>(value: T): NonNullable<T> | null => { return null; } - if (typeof value === 'number' && (isNaN(value) || !isFinite(value))) { + if (typeof value === 'number' && (Number.isNaN(value) || !Number.isFinite(value))) { return null; } @@ -217,18 +217,22 @@ export class MetadataService { if (videoStreams[0]) { switch (videoStreams[0].rotation) { - case -90: + case -90: { exifData.orientation = Orientation.Rotate90CW; break; - case 0: + } + case 0: { exifData.orientation = Orientation.Horizontal; break; - case 90: + } + case 90: { exifData.orientation = Orientation.Rotate270CW; break; - case 180: + } + case 180: { exifData.orientation = Orientation.Rotate180; break; + } } } } @@ -243,7 +247,7 @@ export class MetadataService { const timeZoneOffset = tzOffset(firstDateTime(tags as Tags)) ?? 0; if (dateTimeOriginal && timeZoneOffset) { - localDateTime = new Date(dateTimeOriginal.getTime() + timeZoneOffset * 60000); + localDateTime = new Date(dateTimeOriginal.getTime() + timeZoneOffset * 60_000); } await this.assetRepository.save({ id: asset.id, @@ -413,7 +417,13 @@ export class MetadataService { const checksum = this.cryptoRepository.hashSha1(video); let motionAsset = await this.assetRepository.getByChecksum(asset.ownerId, checksum); - if (!motionAsset) { + if (motionAsset) { + this.logger.debug( + `Asset ${asset.id}'s motion photo video with checksum ${checksum.toString( + 'base64', + )} already exists in the repository`, + ); + } else { // We create a UUID in advance so that each extracted video can have a unique filename // (allowing us to delete old ones if necessary) const motionAssetId = this.cryptoRepository.randomUUID(); @@ -448,12 +458,6 @@ export class MetadataService { await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } }); this.logger.log(`Removed old motion photo video asset (${asset.livePhotoVideoId})`); } - } else { - this.logger.debug( - `Asset ${asset.id}'s motion photo video with checksum ${checksum.toString( - 'base64', - )} already exists in the repository`, - ); } this.logger.debug(`Finished motion photo video extraction (${asset.id})`); @@ -494,7 +498,7 @@ export class MetadataService { fileSizeInByte: stats.size, fNumber: validate(tags.FNumber), focalLength: validate(tags.FocalLength), - fps: validate(parseFloat(tags.VideoFrameRate!)), + fps: validate(Number.parseFloat(tags.VideoFrameRate!)), iso: validate(tags.ISO), latitude: validate(tags.GPSLatitude), lensModel: tags.LensModel ?? null, diff --git a/server/src/domain/partner/partner.service.ts b/server/src/domain/partner/partner.service.ts index 7a9cf182b4..a3f9a9f3df 100644 --- a/server/src/domain/partner/partner.service.ts +++ b/server/src/domain/partner/partner.service.ts @@ -24,7 +24,7 @@ export class PartnerService { } const partner = await this.repository.create(partnerId); - return this.map(partner, PartnerDirection.SharedBy); + return this.mapToPartnerEntity(partner, PartnerDirection.SharedBy); } async remove(auth: AuthDto, sharedWithId: string): Promise<void> { @@ -43,7 +43,7 @@ export class PartnerService { return partners .filter((partner) => partner.sharedBy && partner.sharedWith) // Filter out soft deleted users .filter((partner) => partner[key] === auth.user.id) - .map((partner) => this.map(partner, direction)); + .map((partner) => this.mapToPartnerEntity(partner, direction)); } async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> { @@ -51,10 +51,10 @@ export class PartnerService { const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id }; const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline }); - return this.map(entity, PartnerDirection.SharedWith); + return this.mapToPartnerEntity(entity, PartnerDirection.SharedWith); } - private map(partner: PartnerEntity, direction: PartnerDirection): PartnerResponseDto { + private mapToPartnerEntity(partner: PartnerEntity, direction: PartnerDirection): PartnerResponseDto { // this is opposite to return the non-me user of the "partner" const user = mapUser( direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy, diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index d5b7a27d7a..e1937524af 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -814,7 +814,7 @@ describe(PersonService.name, () => { } const faces = [ - { face: faceStub.noPerson1, distance: 0.0 }, + { face: faceStub.noPerson1, distance: 0 }, { face: faceStub.primaryFace1, distance: 0.2 }, { face: faceStub.noPerson2, distance: 0.3 }, { face: faceStub.face1, distance: 0.4 }, @@ -843,7 +843,7 @@ describe(PersonService.name, () => { it('should create a new person if the face is a core point with no person', async () => { const faces = [ - { face: faceStub.noPerson1, distance: 0.0 }, + { face: faceStub.noPerson1, distance: 0 }, { face: faceStub.noPerson2, distance: 0.3 }, ] as FaceSearchResult[]; @@ -867,7 +867,7 @@ describe(PersonService.name, () => { }); it('should defer non-core faces to end of queue', async () => { - const faces = [{ face: faceStub.noPerson1, distance: 0.0 }] as FaceSearchResult[]; + const faces = [{ face: faceStub.noPerson1, distance: 0 }] as FaceSearchResult[]; configMock.load.mockResolvedValue([ { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 2 }, @@ -888,7 +888,7 @@ describe(PersonService.name, () => { }); it('should not assign person to non-core face with no matching person', async () => { - const faces = [{ face: faceStub.noPerson1, distance: 0.0 }] as FaceSearchResult[]; + const faces = [{ face: faceStub.noPerson1, distance: 0 }] as FaceSearchResult[]; configMock.load.mockResolvedValue([ { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 2 }, diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index 39c7eb9439..576f94c491 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -122,7 +122,7 @@ export class PersonService { } if (changeFeaturePhoto.length > 0) { // Remove duplicates - await this.createNewFeaturePhoto(Array.from(new Set(changeFeaturePhoto))); + await this.createNewFeaturePhoto([...new Set(changeFeaturePhoto)]); } return result; } @@ -332,7 +332,7 @@ export class PersonService { this.logger.debug(`${faces.length} faces detected in ${asset.resizePath}`); this.logger.verbose(faces.map((face) => ({ ...face, embedding: `vector(${face.embedding.length})` }))); - if (faces.length) { + if (faces.length > 0) { await this.jobRepository.queue({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }); const mappedFaces = faces.map((face) => ({ @@ -417,7 +417,7 @@ export class PersonService { numResults: machineLearning.facialRecognition.minFaces, }); - this.logger.debug(`Face ${id} has ${matches.length} match${matches.length != 1 ? 'es' : ''}`); + this.logger.debug(`Face ${id} has ${matches.length} match${matches.length == 1 ? '' : 'es'}`); const isCore = matches.length >= machineLearning.facialRecognition.minFaces; if (!isCore && !deferred) { diff --git a/server/src/domain/repositories/database.repository.ts b/server/src/domain/repositories/database.repository.ts index 496081ddb2..07d0afca6b 100644 --- a/server/src/domain/repositories/database.repository.ts +++ b/server/src/domain/repositories/database.repository.ts @@ -15,7 +15,7 @@ export enum DatabaseLock { export const IDatabaseRepository = 'IDatabaseRepository'; export interface IDatabaseRepository { - getExtensionVersion(extName: string): Promise<Version | null>; + getExtensionVersion(extensionName: string): Promise<Version | null>; getPostgresVersion(): Promise<Version>; createExtension(extension: DatabaseExtension): Promise<void>; runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise<void>; diff --git a/server/src/domain/repositories/media.repository.ts b/server/src/domain/repositories/media.repository.ts index 72da627bf5..60135e62dc 100644 --- a/server/src/domain/repositories/media.repository.ts +++ b/server/src/domain/repositories/media.repository.ts @@ -1,5 +1,5 @@ import { VideoCodec } from '@app/infra/entities'; -import { Writable } from 'stream'; +import { Writable } from 'node:stream'; export const IMediaRepository = 'IMediaRepository'; diff --git a/server/src/domain/repositories/storage.repository.ts b/server/src/domain/repositories/storage.repository.ts index 8a01c73d51..c55aaf7ecd 100644 --- a/server/src/domain/repositories/storage.repository.ts +++ b/server/src/domain/repositories/storage.repository.ts @@ -1,7 +1,7 @@ import { FSWatcher, WatchOptions } from 'chokidar'; -import { Stats } from 'fs'; -import { FileReadOptions } from 'fs/promises'; -import { Readable } from 'stream'; +import { Stats } from 'node:fs'; +import { FileReadOptions } from 'node:fs/promises'; +import { Readable } from 'node:stream'; import { CrawlOptionsDto } from '../library'; export interface ImmichReadStream { diff --git a/server/src/domain/search/search.service.ts b/server/src/domain/search/search.service.ts index 8695c26e0d..932a865d04 100644 --- a/server/src/domain/search/search.service.ts +++ b/server/src/domain/search/search.service.ts @@ -46,7 +46,7 @@ export class SearchService { this.assetRepository.getAssetIdByTag(auth.user.id, options), ]); const assetIds = new Set<string>(results.flatMap((field) => field.items.map((item) => item.data))); - const assets = await this.assetRepository.getByIds(Array.from(assetIds)); + const assets = await this.assetRepository.getByIds([...assetIds]); const assetMap = new Map<string, AssetResponseDto>(assets.map((asset) => [asset.id, mapAsset(asset)])); return results.map(({ fieldName, items }) => ({ @@ -75,7 +75,7 @@ export class SearchService { let assets: AssetEntity[] = []; switch (strategy) { - case SearchStrategy.SMART: + case SearchStrategy.SMART: { const embedding = await this.machineLearning.encodeText( machineLearning.url, { text: query }, @@ -88,10 +88,13 @@ export class SearchService { withArchived, }); break; - case SearchStrategy.TEXT: + } + case SearchStrategy.TEXT: { assets = await this.assetRepository.searchMetadata(query, userIds, { numResults: 250 }); - default: + } + default: { break; + } } return { diff --git a/server/src/domain/server-info/server-info.service.spec.ts b/server/src/domain/server-info/server-info.service.spec.ts index 1f1b51055b..e097509e6a 100644 --- a/server/src/domain/server-info/server-info.service.spec.ts +++ b/server/src/domain/server-info/server-info.service.spec.ts @@ -71,12 +71,12 @@ describe(ServerInfoService.name, () => { await expect(sut.getInfo()).resolves.toEqual({ diskAvailable: '293.0 KiB', - diskAvailableRaw: 300000, + diskAvailableRaw: 300_000, diskSize: '488.3 KiB', - diskSizeRaw: 500000, + diskSizeRaw: 500_000, diskUsagePercentage: 60, diskUse: '293.0 KiB', - diskUseRaw: 300000, + diskUseRaw: 300_000, }); expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library'); @@ -87,12 +87,12 @@ describe(ServerInfoService.name, () => { await expect(sut.getInfo()).resolves.toEqual({ diskAvailable: '286.1 MiB', - diskAvailableRaw: 300000000, + diskAvailableRaw: 300_000_000, diskSize: '476.8 MiB', - diskSizeRaw: 500000000, + diskSizeRaw: 500_000_000, diskUsagePercentage: 60, diskUse: '286.1 MiB', - diskUseRaw: 300000000, + diskUseRaw: 300_000_000, }); expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library'); @@ -107,12 +107,12 @@ describe(ServerInfoService.name, () => { await expect(sut.getInfo()).resolves.toEqual({ diskAvailable: '279.4 GiB', - diskAvailableRaw: 300000000000, + diskAvailableRaw: 300_000_000_000, diskSize: '465.7 GiB', - diskSizeRaw: 500000000000, + diskSizeRaw: 500_000_000_000, diskUsagePercentage: 60, diskUse: '279.4 GiB', - diskUseRaw: 300000000000, + diskUseRaw: 300_000_000_000, }); expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library'); @@ -127,12 +127,12 @@ describe(ServerInfoService.name, () => { await expect(sut.getInfo()).resolves.toEqual({ diskAvailable: '272.8 TiB', - diskAvailableRaw: 300000000000000, + diskAvailableRaw: 300_000_000_000_000, diskSize: '454.7 TiB', - diskSizeRaw: 500000000000000, + diskSizeRaw: 500_000_000_000_000, diskUsagePercentage: 60, diskUse: '272.8 TiB', - diskUseRaw: 300000000000000, + diskUseRaw: 300_000_000_000_000, }); expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library'); @@ -147,12 +147,12 @@ describe(ServerInfoService.name, () => { await expect(sut.getInfo()).resolves.toEqual({ diskAvailable: '266.5 PiB', - diskAvailableRaw: 300000000000000000, + diskAvailableRaw: 300_000_000_000_000_000, diskSize: '444.1 PiB', - diskSizeRaw: 500000000000000000, + diskSizeRaw: 500_000_000_000_000_000, diskUsagePercentage: 60, diskUse: '266.5 PiB', - diskUseRaw: 300000000000000000, + diskUseRaw: 300_000_000_000_000_000, }); expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library'); @@ -219,7 +219,7 @@ describe(ServerInfoService.name, () => { userName: '1 User', photos: 10, videos: 11, - usage: 12345, + usage: 12_345, quotaSizeInBytes: 0, }, { @@ -227,7 +227,7 @@ describe(ServerInfoService.name, () => { userName: '2 User', photos: 10, videos: 20, - usage: 123456, + usage: 123_456, quotaSizeInBytes: 0, }, { @@ -235,7 +235,7 @@ describe(ServerInfoService.name, () => { userName: '3 User', photos: 100, videos: 0, - usage: 987654, + usage: 987_654, quotaSizeInBytes: 0, }, ]); @@ -243,12 +243,12 @@ describe(ServerInfoService.name, () => { await expect(sut.getStatistics()).resolves.toEqual({ photos: 120, videos: 31, - usage: 1123455, + usage: 1_123_455, usageByUser: [ { photos: 10, quotaSizeInBytes: 0, - usage: 12345, + usage: 12_345, userName: '1 User', userId: 'user1', videos: 11, @@ -256,7 +256,7 @@ describe(ServerInfoService.name, () => { { photos: 10, quotaSizeInBytes: 0, - usage: 123456, + usage: 123_456, userName: '2 User', userId: 'user2', videos: 20, @@ -264,7 +264,7 @@ describe(ServerInfoService.name, () => { { photos: 100, quotaSizeInBytes: 0, - usage: 987654, + usage: 987_654, userName: '3 User', userId: 'user3', videos: 0, diff --git a/server/src/domain/server-info/server-info.service.ts b/server/src/domain/server-info/server-info.service.ts index 7da045a18a..51d26b2c3d 100644 --- a/server/src/domain/server-info/server-info.service.ts +++ b/server/src/domain/server-info/server-info.service.ts @@ -67,7 +67,7 @@ export class ServerInfoService { serverInfo.diskAvailableRaw = diskInfo.available; serverInfo.diskSizeRaw = diskInfo.total; serverInfo.diskUseRaw = diskInfo.total - diskInfo.free; - serverInfo.diskUsagePercentage = parseFloat(usagePercentage); + serverInfo.diskUsagePercentage = Number.parseFloat(usagePercentage); return serverInfo; } diff --git a/server/src/domain/shared-link/shared-link.service.ts b/server/src/domain/shared-link/shared-link.service.ts index b2b488138f..54e6f60521 100644 --- a/server/src/domain/shared-link/shared-link.service.ts +++ b/server/src/domain/shared-link/shared-link.service.ts @@ -21,7 +21,7 @@ export class SharedLinkService { } getAll(auth: AuthDto): Promise<SharedLinkResponseDto[]> { - return this.repository.getAll(auth.user.id).then((links) => links.map(mapSharedLink)); + return this.repository.getAll(auth.user.id).then((links) => links.map((link) => mapSharedLink(link))); } async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise<SharedLinkResponseDto> { @@ -30,7 +30,7 @@ export class SharedLinkService { } const sharedLink = await this.findOrFail(auth.user.id, auth.sharedLink.id); - const response = this.map(sharedLink, { withExif: sharedLink.showExif }); + const response = this.mapToSharedLink(sharedLink, { withExif: sharedLink.showExif }); if (sharedLink.password) { response.token = this.validateAndRefreshToken(sharedLink, dto); } @@ -40,19 +40,20 @@ export class SharedLinkService { async get(auth: AuthDto, id: string): Promise<SharedLinkResponseDto> { const sharedLink = await this.findOrFail(auth.user.id, id); - return this.map(sharedLink, { withExif: true }); + return this.mapToSharedLink(sharedLink, { withExif: true }); } async create(auth: AuthDto, dto: SharedLinkCreateDto): Promise<SharedLinkResponseDto> { switch (dto.type) { - case SharedLinkType.ALBUM: + case SharedLinkType.ALBUM: { if (!dto.albumId) { throw new BadRequestException('Invalid albumId'); } await this.access.requirePermission(auth, Permission.ALBUM_SHARE, dto.albumId); break; + } - case SharedLinkType.INDIVIDUAL: + case SharedLinkType.INDIVIDUAL: { if (!dto.assetIds || dto.assetIds.length === 0) { throw new BadRequestException('Invalid assetIds'); } @@ -60,6 +61,7 @@ export class SharedLinkService { await this.access.requirePermission(auth, Permission.ASSET_SHARE, dto.assetIds); break; + } } const sharedLink = await this.repository.create({ @@ -76,7 +78,7 @@ export class SharedLinkService { showExif: dto.showMetadata ?? true, }); - return this.map(sharedLink, { withExif: true }); + return this.mapToSharedLink(sharedLink, { withExif: true }); } async update(auth: AuthDto, id: string, dto: SharedLinkEditDto) { @@ -91,7 +93,7 @@ export class SharedLinkService { allowDownload: dto.allowDownload, showExif: dto.showMetadata, }); - return this.map(sharedLink, { withExif: true }); + return this.mapToSharedLink(sharedLink, { withExif: true }); } async remove(auth: AuthDto, id: string): Promise<void> { @@ -173,7 +175,7 @@ export class SharedLinkService { const sharedLink = await this.findOrFail(auth.sharedLink.userId, auth.sharedLink.id); const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id; - const assetCount = sharedLink.assets.length || sharedLink.album?.assets.length || 0; + const assetCount = sharedLink.assets.length ?? sharedLink.album?.assets.length ?? 0; return { title: sharedLink.album ? sharedLink.album.albumName : 'Public Share', @@ -184,7 +186,7 @@ export class SharedLinkService { }; } - private map(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) { + private mapToSharedLink(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) { return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithoutMetadata(sharedLink); } diff --git a/server/src/domain/smart-info/smart-info.constant.ts b/server/src/domain/smart-info/smart-info.constant.ts index 3867d0885a..66c31b9851 100644 --- a/server/src/domain/smart-info/smart-info.constant.ts +++ b/server/src/domain/smart-info/smart-info.constant.ts @@ -111,8 +111,12 @@ export const CLIP_MODEL_INFO: Record<string, ModelInfo> = { }; export function cleanModelName(modelName: string): string { - const tokens = modelName.split('/'); - return tokens[tokens.length - 1].replace(/:/g, '_'); + const token = modelName.split('/').at(-1); + if (!token) { + throw new Error(`Invalid model name: ${modelName}`); + } + + return token.replaceAll(':', '_'); } export function getCLIPModelInfo(modelName: string): ModelInfo { diff --git a/server/src/domain/storage-template/storage-template.service.spec.ts b/server/src/domain/storage-template/storage-template.service.spec.ts index a49f0347aa..6e17ca64e9 100644 --- a/server/src/domain/storage-template/storage-template.service.spec.ts +++ b/server/src/domain/storage-template/storage-template.service.spec.ts @@ -269,7 +269,7 @@ describe(StorageTemplateService.name, () => { when(storageMock.stat) .calledWith(newPath) .mockResolvedValue({ size: 5000 } as Stats); - when(cryptoMock.hashFile).calledWith(newPath).mockResolvedValue(Buffer.from('different-hash', 'utf-8')); + when(cryptoMock.hashFile).calledWith(newPath).mockResolvedValue(Buffer.from('different-hash', 'utf8')); when(assetMock.save) .calledWith({ id: assetStub.image.id, originalPath: newPath }) @@ -311,9 +311,9 @@ describe(StorageTemplateService.name, () => { }); it.each` - failedPathChecksum | failedPathSize | reason - ${assetStub.image.checksum} | ${500} | ${'file size'} - ${Buffer.from('bad checksum', 'utf-8')} | ${assetStub.image.exifInfo?.fileSizeInByte} | ${'checksum'} + failedPathChecksum | failedPathSize | reason + ${assetStub.image.checksum} | ${500} | ${'file size'} + ${Buffer.from('bad checksum', 'utf8')} | ${assetStub.image.exifInfo?.fileSizeInByte} | ${'checksum'} `( 'should fail to migrate previously failed move from previous new path when old path no longer exists if $reason validation fails', async ({ failedPathChecksum, failedPathSize }) => { diff --git a/server/src/domain/storage-template/storage-template.service.ts b/server/src/domain/storage-template/storage-template.service.ts index cbed4a06c9..d696982540 100644 --- a/server/src/domain/storage-template/storage-template.service.ts +++ b/server/src/domain/storage-template/storage-template.service.ts @@ -86,7 +86,8 @@ export class StorageTemplateService { } async handleMigrationSingle({ id }: IEntityJob) { - const storageTemplateEnabled = (await this.configCore.getConfig()).storageTemplate.enabled; + const config = await this.configCore.getConfig(); + const storageTemplateEnabled = config.storageTemplate.enabled; if (!storageTemplateEnabled) { return true; } @@ -109,8 +110,9 @@ export class StorageTemplateService { async handleMigration() { this.logger.log('Starting storage template migration'); - const storageTemplateEnabled = (await this.configCore.getConfig()).storageTemplate.enabled; - if (!storageTemplateEnabled) { + const { storageTemplate } = await this.configCore.getConfig(); + const { enabled } = storageTemplate; + if (!enabled) { this.logger.log('Storage template migration disabled, skipping'); return true; } @@ -145,7 +147,7 @@ export class StorageTemplateService { } return this.databaseRepository.withLock(DatabaseLock.StorageTemplateMigration, async () => { - const { id, sidecarPath, originalPath, exifInfo } = asset; + const { id, sidecarPath, originalPath, exifInfo, checksum } = asset; const oldPath = originalPath; const newPath = await this.getTemplatePath(asset, metadata); @@ -160,7 +162,7 @@ export class StorageTemplateService { pathType: AssetPathType.ORIGINAL, oldPath, newPath, - assetInfo: { sizeInBytes: exifInfo.fileSizeInByte, checksum: asset.checksum }, + assetInfo: { sizeInBytes: exifInfo.fileSizeInByte, checksum }, }); if (sidecarPath) { await this.storageCore.moveFile({ @@ -171,7 +173,7 @@ export class StorageTemplateService { }); } } catch (error: any) { - this.logger.error(`Problem applying storage template`, error?.stack, { id: asset.id, oldPath, newPath }); + this.logger.error(`Problem applying storage template`, error?.stack, { id, oldPath, newPath }); } }); } @@ -181,8 +183,8 @@ export class StorageTemplateService { try { const source = asset.originalPath; - const ext = path.extname(source).split('.').pop() as string; - const sanitized = sanitize(path.basename(filename, `.${ext}`)); + const extension = path.extname(source).split('.').pop() as string; + const sanitized = sanitize(path.basename(filename, `.${extension}`)); const rootPath = StorageCore.getLibraryFolder({ id: asset.ownerId, storageLabel }); let albumName = null; @@ -194,11 +196,11 @@ export class StorageTemplateService { const storagePath = this.render(this.template.compiled, { asset, filename: sanitized, - extension: ext, + extension: extension, albumName, }); const fullPath = path.normalize(path.join(rootPath, storagePath)); - let destination = `${fullPath}.${ext}`; + let destination = `${fullPath}.${extension}`; if (!fullPath.startsWith(rootPath)) { this.logger.warn(`Skipped attempt to access an invalid path: ${fullPath}. Path should start with ${rootPath}`); @@ -223,8 +225,8 @@ export class StorageTemplateService { * The lines below will be used to check if the differences between the source and destination is only the * +7 suffix, and if so, it will be considered as already migrated. */ - if (source.startsWith(fullPath) && source.endsWith(`.${ext}`)) { - const diff = source.replace(fullPath, '').replace(`.${ext}`, ''); + if (source.startsWith(fullPath) && source.endsWith(`.${extension}`)) { + const diff = source.replace(fullPath, '').replace(`.${extension}`, ''); const hasDuplicationAnnotation = /^\+\d+$/.test(diff); if (hasDuplicationAnnotation) { return source; @@ -240,7 +242,7 @@ export class StorageTemplateService { } duplicateCount++; - destination = `${fullPath}+${duplicateCount}.${ext}`; + destination = `${fullPath}+${duplicateCount}.${extension}`; } return destination; @@ -264,9 +266,9 @@ export class StorageTemplateService { extension: 'jpg', albumName: 'album', }); - } catch (e) { - this.logger.warn(`Storage template validation failed: ${JSON.stringify(e)}`); - throw new Error(`Invalid storage template: ${e}`); + } catch (error) { + this.logger.warn(`Storage template validation failed: ${JSON.stringify(error)}`); + throw new Error(`Invalid storage template: ${error}`); } } @@ -282,7 +284,7 @@ export class StorageTemplateService { return { raw: template, compiled: handlebar.compile(template, { knownHelpers: undefined, strict: true }), - needsAlbum: template.indexOf('{{album}}') !== -1, + needsAlbum: template.includes('{{album}}'), }; } @@ -295,7 +297,7 @@ export class StorageTemplateService { filetypefull: asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO', assetId: asset.id, //just throw into the root if it doesn't belong to an album - album: (albumName && sanitize(albumName.replace(/\.+/g, ''))) || '.', + album: (albumName && sanitize(albumName.replaceAll(/\.+/g, ''))) || '.', }; const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; diff --git a/server/src/domain/storage/storage.core.ts b/server/src/domain/storage/storage.core.ts index fef954baa0..9456fd66b1 100644 --- a/server/src/domain/storage/storage.core.ts +++ b/server/src/domain/storage/storage.core.ts @@ -118,40 +118,44 @@ export class StorageCore { async moveAssetFile(asset: AssetEntity, pathType: GeneratedAssetPath) { const { id: entityId, resizePath, webpPath, encodedVideoPath } = asset; switch (pathType) { - case AssetPathType.JPEG_THUMBNAIL: + case AssetPathType.JPEG_THUMBNAIL: { return this.moveFile({ entityId, pathType, oldPath: resizePath, newPath: StorageCore.getLargeThumbnailPath(asset), }); - case AssetPathType.WEBP_THUMBNAIL: + } + case AssetPathType.WEBP_THUMBNAIL: { return this.moveFile({ entityId, pathType, oldPath: webpPath, newPath: StorageCore.getSmallThumbnailPath(asset), }); - case AssetPathType.ENCODED_VIDEO: + } + case AssetPathType.ENCODED_VIDEO: { return this.moveFile({ entityId, pathType, oldPath: encodedVideoPath, newPath: StorageCore.getEncodedVideoPath(asset), }); + } } } async movePersonFile(person: PersonEntity, pathType: PersonPathType) { const { id: entityId, thumbnailPath } = person; switch (pathType) { - case PersonPathType.FACE: + case PersonPathType.FACE: { await this.moveFile({ entityId, pathType, oldPath: thumbnailPath, newPath: StorageCore.getPersonThumbnailPath(person), }); + } } } @@ -168,7 +172,8 @@ export class StorageCore { this.logger.log(`Attempting to finish incomplete move: ${move.oldPath} => ${move.newPath}`); const oldPathExists = await this.repository.checkFileExists(move.oldPath); const newPathExists = await this.repository.checkFileExists(move.newPath); - const actualPath = oldPathExists ? move.oldPath : newPathExists ? move.newPath : null; + const newPathCheck = newPathExists ? move.newPath : null; + const actualPath = oldPathExists ? move.oldPath : newPathCheck; if (!actualPath) { this.logger.warn('Unable to complete move. File does not exist at either location.'); return; @@ -177,13 +182,14 @@ export class StorageCore { const fileAtNewLocation = actualPath === move.newPath; this.logger.log(`Found file at ${fileAtNewLocation ? 'new' : 'old'} location`); - if (fileAtNewLocation) { - if (!(await this.verifyNewPathContentsMatchesExpected(move.oldPath, move.newPath, assetInfo))) { - this.logger.fatal( - `Skipping move as file verification failed, old file is missing and new file is different to what was expected`, - ); - return; - } + if ( + fileAtNewLocation && + !(await this.verifyNewPathContentsMatchesExpected(move.oldPath, move.newPath, assetInfo)) + ) { + this.logger.fatal( + `Skipping move as file verification failed, old file is missing and new file is different to what was expected`, + ); + return; } move = await this.moveRepository.update({ id: move.id, oldPath: actualPath, newPath }); @@ -200,10 +206,10 @@ export class StorageCore { try { this.logger.debug(`Attempting to rename file: ${move.oldPath} => ${newPath}`); await this.repository.rename(move.oldPath, newPath); - } catch (err: any) { - if (err.code !== 'EXDEV') { + } catch (error: any) { + if (error.code !== 'EXDEV') { this.logger.warn( - `Unable to complete move. Error renaming file with code ${err.code} and message: ${err.message}`, + `Unable to complete move. Error renaming file with code ${error.code} and message: ${error.message}`, ); return; } @@ -218,8 +224,8 @@ export class StorageCore { try { await this.repository.unlink(move.oldPath); - } catch (err: any) { - this.logger.warn(`Unable to delete old file, it will now no longer be tracked by Immich: ${err.message}`); + } catch (error: any) { + this.logger.warn(`Unable to delete old file, it will now no longer be tracked by Immich: ${error.message}`); } } } @@ -233,14 +239,17 @@ export class StorageCore { newPath: string, assetInfo?: { sizeInBytes: number; checksum: Buffer }, ) { - const oldPathSize = assetInfo ? assetInfo.sizeInBytes : (await this.repository.stat(oldPath)).size; - const newPathSize = (await this.repository.stat(newPath)).size; + const oldStat = await this.repository.stat(oldPath); + const newStat = await this.repository.stat(newPath); + const oldPathSize = assetInfo ? assetInfo.sizeInBytes : oldStat.size; + const newPathSize = newStat.size; this.logger.debug(`File size check: ${newPathSize} === ${oldPathSize}`); if (newPathSize !== oldPathSize) { this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`); return false; } - if (assetInfo && (await this.configCore.getConfig()).storageTemplate.hashVerificationEnabled) { + const config = await this.configCore.getConfig(); + if (assetInfo && config.storageTemplate.hashVerificationEnabled) { const { checksum } = assetInfo; const newChecksum = await this.cryptoRepository.hashFile(newPath); if (!newChecksum.equals(checksum)) { @@ -266,23 +275,29 @@ export class StorageCore { private savePath(pathType: PathType, id: string, newPath: string) { switch (pathType) { - case AssetPathType.ORIGINAL: + case AssetPathType.ORIGINAL: { return this.assetRepository.save({ id, originalPath: newPath }); - case AssetPathType.JPEG_THUMBNAIL: + } + case AssetPathType.JPEG_THUMBNAIL: { return this.assetRepository.save({ id, resizePath: newPath }); - case AssetPathType.WEBP_THUMBNAIL: + } + case AssetPathType.WEBP_THUMBNAIL: { return this.assetRepository.save({ id, webpPath: newPath }); - case AssetPathType.ENCODED_VIDEO: + } + case AssetPathType.ENCODED_VIDEO: { return this.assetRepository.save({ id, encodedVideoPath: newPath }); - case AssetPathType.SIDECAR: + } + case AssetPathType.SIDECAR: { return this.assetRepository.save({ id, sidecarPath: newPath }); - case PersonPathType.FACE: + } + case PersonPathType.FACE: { return this.personRepository.update({ id, thumbnailPath: newPath }); + } } } static getNestedFolder(folder: StorageFolder, ownerId: string, filename: string): string { - return join(StorageCore.getFolderLocation(folder, ownerId), filename.substring(0, 2), filename.substring(2, 4)); + return join(StorageCore.getFolderLocation(folder, ownerId), filename.slice(0, 2), filename.slice(2, 4)); } static getNestedPath(folder: StorageFolder, ownerId: string, filename: string): string { diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index 8a33c7061a..0a20e5cc2a 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -132,7 +132,7 @@ export const defaults = Object.freeze<SystemConfig>({ watch: { enabled: false, usePolling: false, - interval: 10000, + interval: 10_000, }, }, server: { @@ -184,22 +184,30 @@ export class SystemConfigCore { const hasFeature = await this.hasFeature(feature); if (!hasFeature) { switch (feature) { - case FeatureFlag.SMART_SEARCH: + case FeatureFlag.SMART_SEARCH: { throw new BadRequestException('Smart search is not enabled'); - case FeatureFlag.FACIAL_RECOGNITION: + } + case FeatureFlag.FACIAL_RECOGNITION: { throw new BadRequestException('Facial recognition is not enabled'); - case FeatureFlag.SIDECAR: + } + case FeatureFlag.SIDECAR: { throw new BadRequestException('Sidecar is not enabled'); - case FeatureFlag.SEARCH: + } + case FeatureFlag.SEARCH: { throw new BadRequestException('Search is not enabled'); - case FeatureFlag.OAUTH: + } + case FeatureFlag.OAUTH: { throw new BadRequestException('OAuth is not enabled'); - case FeatureFlag.PASSWORD_LOGIN: + } + case FeatureFlag.PASSWORD_LOGIN: { throw new BadRequestException('Password login is not enabled'); - case FeatureFlag.CONFIG_FILE: + } + case FeatureFlag.CONFIG_FILE: { throw new BadRequestException('Config file is not set'); - default: + } + default: { throw new ForbiddenException(`Missing required feature: ${feature}`); + } } } } @@ -278,9 +286,9 @@ export class SystemConfigCore { for (const validator of this.validators) { await validator(newConfig, oldConfig); } - } catch (e) { - this.logger.warn(`Unable to save system config due to a validation error: ${e}`); - throw new BadRequestException(e instanceof Error ? e.message : e); + } catch (error) { + this.logger.warn(`Unable to save system config due to a validation error: ${error}`); + throw new BadRequestException(error instanceof Error ? error.message : error); } const updates: SystemConfigEntity[] = []; @@ -330,19 +338,20 @@ export class SystemConfigCore { private async loadFromFile(filepath: string, force = false) { if (force || !this.configCache) { try { - const file = JSON.parse((await this.repository.readFile(filepath)).toString()); + const file = await this.repository.readFile(filepath); + const json = JSON.parse(file.toString()); const overrides: SystemConfigEntity<SystemConfigValue>[] = []; for (const key of Object.values(SystemConfigKey)) { - const value = _.get(file, key); - this.unsetDeep(file, key); + const value = _.get(json, key); + this.unsetDeep(json, key); if (value !== undefined) { overrides.push({ key, value }); } } - if (!_.isEmpty(file)) { - this.logger.warn(`Unknown keys found: ${JSON.stringify(file, null, 2)}`); + if (!_.isEmpty(json)) { + this.logger.warn(`Unknown keys found: ${JSON.stringify(json, null, 2)}`); } this.configCache = overrides; diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index 9a5862db05..191480b2b7 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -136,7 +136,7 @@ const updatedConfig = Object.freeze<SystemConfig>({ watch: { enabled: false, usePolling: false, - interval: 10000, + interval: 10_000, }, }, }); diff --git a/server/src/domain/system-config/system-config.service.ts b/server/src/domain/system-config/system-config.service.ts index 261f8f9a16..5bf597e35d 100644 --- a/server/src/domain/system-config/system-config.service.ts +++ b/server/src/domain/system-config/system-config.service.ts @@ -121,7 +121,7 @@ export class SystemConfigService { private async setLogLevel({ logging }: SystemConfig) { const envLevel = this.getEnvLogLevel(); const configLevel = logging.enabled ? logging.level : false; - const level = envLevel ? envLevel : configLevel; + const level = envLevel ?? configLevel; ImmichLogger.setLogLevel(level); this.logger.log(`LogLevel=${level} ${envLevel ? '(set via LOG_LEVEL)' : '(set via system config)'}`); } diff --git a/server/src/domain/tag/tag.service.ts b/server/src/domain/tag/tag.service.ts index f7f06c4177..38f1de1bcb 100644 --- a/server/src/domain/tag/tag.service.ts +++ b/server/src/domain/tag/tag.service.ts @@ -10,7 +10,7 @@ export class TagService { constructor(@Inject(ITagRepository) private repository: ITagRepository) {} getAll(auth: AuthDto) { - return this.repository.getAll(auth.user.id).then((tags) => tags.map(mapTag)); + return this.repository.getAll(auth.user.id).then((tags) => tags.map((tag) => mapTag(tag))); } async getById(auth: AuthDto, id: string): Promise<TagResponseDto> { @@ -78,10 +78,10 @@ export class TagService { const results: AssetIdsResponseDto[] = []; for (const assetId of dto.assetIds) { const hasAsset = await this.repository.hasAsset(auth.user.id, id, assetId); - if (!hasAsset) { - results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND }); - } else { + if (hasAsset) { results.push({ assetId, success: true }); + } else { + results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND }); } } diff --git a/server/src/domain/user/response-dto/user-response.dto.ts b/server/src/domain/user/response-dto/user-response.dto.ts index e6dff1655c..15800b9933 100644 --- a/server/src/domain/user/response-dto/user-response.dto.ts +++ b/server/src/domain/user/response-dto/user-response.dto.ts @@ -5,10 +5,7 @@ import { IsEnum } from 'class-validator'; export const getRandomAvatarColor = (user: UserEntity): UserAvatarColor => { const values = Object.values(UserAvatarColor); const randomIndex = Math.floor( - user.email - .split('') - .map((letter) => letter.charCodeAt(0)) - .reduce((a, b) => a + b, 0) % values.length, + [...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length, ); return values[randomIndex] as UserAvatarColor; }; diff --git a/server/src/domain/user/user.core.ts b/server/src/domain/user/user.core.ts index b19ee5e84f..691bc7de49 100644 --- a/server/src/domain/user/user.core.ts +++ b/server/src/domain/user/user.core.ts @@ -1,6 +1,6 @@ import { LibraryType, UserEntity } from '@app/infra/entities'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; -import path from 'path'; +import path from 'node:path'; import sanitize from 'sanitize-filename'; import { ICryptoRepository, ILibraryRepository, IUserRepository } from '../repositories'; import { UserResponseDto } from './response-dto'; @@ -97,7 +97,7 @@ export class UserCore { payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS); } if (payload.storageLabel) { - payload.storageLabel = sanitize(payload.storageLabel.replace(/\./g, '')); + payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', '')); } const userEntity = await this.userRepository.create(payload); await this.libraryRepository.create({ diff --git a/server/src/domain/user/user.service.spec.ts b/server/src/domain/user/user.service.spec.ts index 78743a0439..13ae149b4e 100644 --- a/server/src/domain/user/user.service.spec.ts +++ b/server/src/domain/user/user.service.spec.ts @@ -418,7 +418,7 @@ describe(UserService.name, () => { it('should default to a random password', async () => { userMock.getAdmin.mockResolvedValue(userStub.admin); - const ask = jest.fn().mockResolvedValue(undefined); + const ask = jest.fn().mockImplementation(() => {}); const response = await sut.resetAdminPassword(ask); diff --git a/server/src/domain/user/user.service.ts b/server/src/domain/user/user.service.ts index bdb8f74ed7..7855b1ed6b 100644 --- a/server/src/domain/user/user.service.ts +++ b/server/src/domain/user/user.service.ts @@ -1,7 +1,7 @@ import { UserEntity } from '@app/infra/entities'; import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common'; -import { randomBytes } from 'crypto'; +import { randomBytes } from 'node:crypto'; import { AuthDto } from '../auth'; import { CacheControl, ImmichFileResponse } from '../domain.util'; import { IEntityJob, JobName } from '../job'; @@ -39,7 +39,7 @@ export class UserService { async getAll(auth: AuthDto, isAll: boolean): Promise<UserResponseDto[]> { const users = await this.userRepository.getList({ withDeleted: !isAll }); - return users.map(mapUser); + return users.map((user) => mapUser(user)); } async get(userId: string): Promise<UserResponseDto> { @@ -125,7 +125,7 @@ export class UserService { } const providedPassword = await ask(mapUser(admin)); - const password = providedPassword || randomBytes(24).toString('base64').replace(/\W/g, ''); + const password = providedPassword || randomBytes(24).toString('base64').replaceAll(/\W/g, ''); await this.userCore.updateUser(admin, admin.id, { password }); @@ -188,9 +188,10 @@ export class UserService { return false; } - const msInDay = 86400000; + // TODO use luxon for date calculation + const msInDay = 86_400_000; const msDeleteWait = msInDay * 7; - const msSinceDelete = new Date().getTime() - (Date.parse(user.deletedAt.toString()) || 0); + const msSinceDelete = Date.now() - (Date.parse(user.deletedAt.toString()) || 0); return msSinceDelete >= msDeleteWait; } diff --git a/server/src/immich-admin/commands/reset-admin-password.command.ts b/server/src/immich-admin/commands/reset-admin-password.command.ts index af36c590c9..d19ddf4338 100644 --- a/server/src/immich-admin/commands/reset-admin-password.command.ts +++ b/server/src/immich-admin/commands/reset-admin-password.command.ts @@ -13,20 +13,20 @@ export class ResetAdminPasswordCommand extends CommandRunner { super(); } - async run(): Promise<void> { - const ask = (admin: UserResponseDto) => { - const { id, oauthId, email, name } = admin; - console.log(`Found Admin: + ask = (admin: UserResponseDto) => { + const { id, oauthId, email, name } = admin; + console.log(`Found Admin: - ID=${id} - OAuth ID=${oauthId} - Email=${email} - Name=${name}`); - return this.inquirer.ask<{ password: string }>('prompt-password', undefined).then(({ password }) => password); - }; + return this.inquirer.ask<{ password: string }>('prompt-password', {}).then(({ password }) => password); + }; + async run(): Promise<void> { try { - const { password, provided } = await this.userService.resetAdminPassword(ask); + const { password, provided } = await this.userService.resetAdminPassword(this.ask); if (provided) { console.log(`The admin password has been updated.`); @@ -46,7 +46,7 @@ export class PromptPasswordQuestions { message: 'Please choose a new password (optional)', name: 'password', }) - parsePassword(val: string) { - return val; + parsePassword(value: string) { + return value; } } diff --git a/server/src/immich/api-v1/asset/asset.core.ts b/server/src/immich/api-v1/asset/asset.core.ts index ec68c98a1e..0688a65dd6 100644 --- a/server/src/immich/api-v1/asset/asset.core.ts +++ b/server/src/immich/api-v1/asset/asset.core.ts @@ -38,7 +38,7 @@ export class AssetCore { isArchived: dto.isArchived ?? false, duration: dto.duration || null, isVisible: dto.isVisible ?? true, - livePhotoVideo: livePhotoAssetId != null ? ({ id: livePhotoAssetId } as AssetEntity) : null, + livePhotoVideo: livePhotoAssetId === null ? null : ({ id: livePhotoAssetId } as AssetEntity), resizePath: null, webpPath: null, thumbhash: null, diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/immich/api-v1/asset/asset.service.spec.ts index 8d3046c3a3..d5fde4a625 100644 --- a/server/src/immich/api-v1/asset/asset.service.spec.ts +++ b/server/src/immich/api-v1/asset/asset.service.spec.ts @@ -51,8 +51,8 @@ const _getAsset_1 = () => { asset_1.encodedVideoPath = ''; asset_1.duration = '0:00:00.000000'; asset_1.exifInfo = new ExifEntity(); - asset_1.exifInfo.latitude = 49.533547; - asset_1.exifInfo.longitude = 10.703075; + asset_1.exifInfo.latitude = 49.533_547; + asset_1.exifInfo.longitude = 10.703_075; return asset_1; }; diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index efad71dc89..6d59647cbf 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/immich/api-v1/asset/asset.service.ts @@ -27,7 +27,6 @@ import { AssetSearchDto } from './dto/asset-search.dto'; import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; import { CreateAssetDto } from './dto/create-asset.dto'; import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto'; -import { SearchPropertiesDto } from './dto/search-properties.dto'; import { ServeFileDto } from './dto/serve-file.dto'; import { AssetBulkUploadCheckResponseDto, @@ -163,7 +162,8 @@ export class AssetService { const possibleSearchTerm = new Set<string>(); const rows = await this.assetRepositoryV1.getSearchPropertiesByUserId(auth.user.id); - rows.forEach((row: SearchPropertiesDto) => { + + for (const row of rows) { // tags row.tags?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase())); @@ -187,9 +187,9 @@ export class AssetService { possibleSearchTerm.add(row.city?.toLowerCase() || ''); possibleSearchTerm.add(row.state?.toLowerCase() || ''); possibleSearchTerm.add(row.country?.toLowerCase() || ''); - }); + } - return Array.from(possibleSearchTerm).filter((x) => x != null && x != ''); + return [...possibleSearchTerm].filter((x) => x != null && x != ''); } async getCuratedLocation(auth: AuthDto): Promise<CuratedLocationsResponseDto[]> { @@ -249,18 +249,18 @@ export class AssetService { private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) { switch (format) { - case GetAssetThumbnailFormatEnum.WEBP: + case GetAssetThumbnailFormatEnum.WEBP: { if (asset.webpPath) { return asset.webpPath; } this.logger.warn(`WebP thumbnail requested but not found for asset ${asset.id}, falling back to JPEG`); - - case GetAssetThumbnailFormatEnum.JPEG: - default: + } + case GetAssetThumbnailFormatEnum.JPEG: { if (!asset.resizePath) { throw new NotFoundException(`No thumbnail found for asset ${asset.id}`); } return asset.resizePath; + } } } diff --git a/server/src/immich/app.guard.ts b/server/src/immich/app.guard.ts index 85f0689a8c..bd07d107b1 100644 --- a/server/src/immich/app.guard.ts +++ b/server/src/immich/app.guard.ts @@ -50,8 +50,8 @@ export const SharedLinkRoute = () => applyDecorators(SetMetadata(Metadata.SHARED_ROUTE, true), ApiQuery({ name: 'key', type: String, required: false })); export const AdminRoute = (value = true) => SetMetadata(Metadata.ADMIN_ROUTE, value); -export const Auth = createParamDecorator((data, ctx: ExecutionContext): AuthDto => { - return ctx.switchToHttp().getRequest<{ user: AuthDto }>().user; +export const Auth = createParamDecorator((data, context: ExecutionContext): AuthDto => { + return context.switchToHttp().getRequest<{ user: AuthDto }>().user; }); export const FileResponse = () => @@ -59,15 +59,15 @@ export const FileResponse = () => content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } }, }); -export const GetLoginDetails = createParamDecorator((data, ctx: ExecutionContext): LoginDetails => { - const req = ctx.switchToHttp().getRequest<Request>(); - const userAgent = UAParser(req.headers['user-agent']); +export const GetLoginDetails = createParamDecorator((data, context: ExecutionContext): LoginDetails => { + const request = context.switchToHttp().getRequest<Request>(); + const userAgent = UAParser(request.headers['user-agent']); return { - clientIp: req.ip, - isSecure: req.secure, - deviceType: userAgent.browser.name || userAgent.device.type || (req.headers.devicemodel as string) || '', - deviceOS: userAgent.os.name || (req.headers.devicetype as string) || '', + clientIp: request.ip, + isSecure: request.secure, + deviceType: userAgent.browser.name || userAgent.device.type || (request.headers.devicemodel as string) || '', + deviceOS: userAgent.os.name || (request.headers.devicetype as string) || '', }; }); @@ -95,20 +95,20 @@ export class AppGuard implements CanActivate { return true; } - const req = context.switchToHttp().getRequest<AuthRequest>(); + const request = context.switchToHttp().getRequest<AuthRequest>(); - const authDto = await this.authService.validate(req.headers, req.query as Record<string, string>); + const authDto = await this.authService.validate(request.headers, request.query as Record<string, string>); if (authDto.sharedLink && !isSharedRoute) { - this.logger.warn(`Denied access to non-shared route: ${req.path}`); + this.logger.warn(`Denied access to non-shared route: ${request.path}`); return false; } if (isAdminRoute && !authDto.user.isAdmin) { - this.logger.warn(`Denied access to admin only route: ${req.path}`); + this.logger.warn(`Denied access to admin only route: ${request.path}`); return false; } - req.user = authDto; + request.user = authDto; return true; } diff --git a/server/src/immich/app.service.ts b/server/src/immich/app.service.ts index 0b3a18577c..be82ae4dc8 100644 --- a/server/src/immich/app.service.ts +++ b/server/src/immich/app.service.ts @@ -15,7 +15,7 @@ import { ImmichLogger } from '@app/infra/logger'; import { Injectable } from '@nestjs/common'; import { Cron, CronExpression, Interval } from '@nestjs/schedule'; import { NextFunction, Request, Response } from 'express'; -import { readFileSync } from 'fs'; +import { readFileSync } from 'node:fs'; const render = (index: string, meta: OpenGraphTags) => { const tags = ` @@ -79,15 +79,15 @@ export class AppService { let index = ''; try { index = readFileSync(WEB_ROOT_PATH).toString(); - } catch (error: Error | any) { + } catch { this.logger.warn('Unable to open `www/index.html, skipping SSR.'); } - return async (req: Request, res: Response, next: NextFunction) => { + return async (request: Request, res: Response, next: NextFunction) => { if ( - req.url.startsWith('/api') || - req.method.toLowerCase() !== 'get' || - excludePaths.find((item) => req.url.startsWith(item)) + request.url.startsWith('/api') || + request.method.toLowerCase() !== 'get' || + excludePaths.some((item) => request.url.startsWith(item)) ) { return next(); } @@ -107,7 +107,7 @@ export class AppService { try { for (const { regex, onMatch } of targets) { - const matches = req.url.match(regex); + const matches = request.url.match(regex); if (matches) { const meta = await onMatch(matches); if (meta) { diff --git a/server/src/immich/app.utils.ts b/server/src/immich/app.utils.ts index 0dd984a02d..938f6f4708 100644 --- a/server/src/immich/app.utils.ts +++ b/server/src/immich/app.utils.ts @@ -18,11 +18,11 @@ import { SwaggerModule, } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; -import { writeFileSync } from 'fs'; -import { access, constants } from 'fs/promises'; import _ from 'lodash'; -import path, { isAbsolute } from 'path'; -import { promisify } from 'util'; +import { writeFileSync } from 'node:fs'; +import { access, constants } from 'node:fs/promises'; +import path, { isAbsolute } from 'node:path'; +import { promisify } from 'node:util'; import { applyDecorators, UsePipes, ValidationPipe } from '@nestjs/common'; import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; @@ -55,13 +55,15 @@ export const sendFile = async ( try { const file = await handler(); switch (file.cacheControl) { - case CacheControl.PRIVATE_WITH_CACHE: + case CacheControl.PRIVATE_WITH_CACHE: { res.set('Cache-Control', 'private, max-age=86400, no-transform'); break; + } - case CacheControl.PRIVATE_WITHOUT_CACHE: + case CacheControl.PRIVATE_WITHOUT_CACHE: { res.set('Cache-Control', 'private, no-cache, no-transform'); break; + } } res.header('Content-Type', file.contentType); @@ -94,21 +96,21 @@ export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => return new StreamableFile(stream, { type, length }); }; -function sortKeys<T>(obj: T): T { - if (!obj || typeof obj !== 'object' || Array.isArray(obj)) { - return obj; +function sortKeys<T>(target: T): T { + if (!target || typeof target !== 'object' || Array.isArray(target)) { + return target; } const result: Partial<T> = {}; - const keys = Object.keys(obj).sort() as Array<keyof T>; + const keys = Object.keys(target).sort() as Array<keyof T>; for (const key of keys) { - result[key] = sortKeys(obj[key]); + result[key] = sortKeys(target[key]); } return result as T; } export const routeToErrorMessage = (methodName: string) => - 'Failed to ' + methodName.replace(/[A-Z]+/g, (letter) => ` ${letter.toLowerCase()}`); + 'Failed to ' + methodName.replaceAll(/[A-Z]+/g, (letter) => ` ${letter.toLowerCase()}`); const patchOpenAPI = (document: OpenAPIObject) => { document.paths = sortKeys(document.paths); @@ -152,7 +154,7 @@ const patchOpenAPI = (document: OpenAPIObject) => { continue; } - if ((operation.security || []).find((item) => !!item[Metadata.PUBLIC_SECURITY])) { + if ((operation.security || []).some((item) => !!item[Metadata.PUBLIC_SECURITY])) { delete operation.security; } @@ -177,7 +179,7 @@ const patchOpenAPI = (document: OpenAPIObject) => { return document; }; -export const useSwagger = (app: INestApplication, isDev: boolean) => { +export const useSwagger = (app: INestApplication, isDevelopment: boolean) => { const config = new DocumentBuilder() .setTitle('Immich') .setDescription('Immich API') @@ -203,7 +205,7 @@ export const useSwagger = (app: INestApplication, isDev: boolean) => { operationIdFactory: (controllerKey: string, methodKey: string) => methodKey, }; - const doc = SwaggerModule.createDocument(app, config, options); + const specification = SwaggerModule.createDocument(app, config, options); const customOptions: SwaggerCustomOptions = { swaggerOptions: { @@ -212,11 +214,11 @@ export const useSwagger = (app: INestApplication, isDev: boolean) => { customSiteTitle: 'Immich API Documentation', }; - SwaggerModule.setup('doc', app, doc, customOptions); + SwaggerModule.setup('doc', app, specification, customOptions); - if (isDev) { + if (isDevelopment) { // Generate API Documentation only in development mode const outputPath = path.resolve(process.cwd(), '../open-api/immich-openapi-specs.json'); - writeFileSync(outputPath, JSON.stringify(patchOpenAPI(doc), null, 2), { encoding: 'utf8' }); + writeFileSync(outputPath, JSON.stringify(patchOpenAPI(specification), null, 2), { encoding: 'utf8' }); } }; diff --git a/server/src/immich/controllers/auth.controller.ts b/server/src/immich/controllers/auth.controller.ts index 38cf8f23dc..15018c10de 100644 --- a/server/src/immich/controllers/auth.controller.ts +++ b/server/src/immich/controllers/auth.controller.ts @@ -78,13 +78,13 @@ export class AuthController { @Post('logout') @HttpCode(HttpStatus.OK) logout( - @Req() req: Request, + @Req() request: Request, @Res({ passthrough: true }) res: Response, @Auth() auth: AuthDto, ): Promise<LogoutResponseDto> { res.clearCookie(IMMICH_ACCESS_COOKIE); res.clearCookie(IMMICH_AUTH_TYPE_COOKIE); - return this.service.logout(auth, (req.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]); + return this.service.logout(auth, (request.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]); } } diff --git a/server/src/immich/controllers/oauth.controller.ts b/server/src/immich/controllers/oauth.controller.ts index b7fd0fe021..678e4a4f3c 100644 --- a/server/src/immich/controllers/oauth.controller.ts +++ b/server/src/immich/controllers/oauth.controller.ts @@ -25,9 +25,9 @@ export class OAuthController { @PublicRoute() @Get('mobile-redirect') @Redirect() - redirectOAuthToMobile(@Req() req: Request) { + redirectOAuthToMobile(@Req() request: Request) { return { - url: this.service.getMobileRedirect(req.url), + url: this.service.getMobileRedirect(request.url), statusCode: HttpStatus.TEMPORARY_REDIRECT, }; } diff --git a/server/src/immich/controllers/shared-link.controller.ts b/server/src/immich/controllers/shared-link.controller.ts index 25d4bdca46..86045433d5 100644 --- a/server/src/immich/controllers/shared-link.controller.ts +++ b/server/src/immich/controllers/shared-link.controller.ts @@ -33,10 +33,10 @@ export class SharedLinkController { async getMySharedLink( @Auth() auth: AuthDto, @Query() dto: SharedLinkPasswordDto, - @Req() req: Request, + @Req() request: Request, @Res({ passthrough: true }) res: Response, ): Promise<SharedLinkResponseDto> { - const sharedLinkToken = req.cookies?.[IMMICH_SHARED_LINK_ACCESS_COOKIE]; + const sharedLinkToken = request.cookies?.[IMMICH_SHARED_LINK_ACCESS_COOKIE]; if (sharedLinkToken) { dto.token = sharedLinkToken; } diff --git a/server/src/immich/interceptors/file-upload.interceptor.ts b/server/src/immich/interceptors/file-upload.interceptor.ts index d94761d44a..52cc447e8e 100644 --- a/server/src/immich/interceptors/file-upload.interceptor.ts +++ b/server/src/immich/interceptors/file-upload.interceptor.ts @@ -4,9 +4,9 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nes import { PATH_METADATA } from '@nestjs/common/constants'; import { Reflector } from '@nestjs/core'; import { transformException } from '@nestjs/platform-express/multer/multer/multer.utils'; -import { createHash, randomUUID } from 'crypto'; import { NextFunction, RequestHandler } from 'express'; import multer, { StorageEngine, diskStorage } from 'multer'; +import { createHash, randomUUID } from 'node:crypto'; import { Observable } from 'rxjs'; import { AuthRequest } from '../app.guard'; @@ -40,17 +40,17 @@ interface Callback<T> { (error: null, result: T): void; } -const callbackify = async <T>(fn: (...args: any[]) => T, callback: Callback<T>) => { +const callbackify = async <T>(target: (...arguments_: any[]) => T, callback: Callback<T>) => { try { - return callback(null, await fn()); + return callback(null, await target()); } catch (error: Error | any) { return callback(error); } }; -const asRequest = (req: AuthRequest, file: Express.Multer.File) => { +const asRequest = (request: AuthRequest, file: Express.Multer.File) => { return { - auth: req.user || null, + auth: request.user || null, fieldName: file.fieldname as UploadFieldName, file: mapToUploadFile(file as ImmichFile), }; @@ -94,14 +94,14 @@ export class FileUploadInterceptor implements NestInterceptor { } async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<any>> { - const ctx = context.switchToHttp(); + const context_ = context.switchToHttp(); const route = this.reflect.get<string>(PATH_METADATA, context.getClass()); const handler: RequestHandler | null = this.getHandler(route as Route); if (handler) { await new Promise<void>((resolve, reject) => { const next: NextFunction = (error) => (error ? reject(transformException(error)) : resolve()); - handler(ctx.getRequest(), ctx.getResponse(), next); + handler(context_.getRequest(), context_.getResponse(), next); }); } else { this.logger.warn(`Skipping invalid file upload route: ${route}`); @@ -110,28 +110,31 @@ export class FileUploadInterceptor implements NestInterceptor { return next.handle(); } - private fileFilter(req: AuthRequest, file: Express.Multer.File, callback: multer.FileFilterCallback) { - return callbackify(() => this.assetService.canUploadFile(asRequest(req, file)), callback); + private fileFilter(request: AuthRequest, file: Express.Multer.File, callback: multer.FileFilterCallback) { + return callbackify(() => this.assetService.canUploadFile(asRequest(request, file)), callback); } - private filename(req: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) { - return callbackify(() => this.assetService.getUploadFilename(asRequest(req, file)), callback as Callback<string>); + private filename(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) { + return callbackify( + () => this.assetService.getUploadFilename(asRequest(request, file)), + callback as Callback<string>, + ); } - private destination(req: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) { - return callbackify(() => this.assetService.getUploadFolder(asRequest(req, file)), callback as Callback<string>); + private destination(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) { + return callbackify(() => this.assetService.getUploadFolder(asRequest(request, file)), callback as Callback<string>); } - private handleFile(req: AuthRequest, file: Express.Multer.File, callback: Callback<Partial<ImmichFile>>) { + private handleFile(request: AuthRequest, file: Express.Multer.File, callback: Callback<Partial<ImmichFile>>) { (file as ImmichMulterFile).uuid = randomUUID(); if (!this.isAssetUploadFile(file)) { - this.defaultStorage._handleFile(req, file, callback); + this.defaultStorage._handleFile(request, file, callback); return; } const hash = createHash('sha1'); file.stream.on('data', (chunk) => hash.update(chunk)); - this.defaultStorage._handleFile(req, file, (error, info) => { + this.defaultStorage._handleFile(request, file, (error, info) => { if (error) { hash.destroy(); callback(error); @@ -141,15 +144,16 @@ export class FileUploadInterceptor implements NestInterceptor { }); } - private removeFile(req: AuthRequest, file: Express.Multer.File, callback: (error: Error | null) => void) { - this.defaultStorage._removeFile(req, file, callback); + private removeFile(request: AuthRequest, file: Express.Multer.File, callback: (error: Error | null) => void) { + this.defaultStorage._removeFile(request, file, callback); } private isAssetUploadFile(file: Express.Multer.File) { switch (file.fieldname as UploadFieldName) { case UploadFieldName.ASSET_DATA: - case UploadFieldName.LIVE_PHOTO_DATA: + case UploadFieldName.LIVE_PHOTO_DATA: { return true; + } } return false; @@ -157,14 +161,17 @@ export class FileUploadInterceptor implements NestInterceptor { private getHandler(route: Route) { switch (route) { - case Route.ASSET: + case Route.ASSET: { return this.handlers.assetUpload; + } - case Route.USER: + case Route.USER: { return this.handlers.userProfile; + } - default: + default: { return null; + } } } } diff --git a/server/src/infra/database.config.ts b/server/src/infra/database.config.ts index 5cad312570..9e6cccd198 100644 --- a/server/src/infra/database.config.ts +++ b/server/src/infra/database.config.ts @@ -6,12 +6,13 @@ const urlOrParts = url ? { url } : { host: process.env.DB_HOSTNAME || 'localhost', - port: parseInt(process.env.DB_PORT || '5432'), + port: Number.parseInt(process.env.DB_PORT || '5432'), username: process.env.DB_USERNAME || 'postgres', password: process.env.DB_PASSWORD || 'postgres', database: process.env.DB_DATABASE_NAME || 'immich', }; +/* eslint unicorn/prefer-module: "off" -- We can fix this when migrating to ESM*/ export const databaseConfig: PostgresConnectionOptions = { type: 'postgres', entities: [__dirname + '/entities/*.entity.{js,ts}'], @@ -19,7 +20,7 @@ export const databaseConfig: PostgresConnectionOptions = { migrations: [__dirname + '/migrations/*.{js,ts}'], subscribers: [__dirname + '/subscribers/*.{js,ts}'], migrationsRun: false, - connectTimeoutMS: 10000, // 10 seconds + connectTimeoutMS: 10_000, // 10 seconds parseInt8: true, ...urlOrParts, }; diff --git a/server/src/infra/infra.config.ts b/server/src/infra/infra.config.ts index 90ca9fc818..f72f333344 100644 --- a/server/src/infra/infra.config.ts +++ b/server/src/infra/infra.config.ts @@ -15,8 +15,8 @@ function parseRedisConfig(): RedisOptions { } return { host: process.env.REDIS_HOSTNAME || 'immich_redis', - port: parseInt(process.env.REDIS_PORT || '6379'), - db: parseInt(process.env.REDIS_DBINDEX || '0'), + port: Number.parseInt(process.env.REDIS_PORT || '6379'), + db: Number.parseInt(process.env.REDIS_DBINDEX || '0'), username: process.env.REDIS_USERNAME || undefined, password: process.env.REDIS_PASSWORD || undefined, path: process.env.REDIS_SOCKET || undefined, diff --git a/server/src/infra/infra.util.ts b/server/src/infra/infra.util.ts index 4dc821cd57..585d058e03 100644 --- a/server/src/infra/infra.util.ts +++ b/server/src/infra/infra.util.ts @@ -27,4 +27,4 @@ export const DummyValue = { // maximum number of parameters is 65535. Any query that tries to bind more than that (e.g. searching // by a list of IDs) requires splitting the query into multiple chunks. // We are rounding down this limit, as queries commonly include other filters and parameters. -export const DATABASE_PARAMETER_CHUNK_SIZE = 65500; +export const DATABASE_PARAMETER_CHUNK_SIZE = 65_500; diff --git a/server/src/infra/infra.utils.ts b/server/src/infra/infra.utils.ts index 91608472f7..1036df2afa 100644 --- a/server/src/infra/infra.utils.ts +++ b/server/src/infra/infra.utils.ts @@ -59,21 +59,25 @@ export const isValidInteger = (value: number, options: { min?: number; max?: num export function Chunked(options: { paramIndex?: number; mergeFn?: (results: any) => any } = {}): MethodDecorator { return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { const originalMethod = descriptor.value; - const paramIndex = options.paramIndex ?? 0; - descriptor.value = async function (...args: any[]) { - const arg = args[paramIndex]; + const parameterIndex = options.paramIndex ?? 0; + descriptor.value = async function (...arguments_: any[]) { + const argument = arguments_[parameterIndex]; // Early return if argument length is less than or equal to the chunk size. if ( - (arg instanceof Array && arg.length <= DATABASE_PARAMETER_CHUNK_SIZE) || - (arg instanceof Set && arg.size <= DATABASE_PARAMETER_CHUNK_SIZE) + (Array.isArray(argument) && argument.length <= DATABASE_PARAMETER_CHUNK_SIZE) || + (argument instanceof Set && argument.size <= DATABASE_PARAMETER_CHUNK_SIZE) ) { - return await originalMethod.apply(this, args); + return await originalMethod.apply(this, arguments_); } return Promise.all( - chunks(arg, DATABASE_PARAMETER_CHUNK_SIZE).map(async (chunk) => { - await originalMethod.apply(this, [...args.slice(0, paramIndex), chunk, ...args.slice(paramIndex + 1)]); + chunks(argument, DATABASE_PARAMETER_CHUNK_SIZE).map(async (chunk) => { + await Reflect.apply(originalMethod, this, [ + ...arguments_.slice(0, parameterIndex), + chunk, + ...arguments_.slice(parameterIndex + 1), + ]); }), ).then((results) => (options.mergeFn ? options.mergeFn(results) : results)); }; diff --git a/server/src/infra/migrations/1688392120838-AddLibraryTable.ts b/server/src/infra/migrations/1688392120838-AddLibraryTable.ts index 53a6f780bf..4d394adaf1 100644 --- a/server/src/infra/migrations/1688392120838-AddLibraryTable.ts +++ b/server/src/infra/migrations/1688392120838-AddLibraryTable.ts @@ -24,7 +24,8 @@ export class AddLibraries1688392120838 implements MigrationInterface { ); // Create default library for each user and assign all assets to it - const userIds: string[] = (await queryRunner.query(`SELECT id FROM "users"`)).map((user: any) => user.id); + const users = await queryRunner.query(`SELECT id FROM "users"`); + const userIds: string[] = users.map((user: any) => user.id); for (const userId of userIds) { await queryRunner.query( diff --git a/server/src/infra/migrations/1700713871511-UsePgVectors.ts b/server/src/infra/migrations/1700713871511-UsePgVectors.ts index 9f8a72cff3..a952f1646d 100644 --- a/server/src/infra/migrations/1700713871511-UsePgVectors.ts +++ b/server/src/infra/migrations/1700713871511-UsePgVectors.ts @@ -14,7 +14,7 @@ export class UsePgVectors1700713871511 implements MigrationInterface { const clipModelNameQuery = await queryRunner.query(`SELECT value FROM system_config WHERE key = 'machineLearning.clip.modelName'`); const clipModelName: string = clipModelNameQuery?.[0]?.['value'] ?? 'ViT-B-32__openai'; - const clipDimSize = getCLIPModelInfo(clipModelName.replace(/"/g, '')).dimSize; + const clipDimSize = getCLIPModelInfo(clipModelName.replaceAll('"', '')).dimSize; await queryRunner.query(` ALTER TABLE asset_faces diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts index f275b51713..cb6469195e 100644 --- a/server/src/infra/repositories/access.repository.ts +++ b/server/src/infra/repositories/access.repository.ts @@ -167,7 +167,7 @@ class AlbumAccess implements IAlbumAccess { }) .then( (sharedLinks) => - new Set(sharedLinks.flatMap((sharedLink) => (!!sharedLink.albumId ? [sharedLink.albumId] : []))), + new Set(sharedLinks.flatMap((sharedLink) => (sharedLink.albumId ? [sharedLink.albumId] : []))), ), ), ).then((results) => setUnion(...results)); diff --git a/server/src/infra/repositories/album.repository.ts b/server/src/infra/repositories/album.repository.ts index aa66ba2dc8..2d3fd795db 100644 --- a/server/src/infra/repositories/album.repository.ts +++ b/server/src/infra/repositories/album.repository.ts @@ -71,7 +71,7 @@ export class AlbumRepository implements IAlbumRepository { @ChunkedArray() async getMetadataForIds(ids: string[]): Promise<AlbumAssetCount[]> { // Guard against running invalid query when ids list is empty. - if (!ids.length) { + if (ids.length === 0) { return []; } diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index 226803cfb9..95a227b693 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -24,7 +24,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import _ from 'lodash'; import { DateTime } from 'luxon'; -import path from 'path'; +import path from 'node:path'; import { And, Brackets, @@ -471,7 +471,7 @@ export class AssetRepository implements IAssetRepository { let where: FindOptionsWhere<AssetEntity> | FindOptionsWhere<AssetEntity>[] = {}; switch (property) { - case WithoutProperty.THUMBNAIL: + case WithoutProperty.THUMBNAIL: { where = [ { resizePath: IsNull(), isVisible: true }, { resizePath: '', isVisible: true }, @@ -480,15 +480,17 @@ export class AssetRepository implements IAssetRepository { { thumbhash: IsNull(), isVisible: true }, ]; break; + } - case WithoutProperty.ENCODED_VIDEO: + case WithoutProperty.ENCODED_VIDEO: { where = [ { type: AssetType.VIDEO, encodedVideoPath: IsNull() }, { type: AssetType.VIDEO, encodedVideoPath: '' }, ]; break; + } - case WithoutProperty.EXIF: + case WithoutProperty.EXIF: { relations = { exifInfo: true, jobStatus: true, @@ -500,8 +502,9 @@ export class AssetRepository implements IAssetRepository { }, }; break; + } - case WithoutProperty.SMART_SEARCH: + case WithoutProperty.SMART_SEARCH: { relations = { smartSearch: true, }; @@ -513,8 +516,9 @@ export class AssetRepository implements IAssetRepository { }, }; break; + } - case WithoutProperty.OBJECT_TAGS: + case WithoutProperty.OBJECT_TAGS: { relations = { smartInfo: true, }; @@ -526,8 +530,9 @@ export class AssetRepository implements IAssetRepository { }, }; break; + } - case WithoutProperty.FACES: + case WithoutProperty.FACES: { relations = { faces: true, jobStatus: true, @@ -544,8 +549,9 @@ export class AssetRepository implements IAssetRepository { }, }; break; + } - case WithoutProperty.PERSON: + case WithoutProperty.PERSON: { relations = { faces: true, }; @@ -558,16 +564,19 @@ export class AssetRepository implements IAssetRepository { }, }; break; + } - case WithoutProperty.SIDECAR: + case WithoutProperty.SIDECAR: { where = [ { sidecarPath: IsNull(), isVisible: true }, { sidecarPath: '', isVisible: true }, ]; break; + } - default: + default: { throw new Error(`Invalid getWithout property: ${property}`); + } } return paginate(this.repository, pagination, { @@ -584,18 +593,21 @@ export class AssetRepository implements IAssetRepository { let where: FindOptionsWhere<AssetEntity> | FindOptionsWhere<AssetEntity>[] = {}; switch (property) { - case WithProperty.SIDECAR: + case WithProperty.SIDECAR: { where = [{ sidecarPath: Not(IsNull()), isVisible: true }]; break; - case WithProperty.IS_OFFLINE: + } + case WithProperty.IS_OFFLINE: { if (!libraryId) { throw new Error('Library id is required when finding offline assets'); } where = [{ isOffline: true, libraryId: libraryId }]; break; + } - default: + default: { throw new Error(`Invalid getWith property: ${property}`); + } } return paginate(this.repository, pagination, { diff --git a/server/src/infra/repositories/communication.repository.ts b/server/src/infra/repositories/communication.repository.ts index 23edf85411..ec9eb005bf 100644 --- a/server/src/infra/repositories/communication.repository.ts +++ b/server/src/infra/repositories/communication.repository.ts @@ -51,13 +51,15 @@ export class CommunicationRepository on(event: 'connect' | ServerEvent, callback: OnConnectCallback | OnServerEventCallback) { switch (event) { - case 'connect': + case 'connect': { this.onConnectCallbacks.push(callback); break; + } - default: + default: { this.onServerEventCallbacks[event].push(callback as OnServerEventCallback); break; + } } } diff --git a/server/src/infra/repositories/crypto.repository.ts b/server/src/infra/repositories/crypto.repository.ts index a21bf6253e..f445ed850b 100644 --- a/server/src/infra/repositories/crypto.repository.ts +++ b/server/src/infra/repositories/crypto.repository.ts @@ -1,8 +1,8 @@ import { ICryptoRepository } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { compareSync, hash } from 'bcrypt'; -import { createHash, randomBytes, randomUUID } from 'crypto'; -import { createReadStream } from 'fs'; +import { createHash, randomBytes, randomUUID } from 'node:crypto'; +import { createReadStream } from 'node:fs'; @Injectable() export class CryptoRepository implements ICryptoRepository { @@ -24,7 +24,7 @@ export class CryptoRepository implements ICryptoRepository { return new Promise<Buffer>((resolve, reject) => { const hash = createHash('sha1'); const stream = createReadStream(filepath); - stream.on('error', (err) => reject(err)); + stream.on('error', (error) => reject(error)); stream.on('data', (chunk) => hash.update(chunk)); stream.on('end', () => resolve(hash.digest())); }); diff --git a/server/src/infra/repositories/filesystem.provider.ts b/server/src/infra/repositories/filesystem.provider.ts index c9b44845d4..2ae18432b2 100644 --- a/server/src/infra/repositories/filesystem.provider.ts +++ b/server/src/infra/repositories/filesystem.provider.ts @@ -10,10 +10,10 @@ import { import { ImmichLogger } from '@app/infra/logger'; import archiver from 'archiver'; import chokidar, { WatchOptions } from 'chokidar'; -import { constants, createReadStream, existsSync, mkdirSync } from 'fs'; -import fs, { copyFile, readdir, rename, writeFile } from 'fs/promises'; import { glob } from 'glob'; -import path from 'path'; +import { constants, createReadStream, existsSync, mkdirSync } from 'node:fs'; +import fs, { copyFile, readdir, rename, writeFile } from 'node:fs/promises'; +import path from 'node:path'; export class FilesystemProvider implements IStorageRepository { private logger = new ImmichLogger(FilesystemProvider.name); @@ -60,7 +60,7 @@ export class FilesystemProvider implements IStorageRepository { try { await fs.access(filepath, mode); return true; - } catch (_) { + } catch { return false; } } @@ -68,11 +68,11 @@ export class FilesystemProvider implements IStorageRepository { async unlink(file: string) { try { await fs.unlink(file); - } catch (err) { - if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') { + } catch (error) { + if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') { this.logger.warn(`File ${file} does not exist.`); } else { - throw err; + throw error; } } } diff --git a/server/src/infra/repositories/job.repository.ts b/server/src/infra/repositories/job.repository.ts index 88f3a316ef..8160ff8440 100644 --- a/server/src/infra/repositories/job.repository.ts +++ b/server/src/infra/repositories/job.repository.ts @@ -15,7 +15,7 @@ import { ModuleRef } from '@nestjs/core'; import { SchedulerRegistry } from '@nestjs/schedule'; import { Job, JobsOptions, Processor, Queue, Worker, WorkerOptions } from 'bullmq'; import { CronJob, CronTime } from 'cron'; -import { setTimeout } from 'timers/promises'; +import { setTimeout } from 'node:timers/promises'; import { bullConfig } from '../infra.config'; @Injectable() @@ -24,7 +24,7 @@ export class JobRepository implements IJobRepository { private logger = new ImmichLogger(JobRepository.name); constructor( - private moduleRef: ModuleRef, + private moduleReference: ModuleRef, private schedulerReqistry: SchedulerRegistry, ) {} @@ -118,7 +118,7 @@ export class JobRepository implements IJobRepository { } async queueAll(items: JobItem[]): Promise<void> { - if (!items.length) { + if (items.length === 0) { return; } @@ -167,19 +167,23 @@ export class JobRepository implements IJobRepository { private getJobOptions(item: JobItem): JobsOptions | null { switch (item.name) { - case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: + case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: { return { jobId: item.data.id }; - case JobName.GENERATE_PERSON_THUMBNAIL: + } + case JobName.GENERATE_PERSON_THUMBNAIL: { return { priority: 1 }; - case JobName.QUEUE_FACIAL_RECOGNITION: + } + case JobName.QUEUE_FACIAL_RECOGNITION: { return { jobId: JobName.QUEUE_FACIAL_RECOGNITION }; + } - default: + default: { return null; + } } } private getQueue(queue: QueueName): Queue { - return this.moduleRef.get<Queue>(getQueueToken(queue), { strict: false }); + return this.moduleReference.get<Queue>(getQueueToken(queue), { strict: false }); } } diff --git a/server/src/infra/repositories/machine-learning.repository.ts b/server/src/infra/repositories/machine-learning.repository.ts index 71a6995188..4542c65779 100644 --- a/server/src/infra/repositories/machine-learning.repository.ts +++ b/server/src/infra/repositories/machine-learning.repository.ts @@ -10,7 +10,7 @@ import { VisionModelInput, } from '@app/domain'; import { Injectable } from '@nestjs/common'; -import { readFile } from 'fs/promises'; +import { readFile } from 'node:fs/promises'; const errorPrefix = 'Machine learning request'; diff --git a/server/src/infra/repositories/media.repository.ts b/server/src/infra/repositories/media.repository.ts index 884c24bf9b..bb65dd25c8 100644 --- a/server/src/infra/repositories/media.repository.ts +++ b/server/src/infra/repositories/media.repository.ts @@ -2,10 +2,10 @@ import { CropOptions, IMediaRepository, ResizeOptions, TranscodeOptions, VideoIn import { Colorspace } from '@app/infra/entities'; import { ImmichLogger } from '@app/infra/logger'; import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'; -import fs from 'fs/promises'; +import fs from 'node:fs/promises'; +import { Writable } from 'node:stream'; +import { promisify } from 'node:util'; import sharp from 'sharp'; -import { Writable } from 'stream'; -import { promisify } from 'util'; const probe = promisify<string, FfprobeData>(ffmpeg.ffprobe); sharp.concurrency(0); @@ -91,7 +91,7 @@ export class MediaRepository implements IMediaRepository { } if (typeof output !== 'string') { - throw new Error('Two-pass transcoding does not support writing to a stream'); + throw new TypeError('Two-pass transcoding does not support writing to a stream'); } // two-pass allows for precise control of bitrate at the cost of running twice @@ -124,12 +124,12 @@ export class MediaRepository implements IMediaRepository { .inputOptions(options.inputOptions) .outputOptions(options.outputOptions) .output(output) - .on('error', (err, stdout, stderr) => this.logger.error(stderr || err)); + .on('error', (error, stdout, stderr) => this.logger.error(stderr || error)); } chainPath(existing: string, path: string) { - const sep = existing.endsWith(':') ? '' : ':'; - return `${existing}${sep}${path}`; + const separator = existing.endsWith(':') ? '' : ':'; + return `${existing}${separator}${path}`; } async generateThumbhash(imagePath: string): Promise<Buffer> { diff --git a/server/src/infra/repositories/metadata.repository.ts b/server/src/infra/repositories/metadata.repository.ts index d916795bcb..83c05597a2 100644 --- a/server/src/infra/repositories/metadata.repository.ts +++ b/server/src/infra/repositories/metadata.repository.ts @@ -15,11 +15,11 @@ import { ImmichLogger } from '@app/infra/logger'; import { Inject } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { DefaultReadTaskOptions, exiftool, Tags } from 'exiftool-vendored'; -import { createReadStream, existsSync } from 'fs'; -import { readFile } from 'fs/promises'; import * as geotz from 'geo-tz'; import { getName } from 'i18n-iso-countries'; -import * as readLine from 'readline'; +import { createReadStream, existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import * as readLine from 'node:readline'; import { DataSource, DeepPartial, QueryRunner, Repository } from 'typeorm'; type GeoEntity = GeodataPlacesEntity | GeodataAdmin1Entity | GeodataAdmin2Entity; @@ -69,10 +69,10 @@ export class MetadataRepository implements IMetadataRepository { await this.loadAdmin2(queryRunner); await queryRunner.commitTransaction(); - } catch (e) { - this.logger.fatal('Error importing geodata', e); + } catch (error) { + this.logger.fatal('Error importing geodata', error); await queryRunner.rollbackTransaction(); - throw e; + throw error; } finally { await queryRunner.release(); } @@ -110,10 +110,10 @@ export class MetadataRepository implements IMetadataRepository { queryRunner, (lineSplit: string[]) => this.geodataPlacesRepository.create({ - id: parseInt(lineSplit[0]), + id: Number.parseInt(lineSplit[0]), name: lineSplit[1], - latitude: parseFloat(lineSplit[4]), - longitude: parseFloat(lineSplit[5]), + latitude: Number.parseFloat(lineSplit[4]), + longitude: Number.parseFloat(lineSplit[5]), countryCode: lineSplit[8], admin1Code: lineSplit[10], admin2Code: lineSplit[11], @@ -192,7 +192,8 @@ export class MetadataRepository implements IMetadataRepository { backfillTimezones: true, inferTimezoneFromDatestamps: true, useMWG: true, - numericTags: DefaultReadTaskOptions.numericTags.concat(['FocalLength']), + numericTags: [...DefaultReadTaskOptions.numericTags, 'FocalLength'], + /* eslint unicorn/no-array-callback-reference: off, unicorn/no-array-method-this-argument: off */ geoTz: (lat, lon) => geotz.find(lat, lon)[0], }) .catch((error) => { diff --git a/server/src/infra/repositories/person.repository.ts b/server/src/infra/repositories/person.repository.ts index 195fe5a5b4..85423b74dd 100644 --- a/server/src/infra/repositories/person.repository.ts +++ b/server/src/infra/repositories/person.repository.ts @@ -28,12 +28,7 @@ export class PersonRepository implements IPersonRepository { .createQueryBuilder() .update() .set({ personId: newPersonId }) - .where( - _.omitBy( - { personId: oldPersonId ? oldPersonId : undefined, id: faceIds ? In(faceIds) : undefined }, - _.isUndefined, - ), - ) + .where(_.omitBy({ personId: oldPersonId ?? undefined, id: faceIds ? In(faceIds) : undefined }, _.isUndefined)) .execute(); return result.affected ?? 0; diff --git a/server/src/infra/repositories/smart-info.repository.ts b/server/src/infra/repositories/smart-info.repository.ts index 4f9c52a66e..ab43ff6f91 100644 --- a/server/src/infra/repositories/smart-info.repository.ts +++ b/server/src/infra/repositories/smart-info.repository.ts @@ -31,11 +31,11 @@ export class SmartInfoRepository implements ISmartInfoRepository { throw new Error(`Invalid CLIP model name: ${modelName}`); } - const curDimSize = await this.getDimSize(); - this.logger.verbose(`Current database CLIP dimension size is ${curDimSize}`); + const currentDimSize = await this.getDimSize(); + this.logger.verbose(`Current database CLIP dimension size is ${currentDimSize}`); - if (dimSize != curDimSize) { - this.logger.log(`Dimension size of model ${modelName} is ${dimSize}, but database expects ${curDimSize}.`); + if (dimSize != currentDimSize) { + this.logger.log(`Dimension size of model ${modelName} is ${dimSize}, but database expects ${currentDimSize}.`); await this.updateDimSize(dimSize); } } @@ -119,7 +119,9 @@ export class SmartInfoRepository implements ISmartInfoRepository { cte = cte.andWhere('faces."personId" IS NOT NULL'); } - this.faceColumns.forEach((col) => cte.addSelect(`faces.${col}`, col)); + for (const col of this.faceColumns) { + cte.addSelect(`faces.${col}`, col); + } results = await manager .createQueryBuilder() @@ -157,8 +159,8 @@ export class SmartInfoRepository implements ISmartInfoRepository { throw new Error(`Invalid CLIP dimension size: ${dimSize}`); } - const curDimSize = await this.getDimSize(); - if (curDimSize === dimSize) { + const currentDimSize = await this.getDimSize(); + if (currentDimSize === dimSize) { return; } @@ -181,7 +183,7 @@ export class SmartInfoRepository implements ISmartInfoRepository { $$)`); }); - this.logger.log(`Successfully updated database CLIP dimension size from ${curDimSize} to ${dimSize}.`); + this.logger.log(`Successfully updated database CLIP dimension size from ${currentDimSize} to ${dimSize}.`); } private async getDimSize(): Promise<number> { diff --git a/server/src/infra/repositories/system-config.repository.ts b/server/src/infra/repositories/system-config.repository.ts index 4ab35b4d63..82d0b8c8be 100644 --- a/server/src/infra/repositories/system-config.repository.ts +++ b/server/src/infra/repositories/system-config.repository.ts @@ -1,7 +1,7 @@ import { ISystemConfigRepository } from '@app/domain'; import { InjectRepository } from '@nestjs/typeorm'; import axios from 'axios'; -import { readFile } from 'fs/promises'; +import { readFile } from 'node:fs/promises'; import { In, Repository } from 'typeorm'; import { SystemConfigEntity } from '../entities'; import { DummyValue, GenerateSql } from '../infra.util'; @@ -22,7 +22,7 @@ export class SystemConfigRepository implements ISystemConfigRepository { } readFile(filename: string): Promise<string> { - return readFile(filename, { encoding: 'utf-8' }); + return readFile(filename, { encoding: 'utf8' }); } saveAll(items: SystemConfigEntity[]): Promise<SystemConfigEntity[]> { diff --git a/server/src/infra/repositories/user.repository.ts b/server/src/infra/repositories/user.repository.ts index 5d55eea1f6..640eda0ee4 100644 --- a/server/src/infra/repositories/user.repository.ts +++ b/server/src/infra/repositories/user.repository.ts @@ -74,11 +74,7 @@ export class UserRepository implements IUserRepository { } async delete(user: UserEntity, hard?: boolean): Promise<UserEntity> { - if (hard) { - return this.userRepository.remove(user); - } else { - return this.userRepository.softRemove(user); - } + return hard ? this.userRepository.remove(user) : this.userRepository.softRemove(user); } async restore(user: UserEntity): Promise<UserEntity> { diff --git a/server/src/infra/sql-generator/index.ts b/server/src/infra/sql-generator/index.ts index b4b0978a23..348762d957 100644 --- a/server/src/infra/sql-generator/index.ts +++ b/server/src/infra/sql-generator/index.ts @@ -1,10 +1,11 @@ +#!/usr/bin/env node import { ISystemConfigRepository } from '@app/domain'; import { INestApplication } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { Test } from '@nestjs/testing'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { mkdir, rm, writeFile } from 'fs/promises'; -import { join } from 'path'; +import { mkdir, rm, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; import { databaseConfig } from '../database.config'; import { databaseEntities } from '../entities'; import { GENERATE_SQL_KEY, GenerateSqlQueries } from '../infra.util'; @@ -157,7 +158,7 @@ class SqlGenerator { private async write() { for (const [repoName, data] of Object.entries(this.results)) { - const filename = repoName.replace(/[A-Z]/g, (letter) => `.${letter.toLowerCase()}`).replace('.', ''); + const filename = repoName.replaceAll(/[A-Z]/g, (letter) => `.${letter.toLowerCase()}`).replace('.', ''); const file = join(this.options.targetDir, `${filename}.sql`); await writeFile(file, data.join('\n\n') + '\n'); } diff --git a/server/src/infra/sql-generator/sql.logger.ts b/server/src/infra/sql-generator/sql.logger.ts index 78c3df148e..6f3c298c08 100644 --- a/server/src/infra/sql-generator/sql.logger.ts +++ b/server/src/infra/sql-generator/sql.logger.ts @@ -1,8 +1,6 @@ +import { format } from 'sql-formatter'; import { Logger } from 'typeorm'; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const { format } = require('sql-formatter'); - export class SqlLogger implements Logger { queries: string[] = []; errors: Array<{ error: string | Error; query: string }> = []; diff --git a/server/src/infra/subscribers/audit.subscriber.ts b/server/src/infra/subscribers/audit.subscriber.ts index c0e8313077..896f9ae5e0 100644 --- a/server/src/infra/subscribers/audit.subscriber.ts +++ b/server/src/infra/subscribers/audit.subscriber.ts @@ -16,21 +16,23 @@ export class AuditSubscriber implements EntitySubscriberInterface<AssetEntity | private getAudit(entityName: string, entity: any): Partial<AuditEntity> | null { switch (entityName) { - case AssetEntity.name: + case AssetEntity.name: { const asset = entity as AssetEntity; return { entityType: EntityType.ASSET, entityId: asset.id, ownerId: asset.ownerId, }; + } - case AlbumEntity.name: + case AlbumEntity.name: { const album = entity as AlbumEntity; return { entityType: EntityType.ALBUM, entityId: album.id, ownerId: album.ownerId, }; + } } return null; diff --git a/server/src/main.ts b/server/src/main.ts index c43d6ea461..198b0f0877 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -10,18 +10,21 @@ if (process.argv[2] === immichApp) { function bootstrap() { switch (immichApp) { - case 'immich': + case 'immich': { process.title = 'immich_server'; return server(); - case 'microservices': + } + case 'microservices': { process.title = 'immich_microservices'; return microservices(); - case 'immich-admin': + } + case 'immich-admin': { process.title = 'immich_admin_cli'; return admin(); - default: - console.log(`Invalid app name: ${immichApp}. Expected one of immich|microservices|cli`); - process.exit(1); + } + default: { + throw new Error(`Invalid app name: ${immichApp}. Expected one of immich|microservices|cli`); + } } } void bootstrap(); diff --git a/server/src/microservices/utils/exif/coordinates.spec.ts b/server/src/microservices/utils/exif/coordinates.spec.ts index fd9ffd5d58..b9644fb49a 100644 --- a/server/src/microservices/utils/exif/coordinates.spec.ts +++ b/server/src/microservices/utils/exif/coordinates.spec.ts @@ -7,7 +7,7 @@ describe('parsing latitude from string input', () => { expect(parseLatitude('Infinity')).toBeNull(); expect(parseLatitude('-Infinity')).toBeNull(); expect(parseLatitude('90.001')).toBeNull(); - expect(parseLatitude(-90.000001)).toBeNull(); + expect(parseLatitude(-90.000_001)).toBeNull(); expect(parseLatitude('1000')).toBeNull(); expect(parseLatitude(-1000)).toBeNull(); }); @@ -15,10 +15,10 @@ describe('parsing latitude from string input', () => { it('returns the numeric coordinate for valid inputs', () => { expect(parseLatitude('90')).toBeCloseTo(90); expect(parseLatitude('-90')).toBeCloseTo(-90); - expect(parseLatitude(89.999999)).toBeCloseTo(89.999999); + expect(parseLatitude(89.999_999)).toBeCloseTo(89.999_999); expect(parseLatitude('-89.9')).toBeCloseTo(-89.9); expect(parseLatitude(0)).toBeCloseTo(0); - expect(parseLatitude('-0.0')).toBeCloseTo(-0.0); + expect(parseLatitude('-0.0')).toBeCloseTo(-0); }); }); @@ -32,7 +32,7 @@ describe('parsing longitude from string input', () => { it('returns null for invalid inputs', () => { expect(parseLongitude('')).toBeNull(); expect(parseLongitude('NaN')).toBeNull(); - expect(parseLongitude(Infinity)).toBeNull(); + expect(parseLongitude(Number.POSITIVE_INFINITY)).toBeNull(); expect(parseLongitude('-Infinity')).toBeNull(); expect(parseLongitude('180.001')).toBeNull(); expect(parseLongitude('-180.000001')).toBeNull(); @@ -43,10 +43,10 @@ describe('parsing longitude from string input', () => { it('returns the numeric coordinate for valid inputs', () => { expect(parseLongitude(180)).toBeCloseTo(180); expect(parseLongitude('-180')).toBeCloseTo(-180); - expect(parseLongitude('179.999999')).toBeCloseTo(179.999999); + expect(parseLongitude('179.999999')).toBeCloseTo(179.999_999); expect(parseLongitude(-179.9)).toBeCloseTo(-179.9); expect(parseLongitude('0')).toBeCloseTo(0); - expect(parseLongitude('-0.0')).toBeCloseTo(-0.0); + expect(parseLongitude('-0.0')).toBeCloseTo(-0); }); }); diff --git a/server/src/microservices/utils/numbers.spec.ts b/server/src/microservices/utils/numbers.spec.ts index 19aba8f76a..47f95b8aab 100644 --- a/server/src/microservices/utils/numbers.spec.ts +++ b/server/src/microservices/utils/numbers.spec.ts @@ -2,15 +2,15 @@ import { isDecimalNumber, isNumberInRange, toNumberOrNull } from './numbers'; describe('checks if a number is a decimal number', () => { it('returns false for non-decimal numbers', () => { - expect(isDecimalNumber(NaN)).toBe(false); - expect(isDecimalNumber(Infinity)).toBe(false); - expect(isDecimalNumber(-Infinity)).toBe(false); + expect(isDecimalNumber(Number.NaN)).toBe(false); + expect(isDecimalNumber(Number.POSITIVE_INFINITY)).toBe(false); + expect(isDecimalNumber(Number.NEGATIVE_INFINITY)).toBe(false); }); it('returns true for decimal numbers', () => { expect(isDecimalNumber(0)).toBe(true); expect(isDecimalNumber(-0)).toBe(true); - expect(isDecimalNumber(10.12345)).toBe(true); + expect(isDecimalNumber(10.123_45)).toBe(true); expect(isDecimalNumber(Number.MAX_VALUE)).toBe(true); expect(isDecimalNumber(Number.MIN_VALUE)).toBe(true); }); @@ -26,16 +26,17 @@ describe('checks if a number is within a range', () => { it('returns true for numbers inside the range', () => { expect(isNumberInRange(0, 0, 50)).toBe(true); expect(isNumberInRange(50, 0, 50)).toBe(true); - expect(isNumberInRange(-50.12345, -50.12345, 0)).toBe(true); + expect(isNumberInRange(-50.123_45, -50.123_45, 0)).toBe(true); }); }); describe('converts input to a number or null', () => { it('returns null for invalid inputs', () => { expect(toNumberOrNull(null)).toBeNull(); + // eslint-disable-next-line unicorn/no-useless-undefined expect(toNumberOrNull(undefined)).toBeNull(); expect(toNumberOrNull('')).toBeNull(); - expect(toNumberOrNull(NaN)).toBeNull(); + expect(toNumberOrNull(Number.NaN)).toBeNull(); }); it('returns a number for valid inputs', () => { diff --git a/server/src/microservices/utils/numbers.ts b/server/src/microservices/utils/numbers.ts index 4eb8884b1a..cd6e81d2a2 100644 --- a/server/src/microservices/utils/numbers.ts +++ b/server/src/microservices/utils/numbers.ts @@ -1,12 +1,12 @@ -export function isDecimalNumber(num: number): boolean { - return !Number.isNaN(num) && Number.isFinite(num); +export function isDecimalNumber(number_: number): boolean { + return !Number.isNaN(number_) && Number.isFinite(number_); } /** * Check if `num` is a valid number and is between `start` and `end` (inclusive) */ -export function isNumberInRange(num: number, start: number, end: number): boolean { - return isDecimalNumber(num) && num >= start && num <= end; +export function isNumberInRange(number_: number, start: number, end: number): boolean { + return isDecimalNumber(number_) && number_ >= start && number_ <= end; } export function toNumberOrNull(input: number | string | null | undefined): number | null { @@ -14,6 +14,6 @@ export function toNumberOrNull(input: number | string | null | undefined): numbe return null; } - const num = typeof input === 'string' ? Number.parseFloat(input) : input; - return isDecimalNumber(num) ? num : null; + const number_ = typeof input === 'string' ? Number.parseFloat(input) : input; + return isDecimalNumber(number_) ? number_ : null; } diff --git a/server/src/test-utils/utils.ts b/server/src/test-utils/utils.ts index 67ad5fff34..077239d8a3 100644 --- a/server/src/test-utils/utils.ts +++ b/server/src/test-utils/utils.ts @@ -7,8 +7,8 @@ import { Test } from '@nestjs/testing'; import { DateTime } from 'luxon'; import * as fs from 'node:fs'; import path from 'node:path'; +import { EventEmitter } from 'node:stream'; import { Server } from 'node:tls'; -import { EventEmitter } from 'stream'; import { EntityTarget, ObjectLiteral } from 'typeorm'; import { AppService } from '../immich/app.service'; import { AppService as MicroAppService } from '../microservices/app.service'; @@ -69,7 +69,7 @@ class JobMock implements IJobRepository { return this._handler(item); } queueAll(items: JobItem[]) { - return Promise.all(items.map(this._handler)).then(() => Promise.resolve()); + return Promise.all(items.map((arg) => this._handler(arg))).then(() => {}); } async resume() {} async empty() {} @@ -140,13 +140,13 @@ export const testApp = { export function waitForEvent<T>(emitter: EventEmitter, event: string): Promise<T> { return new Promise((resolve, reject) => { - const success = (val: T) => { + const success = (value: T) => { emitter.off('error', fail); - resolve(val); + resolve(value); }; - const fail = (err: Error) => { + const fail = (error: Error) => { emitter.off(event, success); - reject(err); + reject(error); }; emitter.once(event, success); emitter.once('error', fail); diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index fcc52df8f4..36f646af63 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -164,7 +164,7 @@ export const assetStub = { deletedAt: null, sidecarPath: null, exifInfo: { - fileSizeInByte: 5_000, + fileSizeInByte: 5000, } as ExifEntity, stack: assetStackStub('stack-1', [ { id: 'primary-asset-id' } as AssetEntity, @@ -209,7 +209,7 @@ export const assetStub = { deletedAt: null, sidecarPath: null, exifInfo: { - fileSizeInByte: 5_000, + fileSizeInByte: 5000, } as ExifEntity, }), @@ -249,7 +249,7 @@ export const assetStub = { deletedAt: null, sidecarPath: null, exifInfo: { - fileSizeInByte: 5_000, + fileSizeInByte: 5000, } as ExifEntity, }), @@ -288,7 +288,7 @@ export const assetStub = { faces: [], sidecarPath: null, exifInfo: { - fileSizeInByte: 5_000, + fileSizeInByte: 5000, } as ExifEntity, deletedAt: null, }), @@ -329,7 +329,7 @@ export const assetStub = { faces: [], sidecarPath: null, exifInfo: { - fileSizeInByte: 5_000, + fileSizeInByte: 5000, } as ExifEntity, }), @@ -368,7 +368,7 @@ export const assetStub = { faces: [], sidecarPath: null, exifInfo: { - fileSizeInByte: 5_000, + fileSizeInByte: 5000, } as ExifEntity, deletedAt: null, }), diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts index ee6b767ef0..30dfec1669 100644 --- a/server/test/fixtures/media.stub.ts +++ b/server/test/fixtures/media.stub.ts @@ -94,7 +94,7 @@ export const probeStub = { formatName: 'mov,mp4,m4a,3gp,3g2,mj2', formatLongName: 'QuickTime / MOV', duration: 0, - bitrate: 40000000, + bitrate: 40_000_000, }, }), videoStreamHDR: Object.freeze<VideoInfo>({ diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts index d37a4af6e0..f34e6b06b5 100644 --- a/server/test/repositories/database.repository.mock.ts +++ b/server/test/repositories/database.repository.mock.ts @@ -6,7 +6,7 @@ export const newDatabaseRepositoryMock = (): jest.Mocked<IDatabaseRepository> => getPostgresVersion: jest.fn().mockResolvedValue(new Version(14, 0, 0)), createExtension: jest.fn().mockImplementation(() => Promise.resolve()), runMigrations: jest.fn(), - withLock: jest.fn().mockImplementation((_, func: <R>() => Promise<R>) => func()), + withLock: jest.fn().mockImplementation((_, function_: <R>() => Promise<R>) => function_()), isBusy: jest.fn(), wait: jest.fn(), }; diff --git a/server/tsconfig.json b/server/tsconfig.json index d86cba04c5..6d89fe7088 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "module": "Node16", + "module": "node16", "strict": true, "declaration": true, "removeComments": true, @@ -8,7 +8,7 @@ "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true, - "target": "es2021", + "target": "es2022", "moduleResolution": "node16", "sourceMap": true, "outDir": "./dist", @@ -25,8 +25,8 @@ "@app/infra": ["src/infra"], "@app/infra/*": ["src/infra/*"], "@app/domain": ["src/domain"], - "@app/domain/*": ["src/domain/*"] - } + "@app/domain/*": ["src/domain/*"], + }, }, - "exclude": ["dist", "node_modules", "upload"] + "exclude": ["dist", "node_modules", "upload"], } diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs index 9277676ac5..de62060e0f 100644 --- a/web/.eslintrc.cjs +++ b/web/.eslintrc.cjs @@ -1,12 +1,17 @@ /** @type {import('eslint').Linter.Config} */ module.exports = { root: true, - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:svelte/recommended'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:svelte/recommended', + 'plugin:unicorn/recommended', + ], parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], parserOptions: { sourceType: 'module', - ecmaVersion: 2020, + ecmaVersion: 2022, extraFileExtensions: ['.svelte'], }, env: { @@ -27,6 +32,12 @@ module.exports = { NodeJS: true, }, rules: { + 'unicorn/no-useless-undefined': 'off', + 'unicorn/prefer-spread': 'off', + 'unicorn/no-null': 'off', + 'unicorn/prevent-abbreviations': 'off', + 'unicorn/no-nested-ternary': 'off', + 'unicorn/consistent-function-scoping': 'off', '@typescript-eslint/no-unused-vars': [ 'warn', { diff --git a/web/package-lock.json b/web/package-lock.json index 835b0451e3..93c04c47b6 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -38,13 +38,14 @@ "@types/justified-layout": "^4.1.0", "@types/lodash-es": "^4.17.6", "@types/luxon": "^3.2.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^6.4.1", + "@typescript-eslint/parser": "^6.4.1", "@vitest/coverage-v8": "^1.0.4", "autoprefixer": "^10.4.13", "eslint": "^8.34.0", - "eslint-config-prettier": "^9.0.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.30.0", + "eslint-plugin-unicorn": "^50.0.1", "factory.ts": "^1.3.0", "identity-obj-proxy": "^3.0.0", "postcss": "^8.4.21", @@ -55,7 +56,7 @@ "svelte-preprocess": "^5.0.3", "tailwindcss": "^3.2.7", "tslib": "^2.5.0", - "typescript": "^5.0.0", + "typescript": "^5.3.3", "vite": "^5.0.10", "vitest": "^1.0.4" } @@ -1774,6 +1775,12 @@ "optional": true, "peer": true }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, "node_modules/@types/pbf": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", @@ -2734,6 +2741,18 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytewise": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz", @@ -2894,6 +2913,33 @@ "node": ">= 6" } }, + "node_modules/ci-info": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", + "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -2981,6 +3027,19 @@ "resolved": "https://registry.npmjs.org/copy-image-clipboard/-/copy-image-clipboard-2.1.2.tgz", "integrity": "sha512-3VCXVl2IpFfOyD8drv9DozcNlwmqBqxOlsgkEGyVAzadjlPk1go8YNZyy8QmTnwHPxSFpeCR9OdsStEdVK7qDA==" }, + "node_modules/core-js-compat": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", + "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==", + "dev": true, + "dependencies": { + "browserslist": "^4.22.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3339,6 +3398,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -3568,6 +3636,84 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/eslint-plugin-unicorn": { + "version": "50.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz", + "integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "@eslint-community/eslint-utils": "^4.4.0", + "@eslint/eslintrc": "^2.1.4", + "ci-info": "^4.0.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.34.0", + "esquery": "^1.5.0", + "indent-string": "^4.0.0", + "is-builtin-module": "^3.2.1", + "jsesc": "^3.0.2", + "pluralize": "^8.0.0", + "read-pkg-up": "^7.0.1", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.10.0", + "semver": "^7.5.4", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=8.56.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -4328,6 +4474,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -4567,6 +4719,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "node_modules/is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -4607,6 +4765,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -5039,6 +5212,12 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -5546,6 +5725,27 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -5740,6 +5940,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5752,6 +5961,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -5892,6 +6119,15 @@ "pathe": "^1.1.0" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/pmtiles": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-2.11.0.tgz", @@ -6213,6 +6449,108 @@ "pify": "^2.3.0" } }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6244,6 +6582,15 @@ "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", "dev": true }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", @@ -6261,6 +6608,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regjsparser": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz", + "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -6677,6 +7045,38 @@ "source-map": "^0.6.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz", + "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "dev": true + }, "node_modules/split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -7498,6 +7898,16 @@ "node": ">=10.12.0" } }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/vite": { "version": "5.0.12", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", diff --git a/web/package.json b/web/package.json index fa7934d1a7..9e4ccf460a 100644 --- a/web/package.json +++ b/web/package.json @@ -32,13 +32,14 @@ "@types/justified-layout": "^4.1.0", "@types/lodash-es": "^4.17.6", "@types/luxon": "^3.2.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^6.4.1", + "@typescript-eslint/parser": "^6.4.1", "@vitest/coverage-v8": "^1.0.4", "autoprefixer": "^10.4.13", "eslint": "^8.34.0", - "eslint-config-prettier": "^9.0.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.30.0", + "eslint-plugin-unicorn": "^50.0.1", "factory.ts": "^1.3.0", "identity-obj-proxy": "^3.0.0", "postcss": "^8.4.21", @@ -49,7 +50,7 @@ "svelte-preprocess": "^5.0.3", "tailwindcss": "^3.2.7", "tslib": "^2.5.0", - "typescript": "^5.0.0", + "typescript": "^5.3.3", "vite": "^5.0.10", "vitest": "^1.0.4" }, diff --git a/web/src/api/api.ts b/web/src/api/api.ts index 387c754b25..78228aee1f 100644 --- a/web/src/api/api.ts +++ b/web/src/api/api.ts @@ -26,7 +26,7 @@ import { common, configuration, } from '@immich/sdk'; -import type { ApiParams } from './types'; +import type { ApiParams as ApiParameters } from './types'; class ImmichApi { public activityApi: ActivityApi; @@ -56,8 +56,8 @@ class ImmichApi { return !!this.key; } - constructor(params: configuration.ConfigurationParameters) { - this.config = new configuration.Configuration(params); + constructor(parameters: configuration.ConfigurationParameters) { + this.config = new configuration.Configuration(parameters); this.activityApi = new ActivityApi(this.config); this.albumApi = new AlbumApi(this.config); @@ -80,17 +80,17 @@ class ImmichApi { this.trashApi = new TrashApi(this.config); } - private createUrl(path: string, params?: Record<string, unknown>) { - const searchParams = new URLSearchParams(); - for (const key in params) { - const value = params[key]; + private createUrl(path: string, parameters?: Record<string, unknown>) { + const searchParameters = new URLSearchParams(); + for (const key in parameters) { + const value = parameters[key]; if (value !== undefined && value !== null) { - searchParams.set(key, value.toString()); + searchParameters.set(key, value.toString()); } } const url = new URL(path, common.DUMMY_BASE_URL); - url.search = searchParams.toString(); + url.search = searchParameters.toString(); return (this.config.basePath || base.BASE_PATH) + common.toPathString(url); } @@ -115,17 +115,17 @@ class ImmichApi { this.config.basePath = baseUrl; } - public getAssetFileUrl(...[assetId, isThumb, isWeb]: ApiParams<typeof AssetApiFp, 'serveFile'>) { + public getAssetFileUrl(...[assetId, isThumb, isWeb]: ApiParameters<typeof AssetApiFp, 'serveFile'>) { const path = `/asset/file/${assetId}`; return this.createUrl(path, { isThumb, isWeb, key: this.getKey() }); } - public getAssetThumbnailUrl(...[assetId, format]: ApiParams<typeof AssetApiFp, 'getAssetThumbnail'>) { + public getAssetThumbnailUrl(...[assetId, format]: ApiParameters<typeof AssetApiFp, 'getAssetThumbnail'>) { const path = `/asset/thumbnail/${assetId}`; return this.createUrl(path, { format, key: this.getKey() }); } - public getProfileImageUrl(...[userId]: ApiParams<typeof UserApiFp, 'getProfileImage'>) { + public getProfileImageUrl(...[userId]: ApiParameters<typeof UserApiFp, 'getProfileImage'>) { const path = `/user/profile-image/${userId}`; return this.createUrl(path); } diff --git a/web/src/api/types.ts b/web/src/api/types.ts index a830c947d0..96baf2f3aa 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -1,7 +1,7 @@ import type { Configuration } from '@immich/sdk'; /* eslint-disable @typescript-eslint/no-explicit-any */ -export type ApiFp = (configuration: Configuration) => Record<any, (...args: any) => any>; +export type ApiFp = (configuration: Configuration) => Record<any, (...arguments_: any) => any>; export type OmitLast<T extends readonly unknown[]> = T extends readonly [...infer U, any?] ? U : [...T]; diff --git a/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte b/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte index 20444bb29c..dfb93f7033 100644 --- a/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte +++ b/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte @@ -14,10 +14,10 @@ const deleteUser = async () => { try { const deletedUser = await api.userApi.deleteUser({ id: user.id }); - if (deletedUser.data.deletedAt != null) { - dispatch('success'); - } else { + if (deletedUser.data.deletedAt == undefined) { dispatch('fail'); + } else { + dispatch('success'); } } catch (error) { handleError(error, 'Unable to delete user'); diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/admin-page/jobs/job-tile.svelte index d0904ad94d..75d8ab6b81 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile.svelte +++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte @@ -18,7 +18,7 @@ } from '@mdi/js'; export let title: string; - export let subtitle: string | undefined = undefined; + export let subtitle: string | undefined; export let jobCounts: JobCountsDto; export let queueStatus: QueueStatusDto; export let allowForceCommand = true; diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte index 34604e852c..2efd2c1bf6 100644 --- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte +++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte @@ -131,12 +131,13 @@ jobs[jobId] = data; switch (jobCommand.command) { - case JobCommand.Empty: + case JobCommand.Empty: { notificationController.show({ message: `Cleared jobs for: ${title}`, type: NotificationType.Info, }); break; + } } } catch (error) { handleError(error, `Command '${jobCommand.command}' failed for job: ${title}`); diff --git a/web/src/lib/components/admin-page/restore-dialoge.svelte b/web/src/lib/components/admin-page/restore-dialoge.svelte index 19227a3abd..95525ed9d3 100644 --- a/web/src/lib/components/admin-page/restore-dialoge.svelte +++ b/web/src/lib/components/admin-page/restore-dialoge.svelte @@ -12,7 +12,7 @@ const restoreUser = async () => { const restoredUser = await api.userApi.restoreUser({ id: user.id }); - if (restoredUser.data.deletedAt == null) { + if (restoredUser.data.deletedAt == undefined) { dispatch('success'); } else { dispatch('fail'); diff --git a/web/src/lib/components/admin-page/settings/admin-settings.svelte b/web/src/lib/components/admin-page/settings/admin-settings.svelte index 98e202336a..3f7ddf7614 100644 --- a/web/src/lib/components/admin-page/settings/admin-settings.svelte +++ b/web/src/lib/components/admin-page/settings/admin-settings.svelte @@ -19,11 +19,7 @@ const dispatch = createEventDispatcher<{ save: void }>(); const handleReset = async (detail: SettingsEventType['reset']) => { - if (detail.default) { - await resetToDefault(detail.configKeys); - } else { - await reset(detail.configKeys); - } + await (detail.default ? resetToDefault(detail.configKeys) : reset(detail.configKeys)); }; const handleSave = async (update: Partial<SystemConfigDto>) => { @@ -47,7 +43,10 @@ const reset = async (configKeys: Array<keyof SystemConfigDto>) => { const { data: resetConfig } = await api.systemConfigApi.getConfig(); - config = configKeys.reduce((acc, key) => ({ ...acc, [key]: resetConfig[key] }), config); + + for (const key of configKeys) { + config = { ...config, [key]: resetConfig[key] }; + } notificationController.show({ message: 'Reset settings to the recent saved settings', @@ -56,7 +55,9 @@ }; const resetToDefault = async (configKeys: Array<keyof SystemConfigDto>) => { - config = configKeys.reduce((acc, key) => ({ ...acc, [key]: defaultConfig[key] }), config); + for (const key of configKeys) { + config = { ...config, [key]: defaultConfig[key] }; + } notificationController.show({ message: 'Reset settings to default', diff --git a/web/src/lib/components/admin-page/settings/setting-checkboxes.svelte b/web/src/lib/components/admin-page/settings/setting-checkboxes.svelte index ba5d0b2408..506cd042ba 100644 --- a/web/src/lib/components/admin-page/settings/setting-checkboxes.svelte +++ b/web/src/lib/components/admin-page/settings/setting-checkboxes.svelte @@ -11,11 +11,7 @@ export let disabled = false; function handleCheckboxChange(option: string) { - if (value.includes(option)) { - value = value.filter((item) => item !== option); - } else { - value = [...value, option]; - } + value = value.includes(option) ? value.filter((item) => item !== option) : [...value, option]; } </script> diff --git a/web/src/lib/components/admin-page/settings/setting-select.svelte b/web/src/lib/components/admin-page/settings/setting-select.svelte index 5b080c2328..d79f60a2ac 100644 --- a/web/src/lib/components/admin-page/settings/setting-select.svelte +++ b/web/src/lib/components/admin-page/settings/setting-select.svelte @@ -17,7 +17,7 @@ const handleChange = (e: Event) => { value = (e.target as HTMLInputElement).value; if (number) { - value = parseInt(value); + value = Number.parseInt(value); } dispatch('select', value); }; diff --git a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte index 026a5b4788..5e2d7b781b 100644 --- a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte +++ b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte @@ -38,7 +38,7 @@ $: parsedTemplate = () => { try { return renderTemplate(config.storageTemplate.template); - } catch (error) { + } catch { return 'error'; } }; diff --git a/web/src/lib/components/album-page/__tests__/album-card.spec.ts b/web/src/lib/components/album-page/__tests__/album-card.spec.ts index 9bdf97a211..bbcd2f5c6a 100644 --- a/web/src/lib/components/album-page/__tests__/album-card.spec.ts +++ b/web/src/lib/components/album-page/__tests__/album-card.spec.ts @@ -122,10 +122,10 @@ describe('AlbumCard component', () => { const onClickHandler = vi.fn(); sut.component.$on('showalbumcontextmenu', onClickHandler); - const contextMenuBtnParent = sut.getByTestId('context-button-parent'); + const contextMenuButtonParent = sut.getByTestId('context-button-parent'); // Mock getBoundingClientRect to return a bounding rectangle that will result in the expected position - contextMenuBtnParent.getBoundingClientRect = () => ({ + contextMenuButtonParent.getBoundingClientRect = () => ({ x: 123, y: 456, width: 0, @@ -138,7 +138,7 @@ describe('AlbumCard component', () => { }); await fireEvent( - contextMenuBtnParent, + contextMenuButtonParent, new MouseEvent('click', { clientX: 123, clientY: 456, diff --git a/web/src/lib/components/album-page/album-card.svelte b/web/src/lib/components/album-page/album-card.svelte index a9e94c4dd8..34b87e8df4 100644 --- a/web/src/lib/components/album-page/album-card.svelte +++ b/web/src/lib/components/album-page/album-card.svelte @@ -25,7 +25,7 @@ const dispatchShowContextMenu = createEventDispatcher<OnShowContextMenu>(); const loadHighQualityThumbnail = async (thubmnailId: string | null) => { - if (thubmnailId == null) { + if (thubmnailId == undefined) { return; } diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index 23cf5959c5..9777cd1f5b 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -83,11 +83,12 @@ } if (!$showAssetViewer) { switch (event.key) { - case 'Escape': + case 'Escape': { if ($isMultiSelectState) { assetInteractionStore.clearMultiselect(); } return; + } } } }; diff --git a/web/src/lib/components/album-page/share-info-modal.svelte b/web/src/lib/components/album-page/share-info-modal.svelte index 144c2dcce5..d05760c390 100644 --- a/web/src/lib/components/album-page/share-info-modal.svelte +++ b/web/src/lib/components/album-page/share-info-modal.svelte @@ -30,8 +30,8 @@ try { const { data } = await api.userApi.getMyUserInfo(); currentUser = data; - } catch (e) { - handleError(e, 'Unable to refresh user'); + } catch (error) { + handleError(error, 'Unable to refresh user'); } }); @@ -58,8 +58,8 @@ dispatch('remove', userId); const message = userId === 'me' ? `Left ${album.albumName}` : `Removed ${selectedRemoveUser.name}`; notificationController.show({ type: NotificationType.Info, message }); - } catch (e) { - handleError(e, 'Unable to remove user'); + } catch (error) { + handleError(error, 'Unable to remove user'); } finally { selectedRemoveUser = null; } diff --git a/web/src/lib/components/album-page/thumbnail-selection.svelte b/web/src/lib/components/album-page/thumbnail-selection.svelte index e2c1968b29..f098fbf69a 100644 --- a/web/src/lib/components/album-page/thumbnail-selection.svelte +++ b/web/src/lib/components/album-page/thumbnail-selection.svelte @@ -16,11 +16,7 @@ }>(); $: isSelected = (id: string): boolean | undefined => { - if (!selectedThumbnail && album.albumThumbnailAssetId == id) { - return true; - } else { - return selectedThumbnail?.id == id; - } + return !selectedThumbnail && album.albumThumbnailAssetId == id ? true : selectedThumbnail?.id == id; }; </script> diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/components/album-page/user-selection-modal.svelte index 8e77df9d95..60e082d439 100644 --- a/web/src/lib/components/album-page/user-selection-modal.svelte +++ b/web/src/lib/components/album-page/user-selection-modal.svelte @@ -28,9 +28,9 @@ users = data.filter((user) => !(user.deletedAt || user.id === album.ownerId)); // Remove the existed shared users from the album - album.sharedUsers.forEach((sharedUser) => { + for (const sharedUser of album.sharedUsers) { users = users.filter((user) => user.id !== sharedUser.id); - }); + } }); const getSharedLinks = async () => { @@ -40,11 +40,9 @@ }; const handleSelect = (user: UserResponseDto) => { - if (selectedUsers.includes(user)) { - selectedUsers = selectedUsers.filter((selectedUser) => selectedUser.id !== user.id); - } else { - selectedUsers = [...selectedUsers, user]; - } + selectedUsers = selectedUsers.includes(user) + ? selectedUsers.filter((selectedUser) => selectedUser.id !== user.id) + : [...selectedUsers, user]; }; const handleUnselect = (user: UserResponseDto) => { @@ -122,7 +120,7 @@ size="sm" fullwidth rounded="full" - disabled={!selectedUsers.length} + disabled={selectedUsers.length === 0} on:click={() => dispatch('select', selectedUsers)}>Add</Button > </div> diff --git a/web/src/lib/components/asset-viewer/activity-viewer.svelte b/web/src/lib/components/asset-viewer/activity-viewer.svelte index b9c280ff92..723a2fa7fb 100644 --- a/web/src/lib/components/asset-viewer/activity-viewer.svelte +++ b/web/src/lib/components/asset-viewer/activity-viewer.svelte @@ -66,7 +66,7 @@ close: void; }>(); - $: showDeleteReaction = Array(reactions.length).fill(false); + $: showDeleteReaction = Array.from({ length: reactions.length }).fill(false); $: { if (innerHeight && activityHeight) { divHeight = innerHeight - activityHeight; @@ -198,7 +198,7 @@ {/if} {#if reaction.user.id === user.id || albumOwnerId === user.id} <div class="flex items-start w-fit pt-[5px]" title="Delete comment"> - <button on:click={() => (!showDeleteReaction[index] ? showOptionsMenu(index) : '')}> + <button on:click={() => (showDeleteReaction[index] ? '' : showOptionsMenu(index))}> <Icon path={mdiDotsVertical} /> </button> </div> @@ -244,7 +244,7 @@ {/if} {#if reaction.user.id === user.id || albumOwnerId === user.id} <div class="flex items-start w-fit" title="Delete like"> - <button on:click={() => (!showDeleteReaction[index] ? showOptionsMenu(index) : '')}> + <button on:click={() => (showDeleteReaction[index] ? '' : showOptionsMenu(index))}> <Icon path={mdiDotsVertical} /> </button> </div> diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 058b5f42e1..0218ee7754 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -145,11 +145,7 @@ albumId: album.id, type: ReactionType.Like, }); - if (data.length > 0) { - isLiked = data[0]; - } else { - isLiked = null; - } + isLiked = data.length > 0 ? data[0] : null; } catch (error) { handleError(error, "Can't get Favorite"); } @@ -238,8 +234,8 @@ try { const { data } = await api.albumApi.getAllAlbums({ assetId: asset.id }); appearsInAlbums = data; - } catch (e) { - console.error('Error getting album that asset belong to', e); + } catch (error) { + console.error('Error getting album that asset belong to', error); } }; @@ -260,40 +256,48 @@ switch (key) { case 'a': - case 'A': + case 'A': { if (shiftKey) { toggleArchive(); } return; - case 'ArrowLeft': + } + case 'ArrowLeft': { navigateAssetBackward(); return; - case 'ArrowRight': + } + case 'ArrowRight': { navigateAssetForward(); return; + } case 'd': - case 'D': + case 'D': { if (shiftKey) { downloadFile(asset); } return; - case 'Delete': + } + case 'Delete': { trashOrDelete(shiftKey); return; - case 'Escape': + } + case 'Escape': { if (isShowDeleteConfirmation) { isShowDeleteConfirmation = false; return; } closeViewer(); return; - case 'f': + } + case 'f': { toggleFavorite(); return; - case 'i': + } + case 'i': { isShowActivity = false; $isShowDetail = !$isShowDetail; return; + } } }; @@ -383,8 +387,8 @@ message: 'Moved to trash', type: NotificationType.Info, }); - } catch (e) { - handleError(e, 'Unable to trash asset'); + } catch (error) { + handleError(error, 'Unable to trash asset'); } }; @@ -398,8 +402,8 @@ message: 'Permanently deleted asset', type: NotificationType.Info, }); - } catch (e) { - handleError(e, 'Unable to delete asset'); + } catch (error) { + handleError(error, 'Unable to delete asset'); } finally { isShowDeleteConfirmation = false; } @@ -537,11 +541,7 @@ const handleStackedAssetMouseEvent = (e: CustomEvent<{ isMouseOver: boolean }>, asset: AssetResponseDto) => { const { isMouseOver } = e.detail; - if (isMouseOver) { - previewStackedAsset = asset; - } else { - previewStackedAsset = undefined; - } + previewStackedAsset = isMouseOver ? asset : undefined; }; const handleUnstack = async () => { diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 30f3ed0cdb..fea5676b54 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -108,10 +108,11 @@ } const ctrl = event.ctrlKey; switch (event.key) { - case 'Enter': + case 'Enter': { if (ctrl && event.target === textArea) { handleFocusOut(); } + } } }; @@ -222,7 +223,7 @@ bind:this={textArea} class="max-h-[500px] w-full resize-none overflow-hidden border-b border-gray-500 bg-transparent text-base text-black outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:text-white dark:focus:border-immich-dark-primary" - placeholder={!isOwner ? '' : 'Add a description'} + placeholder={isOwner ? 'Add a description' : ''} on:focusin={handleFocusIn} on:focusout={handleFocusOut} on:input={() => autoGrowHeight(textArea)} diff --git a/web/src/lib/components/asset-viewer/panorama-viewer.svelte b/web/src/lib/components/asset-viewer/panorama-viewer.svelte index 4013568287..be398ee07b 100644 --- a/web/src/lib/components/asset-viewer/panorama-viewer.svelte +++ b/web/src/lib/components/asset-viewer/panorama-viewer.svelte @@ -20,9 +20,9 @@ dataUrl = URL.createObjectURL(data); return dataUrl; } else { - throw new Error('Invalid data format'); + throw new TypeError('Invalid data format'); } - } catch (error) { + } catch { errorMessage = 'Failed to load asset'; return ''; } diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 177c5ba151..cf7c16b3d8 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -20,7 +20,7 @@ let assetData: string; let abortController: AbortController; let hasZoomed = false; - let copyImageToClipboard: (src: string) => Promise<Blob>; + let copyImageToClipboard: (source: string) => Promise<Blob>; let canCopyImagesToClipboard: () => boolean; $: if (imgElement) { @@ -90,8 +90,8 @@ message: 'Copied image to clipboard.', timeout: 3000, }); - } catch (err) { - console.error('Error [photo-viewer]:', err); + } catch (error) { + console.error('Error [photo-viewer]:', error); notificationController.show({ type: NotificationType.Error, message: 'Copying image to clipboard failed.', diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte index 1be7e8ad21..c7aa0b6d83 100644 --- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte @@ -2,6 +2,7 @@ import { onMount, tick } from 'svelte'; import { fade } from 'svelte/transition'; import { thumbHashToDataURL } from 'thumbhash'; + // eslint-disable-next-line unicorn/prefer-node-protocol import { Buffer } from 'buffer'; import { mdiEyeOffOutline } from '@mdi/js'; import Icon from '$lib/components/elements/icon.svelte'; diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index 0cea5be9fb..de540b3208 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -161,7 +161,7 @@ {#if asset.stackCount && showStackedIcon} <div - class="absolute {asset.type == AssetTypeEnum.Image && asset.livePhotoVideoId == null + class="absolute {asset.type == AssetTypeEnum.Image && asset.livePhotoVideoId == undefined ? 'top-0 right-0' : 'top-7 right-1'} z-20 flex place-items-center gap-1 text-xs font-medium text-white" > diff --git a/web/src/lib/components/elements/dropdown.svelte b/web/src/lib/components/elements/dropdown.svelte index b69b191f25..57757dafdb 100644 --- a/web/src/lib/components/elements/dropdown.svelte +++ b/web/src/lib/components/elements/dropdown.svelte @@ -26,7 +26,7 @@ export let options: T[]; export let selectedOption = options[0]; - export let render: (item: T) => string | RenderedOption = (item) => String(item); + export let render: (item: T) => string | RenderedOption = String; type RenderedOption = { title: string; @@ -54,13 +54,15 @@ const renderOption = (option: T): RenderedOption => { const renderedOption = render(option); switch (typeof renderedOption) { - case 'string': + case 'string': { return { title: renderedOption }; - default: + } + default: { return { title: renderedOption.title, icon: renderedOption.icon, }; + } } }; diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte index ec1b614332..cb02ce5bb2 100644 --- a/web/src/lib/components/faces-page/assign-face-side-panel.svelte +++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte @@ -47,8 +47,8 @@ img.src = data; await new Promise<void>((resolve) => { - img.onload = () => resolve(); - img.onerror = () => resolve(); + img.addEventListener('load', () => resolve()); + img.addEventListener('error', () => resolve()); }); image = img; @@ -56,13 +56,20 @@ if (image === null) { return null; } - const { boundingBoxX1: x1, boundingBoxX2: x2, boundingBoxY1: y1, boundingBoxY2: y2 } = face; + const { + boundingBoxX1: x1, + boundingBoxX2: x2, + boundingBoxY1: y1, + boundingBoxY2: y2, + imageWidth, + imageHeight, + } = face; const coordinates = { - x1: (image.naturalWidth / face.imageWidth) * x1, - x2: (image.naturalWidth / face.imageWidth) * x2, - y1: (image.naturalHeight / face.imageHeight) * y1, - y2: (image.naturalHeight / face.imageHeight) * y2, + x1: (image.naturalWidth / imageWidth) * x1, + x2: (image.naturalWidth / imageWidth) * x2, + y1: (image.naturalHeight / imageHeight) * y1, + y2: (image.naturalHeight / imageHeight) * y2, }; const faceWidth = coordinates.x2 - coordinates.x1; @@ -72,17 +79,17 @@ faceImage.src = image.src; await new Promise((resolve) => { - faceImage.onload = resolve; - faceImage.onerror = () => resolve(null); + faceImage.addEventListener('load', resolve); + faceImage.addEventListener('error', () => resolve(null)); }); const canvas = document.createElement('canvas'); canvas.width = faceWidth; canvas.height = faceHeight; - const ctx = canvas.getContext('2d'); - if (ctx) { - ctx.drawImage(faceImage, coordinates.x1, coordinates.y1, faceWidth, faceHeight, 0, 0, faceWidth, faceHeight); + const context = canvas.getContext('2d'); + if (context) { + context.drawImage(faceImage, coordinates.x1, coordinates.y1, faceWidth, faceHeight, 0, 0, faceWidth, faceHeight); return canvas.toDataURL(); } else { diff --git a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte index cb5022d23d..ab77a0df03 100644 --- a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte +++ b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte @@ -71,7 +71,7 @@ }} > <ImageThumbnail - border={potentialMergePeople.length !== 0} + border={potentialMergePeople.length > 0} circle shadow url={api.getPeopleThumbnailUrl(personMerge2.id)} diff --git a/web/src/lib/components/faces-page/people-list.svelte b/web/src/lib/components/faces-page/people-list.svelte index 5ae4bcc775..5794e0c67d 100644 --- a/web/src/lib/components/faces-page/people-list.svelte +++ b/web/src/lib/components/faces-page/people-list.svelte @@ -34,10 +34,8 @@ people = peopleCopy; return; } - if (!force) { - if (people.length < maximumLengthSearchPeople && name.startsWith(searchWord)) { - return; - } + if (!force && people.length < maximumLengthSearchPeople && name.startsWith(searchWord)) { + return; } const timeout = setTimeout(() => (isSearchingPeople = true), timeBeforeShowLoadingSpinner); diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte index 94aba63f5b..04fd47c273 100644 --- a/web/src/lib/components/faces-page/person-side-panel.svelte +++ b/web/src/lib/components/faces-page/person-side-panel.svelte @@ -72,8 +72,8 @@ allPeople = data.people; const result = await api.faceApi.getFaces({ id: assetId }); peopleWithFaces = result.data; - selectedPersonToCreate = new Array<string | null>(peopleWithFaces.length); - selectedPersonToReassign = new Array<PersonResponseDto | null>(peopleWithFaces.length); + selectedPersonToCreate = Array.from({ length: peopleWithFaces.length }); + selectedPersonToReassign = Array.from({ length: peopleWithFaces.length }); } catch (error) { handleError(error, "Can't get faces"); } finally { @@ -106,20 +106,20 @@ selectedPersonToReassign.filter((person) => person !== null).length; if (numberOfChanges > 0) { try { - for (let i = 0; i < peopleWithFaces.length; i++) { - const personId = selectedPersonToReassign[i]?.id; + for (const [index, peopleWithFace] of peopleWithFaces.entries()) { + const personId = selectedPersonToReassign[index]?.id; if (personId) { await api.faceApi.reassignFacesById({ id: personId, - faceDto: { id: peopleWithFaces[i].id }, + faceDto: { id: peopleWithFace.id }, }); - } else if (selectedPersonToCreate[i]) { + } else if (selectedPersonToCreate[index]) { const { data } = await api.personApi.createPerson(); numberOfPersonToCreate.push(data.id); await api.faceApi.reassignFacesById({ id: data.id, - faceDto: { id: peopleWithFaces[i].id }, + faceDto: { id: peopleWithFace.id }, }); } } @@ -138,7 +138,7 @@ clearTimeout(loaderLoadingDoneTimeout); dispatch('refresh'); } else { - automaticRefreshTimeout = setTimeout(() => dispatch('refresh'), 15000); + automaticRefreshTimeout = setTimeout(() => dispatch('refresh'), 15_000); } }; diff --git a/web/src/lib/components/forms/create-user-form.svelte b/web/src/lib/components/forms/create-user-form.svelte index 2d3ab93410..b0434648c0 100644 --- a/web/src/lib/components/forms/create-user-form.svelte +++ b/web/src/lib/components/forms/create-user-form.svelte @@ -14,7 +14,7 @@ let confirmPassowrd = ''; let canCreateUser = false; - let quotaSize: number | undefined = undefined; + let quotaSize: number | undefined; let isCreatingUser = false; $: quotaSizeWarning = quotaSize && convertToBytes(Number(quotaSize), 'GiB') > $serverInfo.diskSizeRaw; @@ -69,11 +69,10 @@ error = 'Error create user account'; isCreatingUser = false; } - } catch (e) { - error = 'Error create user account'; + } catch (error) { isCreatingUser = false; - console.log('[ERROR] registerUser', e); + console.log('[ERROR] registerUser', error); notificationController.show({ message: `Error create new user, check console for more detail`, diff --git a/web/src/lib/components/forms/edit-user-form.svelte b/web/src/lib/components/forms/edit-user-form.svelte index 218cd427fb..1116b87cf6 100644 --- a/web/src/lib/components/forms/edit-user-form.svelte +++ b/web/src/lib/components/forms/edit-user-form.svelte @@ -70,8 +70,8 @@ if (status == 200) { dispatch('resetPasswordSuccess'); } - } catch (e) { - console.error('Error reseting user password', e); + } catch (error) { + console.error('Error reseting user password', error); notificationController.show({ message: 'Error reseting user password, check console for more details', type: NotificationType.Error, diff --git a/web/src/lib/components/forms/library-import-paths-form.svelte b/web/src/lib/components/forms/library-import-paths-form.svelte index 3d31573499..8659cdcd05 100644 --- a/web/src/lib/components/forms/library-import-paths-form.svelte +++ b/web/src/lib/components/forms/library-import-paths-form.svelte @@ -110,7 +110,7 @@ /> {/if} -{#if editImportPath != null} +{#if editImportPath != undefined} <LibraryImportPathForm title="Edit Import Path" submitText="Save" diff --git a/web/src/lib/components/forms/library-scan-settings-form.svelte b/web/src/lib/components/forms/library-scan-settings-form.svelte index 1a07839a9d..dcff0bb0fb 100644 --- a/web/src/lib/components/forms/library-scan-settings-form.svelte +++ b/web/src/lib/components/forms/library-scan-settings-form.svelte @@ -109,7 +109,7 @@ /> {/if} -{#if editExclusionPattern != null} +{#if editExclusionPattern != undefined} <LibraryExclusionPatternForm submitText="Save" canDelete={true} diff --git a/web/src/lib/components/forms/login-form.svelte b/web/src/lib/components/forms/login-form.svelte index e8ad3817bf..eedc3370b8 100644 --- a/web/src/lib/components/forms/login-form.svelte +++ b/web/src/lib/components/forms/login-form.svelte @@ -33,9 +33,9 @@ await oauth.login(window.location); dispatch('success'); return; - } catch (e) { - console.error('Error [login-form] [oauth.callback]', e); - oauthError = (await getServerErrorMessage(e)) || 'Unable to complete OAuth login'; + } catch (error) { + console.error('Error [login-form] [oauth.callback]', error); + oauthError = (await getServerErrorMessage(error)) || 'Unable to complete OAuth login'; oauthLoading = false; } } diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index 96d56fec88..dbab3dc520 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -16,7 +16,8 @@ import { tweened } from 'svelte/motion'; import { mdiChevronDown, mdiChevronLeft, mdiChevronRight, mdiChevronUp, mdiPause, mdiPlay } from '@mdi/js'; - const parseIndex = (s: string | null, max: number | null) => Math.max(Math.min(parseInt(s ?? '') || 0, max ?? 0), 0); + const parseIndex = (s: string | null, max: number | null) => + Math.max(Math.min(Number.parseInt(s ?? '') || 0, max ?? 0), 0); $: memoryIndex = parseIndex($page.url.searchParams.get(QueryParameter.MEMORY_INDEX), $memoryStore?.length - 1); $: assetIndex = parseIndex($page.url.searchParams.get(QueryParameter.ASSET_INDEX), currentMemory?.assets.length - 1); @@ -114,18 +115,19 @@ <div class="flex place-content-center place-items-center gap-2 overflow-hidden"> <CircleIconButton icon={paused ? mdiPlay : mdiPause} forceDark on:click={() => (paused = !paused)} /> - {#each currentMemory.assets as _, i} + {#each currentMemory.assets as _, index} <button class="relative w-full py-2" - on:click={() => goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex}&${QueryParameter.ASSET_INDEX}=${i}`)} + on:click={() => + goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex}&${QueryParameter.ASSET_INDEX}=${index}`)} > <span class="absolute left-0 h-[2px] w-full bg-gray-500" /> {#await resetPromise} - <span class="absolute left-0 h-[2px] bg-white" style:width={`${i < assetIndex ? 100 : 0}%`} /> + <span class="absolute left-0 h-[2px] bg-white" style:width={`${index < assetIndex ? 100 : 0}%`} /> {:then} <span class="absolute left-0 h-[2px] bg-white" - style:width={`${i < assetIndex ? 100 : i > assetIndex ? 0 : $progress * 100}%`} + style:width={`${index < assetIndex ? 100 : index > assetIndex ? 0 : $progress * 100}%`} /> {/await} </button> diff --git a/web/src/lib/components/photos-page/actions/add-to-album.svelte b/web/src/lib/components/photos-page/actions/add-to-album.svelte index 8b4bded170..e89e6376d8 100644 --- a/web/src/lib/components/photos-page/actions/add-to-album.svelte +++ b/web/src/lib/components/photos-page/actions/add-to-album.svelte @@ -26,7 +26,7 @@ const handleAddToNewAlbum = (albumName: string) => { showAlbumPicker = false; - const assetIds = Array.from(getAssets()).map((asset) => asset.id); + const assetIds = [...getAssets()].map((asset) => asset.id); api.albumApi.createAlbum({ createAlbumDto: { albumName, assetIds } }).then((response) => { const { id, albumName } = response.data; @@ -43,7 +43,7 @@ const handleAddToAlbum = async (album: AlbumResponseDto) => { showAlbumPicker = false; - const assetIds = Array.from(getAssets()).map((asset) => asset.id); + const assetIds = [...getAssets()].map((asset) => asset.id); await addAssetsToAlbum(album.id, assetIds); clearSelect(); }; diff --git a/web/src/lib/components/photos-page/actions/archive-action.svelte b/web/src/lib/components/photos-page/actions/archive-action.svelte index fc3739c4e6..731856212e 100644 --- a/web/src/lib/components/photos-page/actions/archive-action.svelte +++ b/web/src/lib/components/photos-page/actions/archive-action.svelte @@ -28,7 +28,7 @@ loading = true; try { - const assets = Array.from(getOwnedAssets()).filter((asset) => asset.isArchived !== isArchived); + const assets = [...getOwnedAssets()].filter((asset) => asset.isArchived !== isArchived); const ids = assets.map(({ id }) => id); if (ids.length > 0) { diff --git a/web/src/lib/components/photos-page/actions/asset-job-actions.svelte b/web/src/lib/components/photos-page/actions/asset-job-actions.svelte index 296197a710..28d683363e 100644 --- a/web/src/lib/components/photos-page/actions/asset-job-actions.svelte +++ b/web/src/lib/components/photos-page/actions/asset-job-actions.svelte @@ -16,11 +16,11 @@ const { clearSelect, getOwnedAssets } = getAssetControlContext(); - $: isAllVideos = Array.from(getOwnedAssets()).every((asset) => asset.type === AssetTypeEnum.Video); + $: isAllVideos = [...getOwnedAssets()].every((asset) => asset.type === AssetTypeEnum.Video); const handleRunJob = async (name: AssetJobName) => { try { - const ids = Array.from(getOwnedAssets()).map(({ id }) => id); + const ids = [...getOwnedAssets()].map(({ id }) => id); await api.assetApi.runAssetJobs({ assetJobsDto: { assetIds: ids, name } }); notificationController.show({ message: api.getAssetJobMessage(name), type: NotificationType.Info }); clearSelect(); diff --git a/web/src/lib/components/photos-page/actions/create-shared-link.svelte b/web/src/lib/components/photos-page/actions/create-shared-link.svelte index 6e700807af..b3e68d3034 100644 --- a/web/src/lib/components/photos-page/actions/create-shared-link.svelte +++ b/web/src/lib/components/photos-page/actions/create-shared-link.svelte @@ -20,7 +20,7 @@ {#if showModal} <CreateSharedLinkModal - assetIds={Array.from(getAssets()).map(({ id }) => id)} + assetIds={[...getAssets()].map(({ id }) => id)} on:close={() => (showModal = false)} on:escape={escape} /> diff --git a/web/src/lib/components/photos-page/actions/delete-assets.svelte b/web/src/lib/components/photos-page/actions/delete-assets.svelte index ac44514498..667de9682a 100644 --- a/web/src/lib/components/photos-page/actions/delete-assets.svelte +++ b/web/src/lib/components/photos-page/actions/delete-assets.svelte @@ -32,9 +32,7 @@ const handleDelete = async () => { loading = true; - const ids = Array.from(getOwnedAssets()) - .filter((a) => !a.isExternal) - .map((a) => a.id); + const ids = [...getOwnedAssets()].filter((a) => !a.isExternal).map((a) => a.id); await deleteAssets(force, onAssetDelete, ids); clearSelect(); isShowConfirmation = false; diff --git a/web/src/lib/components/photos-page/actions/download-action.svelte b/web/src/lib/components/photos-page/actions/download-action.svelte index f4e7f685ee..3619db950e 100644 --- a/web/src/lib/components/photos-page/actions/download-action.svelte +++ b/web/src/lib/components/photos-page/actions/download-action.svelte @@ -11,7 +11,7 @@ const { getAssets, clearSelect } = getAssetControlContext(); const handleDownloadFiles = async () => { - const assets = Array.from(getAssets()); + const assets = [...getAssets()]; if (assets.length === 1) { clearSelect(); await downloadFile(assets[0]); diff --git a/web/src/lib/components/photos-page/actions/favorite-action.svelte b/web/src/lib/components/photos-page/actions/favorite-action.svelte index 8ca73958a3..2a70a0f476 100644 --- a/web/src/lib/components/photos-page/actions/favorite-action.svelte +++ b/web/src/lib/components/photos-page/actions/favorite-action.svelte @@ -28,7 +28,7 @@ loading = true; try { - const assets = Array.from(getOwnedAssets()).filter((asset) => asset.isFavorite !== isFavorite); + const assets = [...getOwnedAssets()].filter((asset) => asset.isFavorite !== isFavorite); const ids = assets.map(({ id }) => id); diff --git a/web/src/lib/components/photos-page/actions/remove-from-album.svelte b/web/src/lib/components/photos-page/actions/remove-from-album.svelte index cf9c32818e..48b33719e5 100644 --- a/web/src/lib/components/photos-page/actions/remove-from-album.svelte +++ b/web/src/lib/components/photos-page/actions/remove-from-album.svelte @@ -11,7 +11,7 @@ import { mdiDeleteOutline } from '@mdi/js'; export let album: AlbumResponseDto; - export let onRemove: ((assetIds: string[]) => void) | undefined = undefined; + export let onRemove: ((assetIds: string[]) => void) | undefined; export let menuItem = false; const { getAssets, clearSelect } = getAssetControlContext(); @@ -20,7 +20,7 @@ const removeFromAlbum = async () => { try { - const ids = Array.from(getAssets()).map((a) => a.id); + const ids = [...getAssets()].map((a) => a.id); const { data: results } = await api.albumApi.removeAssetFromAlbum({ id: album.id, bulkIdsDto: { ids }, @@ -38,8 +38,8 @@ }); clearSelect(); - } catch (e) { - console.error('Error [album-viewer] [removeAssetFromAlbum]', e); + } catch (error) { + console.error('Error [album-viewer] [removeAssetFromAlbum]', error); notificationController.show({ type: NotificationType.Error, message: 'Error removing assets from album, check console for more details', diff --git a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte index f37d021c9a..1389cb76d5 100644 --- a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte +++ b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte @@ -18,7 +18,7 @@ const { data: results } = await api.sharedLinkApi.removeSharedLinkAssets({ id: sharedLink.id, assetIdsDto: { - assetIds: Array.from(getAssets()).map((asset) => asset.id), + assetIds: [...getAssets()].map((asset) => asset.id), }, key: api.getKey(), }); diff --git a/web/src/lib/components/photos-page/actions/restore-assets.svelte b/web/src/lib/components/photos-page/actions/restore-assets.svelte index 4efbbda532..5121be9ce3 100644 --- a/web/src/lib/components/photos-page/actions/restore-assets.svelte +++ b/web/src/lib/components/photos-page/actions/restore-assets.svelte @@ -11,7 +11,7 @@ import { mdiHistory } from '@mdi/js'; import type { OnRestore } from '$lib/utils/actions'; - export let onRestore: OnRestore | undefined = undefined; + export let onRestore: OnRestore | undefined; const { getAssets, clearSelect } = getAssetControlContext(); @@ -21,7 +21,7 @@ loading = true; try { - const ids = Array.from(getAssets()).map((a) => a.id); + const ids = [...getAssets()].map((a) => a.id); await api.trashApi.restoreAssets({ bulkIdsDto: { ids } }); onRestore?.(ids); @@ -31,8 +31,8 @@ }); clearSelect(); - } catch (e) { - handleError(e, 'Error restoring assets'); + } catch (error) { + handleError(error, 'Error restoring assets'); } finally { loading = false; } diff --git a/web/src/lib/components/photos-page/actions/select-all-assets.svelte b/web/src/lib/components/photos-page/actions/select-all-assets.svelte index 1cd3e0abab..c14ea37882 100644 --- a/web/src/lib/components/photos-page/actions/select-all-assets.svelte +++ b/web/src/lib/components/photos-page/actions/select-all-assets.svelte @@ -28,8 +28,8 @@ } selecting = false; - } catch (e) { - handleError(e, 'Error selecting all assets'); + } catch (error) { + handleError(error, 'Error selecting all assets'); } }; </script> diff --git a/web/src/lib/components/photos-page/actions/stack-action.svelte b/web/src/lib/components/photos-page/actions/stack-action.svelte index ef50f28c89..ceaaec8cb4 100644 --- a/web/src/lib/components/photos-page/actions/stack-action.svelte +++ b/web/src/lib/components/photos-page/actions/stack-action.svelte @@ -9,13 +9,13 @@ import { handleError } from '$lib/utils/handle-error'; import type { OnStack } from '$lib/utils/actions'; - export let onStack: OnStack | undefined = undefined; + export let onStack: OnStack | undefined; const { clearSelect, getOwnedAssets } = getAssetControlContext(); const handleStack = async () => { try { - const assets = Array.from(getOwnedAssets()); + const assets = [...getOwnedAssets()]; const parent = assets.at(0); if (parent == undefined) { @@ -33,7 +33,7 @@ for (const asset of children) { asset.stackParentId = parent?.id; // Add grand-children's count to new parent - childrenCount += asset.stackCount == null ? 1 : asset.stackCount + 1; + childrenCount += asset.stackCount == undefined ? 1 : asset.stackCount + 1; // Reset children stack info asset.stackCount = null; asset.stack = []; diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index 07e759fcd7..cf560dbbec 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -48,13 +48,16 @@ $: geometry = (() => { const geometry = []; for (let group of assetsGroupByDate) { - const justifiedLayoutResult = justifiedLayout(group.map(getAssetRatio), { - boxSpacing: 2, - containerWidth: Math.floor(viewport.width), - containerPadding: 0, - targetRowHeightTolerance: 0.15, - targetRowHeight: 235, - }); + const justifiedLayoutResult = justifiedLayout( + group.map((assetGroup) => getAssetRatio(assetGroup)), + { + boxSpacing: 2, + containerWidth: Math.floor(viewport.width), + containerPadding: 0, + targetRowHeightTolerance: 0.15, + targetRowHeight: 235, + }, + ); geometry.push({ ...justifiedLayoutResult, containerWidth: calculateWidth(justifiedLayoutResult.boxes), diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 53bc5440d4..059ce61ed3 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -44,9 +44,7 @@ $: timelineY = element?.scrollTop || 0; $: isEmpty = $assetStore.initialized && $assetStore.buckets.length === 0; - $: idsSelectedAssets = Array.from($selectedAssets) - .filter((a) => !a.isExternal) - .map((a) => a.id); + $: idsSelectedAssets = [...$selectedAssets].filter((a) => !a.isExternal).map((a) => a.id); const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); const dispatch = createEventDispatcher<{ select: AssetResponseDto; escape: void }>(); @@ -86,20 +84,23 @@ if (!$showAssetViewer) { switch (key) { - case 'Escape': + case 'Escape': { dispatch('escape'); return; - case '?': + } + case '?': { if (event.shiftKey) { event.preventDefault(); showShortcuts = !showShortcuts; } return; - case '/': + } + case '/': { event.preventDefault(); goto(AppRoute.EXPLORE); return; - case 'Delete': + } + case 'Delete': { if ($isMultiSelectState) { let force = false; if (shiftKey || !isTrashEnabled) { @@ -113,6 +114,7 @@ trashOrDelete(force); } return; + } } } }; @@ -124,8 +126,8 @@ }; function intersectedHandler(event: CustomEvent) { - const el = event.detail.container as HTMLElement; - const target = el.firstChild as HTMLElement; + const element_ = event.detail.container as HTMLElement; + const target = element_.firstChild as HTMLElement; if (target) { const bucketDate = target.id.split('_')[1]; assetStore.loadBucket(bucketDate, event.detail.position); @@ -160,24 +162,27 @@ switch (action) { case removeAction: case AssetAction.TRASH: - case AssetAction.DELETE: + case AssetAction.DELETE: { // find the next asset to show or close the viewer (await handleNext()) || (await handlePrevious()) || handleClose(); // delete after find the next one assetStore.removeAsset(asset.id); break; + } case AssetAction.ARCHIVE: case AssetAction.UNARCHIVE: case AssetAction.FAVORITE: - case AssetAction.UNFAVORITE: + case AssetAction.UNFAVORITE: { assetStore.updateAsset(asset); break; + } - case AssetAction.ADD: + case AssetAction.ADD: { assetStore.addAsset(asset); break; + } } }; @@ -392,7 +397,7 @@ <div class="mt-8 animate-pulse"> <div class="mb-2 h-4 w-24 rounded-full bg-immich-primary/20 dark:bg-immich-dark-primary/20" /> <div class="flex w-[120%] flex-wrap"> - {#each Array(100) as _} + {#each Array.from({ length: 100 }) as _} <div class="m-[1px] h-[10em] w-[16em] bg-immich-primary/20 dark:bg-immich-dark-primary/20" /> {/each} </div> diff --git a/web/src/lib/components/photos-page/asset-select-control-bar.svelte b/web/src/lib/components/photos-page/asset-select-control-bar.svelte index cb9def8ab5..8410f34aa9 100644 --- a/web/src/lib/components/photos-page/asset-select-control-bar.svelte +++ b/web/src/lib/components/photos-page/asset-select-control-bar.svelte @@ -25,7 +25,7 @@ setContext({ getAssets: () => assets, getOwnedAssets: () => - ownerId !== undefined ? new Set(Array.from(assets).filter((asset) => asset.ownerId === ownerId)) : assets, + ownerId === undefined ? assets : new Set([...assets].filter((asset) => asset.ownerId === ownerId)), clearSelect, }); </script> diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte index 5ddab9eaa3..be48dced22 100644 --- a/web/src/lib/components/photos-page/memory-lane.svelte +++ b/web/src/lib/components/photos-page/memory-lane.svelte @@ -69,10 +69,10 @@ {/if} <div class="inline-block" bind:offsetWidth={innerWidth}> - {#each $memoryStore as memory, i (memory.title)} + {#each $memoryStore as memory, index (memory.title)} <button class="memory-card relative mr-8 inline-block aspect-video h-[215px] rounded-xl" - on:click={() => goto(`${AppRoute.MEMORY}?${QueryParameter.MEMORY_INDEX}=${i}`)} + on:click={() => goto(`${AppRoute.MEMORY}?${QueryParameter.MEMORY_INDEX}=${index}`)} > <img class="h-full w-full rounded-xl object-cover" diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte index b966144c21..045a58ca79 100644 --- a/web/src/lib/components/share-page/individual-shared-viewer.svelte +++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte @@ -38,11 +38,9 @@ const handleUploadAssets = async (files: File[] = []) => { try { let results: (string | undefined)[] = []; - if (!files || files.length === 0 || !Array.isArray(files)) { - results = await openFileUploadDialog(undefined); - } else { - results = await fileUploadHandler(files, undefined); - } + results = await (!files || files.length === 0 || !Array.isArray(files) + ? openFileUploadDialog() + : fileUploadHandler(files)); const { data } = await api.sharedLinkApi.addSharedLinkAssets({ id: sharedLink.id, assetIdsDto: { @@ -57,8 +55,8 @@ message: `Added ${added} assets`, type: NotificationType.Info, }); - } catch (e) { - await handleError(e, 'Unable to add assets to shared link'); + } catch (error) { + await handleError(error, 'Unable to add assets to shared link'); } }; diff --git a/web/src/lib/components/shared-components/album-selection-modal.svelte b/web/src/lib/components/shared-components/album-selection-modal.svelte index 77dd753c25..188bdd5c06 100644 --- a/web/src/lib/components/shared-components/album-selection-modal.svelte +++ b/web/src/lib/components/shared-components/album-selection-modal.svelte @@ -30,13 +30,12 @@ }); $: { - if (search.length > 0 && albums.length > 0) { - filteredAlbums = albums.filter((album) => { - return album.albumName.toLowerCase().includes(search.toLowerCase()); - }); - } else { - filteredAlbums = albums; - } + filteredAlbums = + search.length > 0 && albums.length > 0 + ? albums.filter((album) => { + return album.albumName.toLowerCase().includes(search.toLowerCase()); + }) + : albums; } const handleSelect = (album: AlbumResponseDto) => { diff --git a/web/src/lib/components/shared-components/base-modal.svelte b/web/src/lib/components/shared-components/base-modal.svelte index a1aae869f7..09d22646ca 100644 --- a/web/src/lib/components/shared-components/base-modal.svelte +++ b/web/src/lib/components/shared-components/base-modal.svelte @@ -18,15 +18,15 @@ if (browser) { const scrollTop = document.documentElement.scrollTop; const scrollLeft = document.documentElement.scrollLeft; - window.onscroll = function () { + window.addEventListener('scroll', function () { window.scrollTo(scrollLeft, scrollTop); - }; + }); } }); onDestroy(() => { if (browser) { - window.onscroll = null; + window.addEventListener('scroll', () => {}); } }); </script> diff --git a/web/src/lib/components/shared-components/change-location.svelte b/web/src/lib/components/shared-components/change-location.svelte index 9af69232b8..2186986c56 100644 --- a/web/src/lib/components/shared-components/change-location.svelte +++ b/web/src/lib/components/shared-components/change-location.svelte @@ -29,10 +29,10 @@ }; const handleConfirm = () => { - if (!point) { - dispatch('cancel'); - } else { + if (point) { dispatch('confirm', point); + } else { + dispatch('cancel'); } }; </script> diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte index e446c1ff6f..5d85e8bc85 100644 --- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte +++ b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte @@ -66,7 +66,7 @@ const handleCreateSharedLink = async () => { const expirationTime = getExpirationTimeInMillisecond(); - const currentTime = new Date().getTime(); + const currentTime = Date.now(); const expirationDate = expirationTime ? new Date(currentTime + expirationTime).toISOString() : undefined; try { @@ -84,8 +84,8 @@ }, }); sharedLink = makeSharedLinkUrl($serverConfig.externalDomain, data.key); - } catch (e) { - handleError(e, 'Failed to create shared link'); + } catch (error) { + handleError(error, 'Failed to create shared link'); } }; @@ -99,20 +99,27 @@ const getExpirationTimeInMillisecond = () => { switch (expirationTime) { - case '30 minutes': + case '30 minutes': { return 30 * 60 * 1000; - case '1 hour': + } + case '1 hour': { return 60 * 60 * 1000; - case '6 hours': + } + case '6 hours': { return 6 * 60 * 60 * 1000; - case '1 day': + } + case '1 day': { return 24 * 60 * 60 * 1000; - case '7 days': + } + case '7 days': { return 7 * 24 * 60 * 60 * 1000; - case '30 days': + } + case '30 days': { return 30 * 24 * 60 * 60 * 1000; - default: + } + default: { return 0; + } } }; @@ -123,7 +130,7 @@ try { const expirationTime = getExpirationTimeInMillisecond(); - const currentTime = new Date().getTime(); + const currentTime = Date.now(); const expirationDate: string | null = expirationTime ? new Date(currentTime + expirationTime).toISOString() : null; @@ -146,8 +153,8 @@ }); dispatch('close'); - } catch (e) { - handleError(e, 'Failed to edit shared link'); + } catch (error) { + handleError(error, 'Failed to edit shared link'); } }; </script> diff --git a/web/src/lib/components/shared-components/empty-placeholder.svelte b/web/src/lib/components/shared-components/empty-placeholder.svelte index 1ba673181c..fc234aa4da 100644 --- a/web/src/lib/components/shared-components/empty-placeholder.svelte +++ b/web/src/lib/components/shared-components/empty-placeholder.svelte @@ -7,7 +7,7 @@ export let fullWidth = false; export let src = empty1Url; - const noop = () => undefined; + const noop = () => {}; $: handler = actionHandler || noop; $: width = fullWidth ? 'w-full' : 'w-[50%]'; diff --git a/web/src/lib/components/shared-components/gallery-viewer/asset-selection-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/asset-selection-viewer.svelte index 36347495a8..1b2f80da80 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/asset-selection-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/asset-selection-viewer.svelte @@ -17,14 +17,14 @@ const selectAssetHandler = (event: CustomEvent) => { const { asset }: { asset: AssetResponseDto } = event.detail; - let temp = new Set(selectedAssets); + let temporary = new Set(selectedAssets); if (selectedAssets.has(asset)) { - temp.delete(asset); + temporary.delete(asset); } else { - temp.add(asset); + temporary.add(asset); } - selectedAssets = temp; + selectedAssets = temporary; dispatch('select', { asset, selectedAssets }); }; </script> diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index 449188582f..669d3ec81f 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -35,15 +35,15 @@ const selectAssetHandler = (event: CustomEvent) => { const { asset }: { asset: AssetResponseDto } = event.detail; - let temp = new Set(selectedAssets); + let temporary = new Set(selectedAssets); if (selectedAssets.has(asset)) { - temp.delete(asset); + temporary.delete(asset); } else { - temp.add(asset); + temporary.add(asset); } - selectedAssets = temp; + selectedAssets = temporary; }; const navigateAssetForward = () => { @@ -53,8 +53,8 @@ selectedAsset = assets[currentViewAssetIndex]; pushState(selectedAsset.id); } - } catch (e) { - handleError(e, 'Cannot navigate to the next asset'); + } catch (error) { + handleError(error, 'Cannot navigate to the next asset'); } }; @@ -65,8 +65,8 @@ selectedAsset = assets[currentViewAssetIndex]; pushState(selectedAsset.id); } - } catch (e) { - handleError(e, 'Cannot navigate to previous asset'); + } catch (error) { + handleError(error, 'Cannot navigate to previous asset'); } }; diff --git a/web/src/lib/components/shared-components/map/map.svelte b/web/src/lib/components/shared-components/map/map.svelte index ebbd6797a9..a74a51dc09 100644 --- a/web/src/lib/components/shared-components/map/map.svelte +++ b/web/src/lib/components/shared-components/map/map.svelte @@ -35,6 +35,7 @@ let map: maplibregl.Map; let marker: maplibregl.Marker | null = null; + // eslint-disable-next-line unicorn/prefer-top-level-await $: style = (async () => { const { data } = await api.systemConfigApi.getMapStyle({ theme: $mapSettings.allowDarkMode ? $colorTheme.value : Theme.LIGHT, @@ -60,7 +61,7 @@ } const mapSource = map?.getSource('geojson') as GeoJSONSource; - mapSource.getClusterLeaves(clusterId, 10000, 0, (error, leaves) => { + mapSource.getClusterLeaves(clusterId, 10_000, 0, (error, leaves) => { if (error) { return; } diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index 3a56148dd6..8a83c0cb32 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -30,10 +30,10 @@ const logOut = async () => { resetSavedUser(); const { data } = await api.authenticationApi.logout(); - if (!data.redirectUri.startsWith('/')) { - window.location.href = data.redirectUri; - } else { + if (data.redirectUri.startsWith('/')) { goto(data.redirectUri); + } else { + window.location.href = data.redirectUri; } }; </script> diff --git a/web/src/lib/components/shared-components/notification/notification-card.svelte b/web/src/lib/components/shared-components/notification/notification-card.svelte index 558c80769b..a031b76810 100644 --- a/web/src/lib/components/shared-components/notification/notification-card.svelte +++ b/web/src/lib/components/shared-components/notification/notification-card.svelte @@ -59,7 +59,7 @@ } }; - let removeNotificationTimeout: NodeJS.Timeout | undefined = undefined; + let removeNotificationTimeout: NodeJS.Timeout | undefined; onMount(() => { removeNotificationTimeout = setTimeout(discard, notificationInfo.timeout); diff --git a/web/src/lib/components/shared-components/notification/notification.ts b/web/src/lib/components/shared-components/notification/notification.ts index 815ac89e28..f1a2140461 100644 --- a/web/src/lib/components/shared-components/notification/notification.ts +++ b/web/src/lib/components/shared-components/notification/notification.ts @@ -7,7 +7,7 @@ export enum NotificationType { } export class ImmichNotification { - id = new Date().getTime() + Math.random(); + id = Date.now() + Math.random(); type!: NotificationType; message!: string; action!: NotificationAction; diff --git a/web/src/lib/components/shared-components/portal/portal.svelte b/web/src/lib/components/shared-components/portal/portal.svelte index 431950fd3b..c3dee4212c 100644 --- a/web/src/lib/components/shared-components/portal/portal.svelte +++ b/web/src/lib/components/shared-components/portal/portal.svelte @@ -4,21 +4,21 @@ /** * Usage: <div use:portal={'css selector'}> or <div use:portal={document.body}> */ - export function portal(el: HTMLElement, target: HTMLElement | string = 'body') { - let targetEl; + export function portal(element: HTMLElement, target: HTMLElement | string = 'body') { + let targetElement; async function update(newTarget: HTMLElement | string) { target = newTarget; if (typeof target === 'string') { - targetEl = document.querySelector(target); - if (targetEl === null) { + targetElement = document.querySelector(target); + if (targetElement === null) { await tick(); - targetEl = document.querySelector(target); + targetElement = document.querySelector(target); } - if (targetEl === null) { + if (targetElement === null) { throw new Error(`No element found matching css selector: "${target}"`); } } else if (target instanceof HTMLElement) { - targetEl = target; + targetElement = target; } else { throw new TypeError( `Unknown portal target type: ${ @@ -26,13 +26,13 @@ }. Allowed types: string (CSS selector) or HTMLElement.`, ); } - targetEl.appendChild(el); - el.hidden = false; + targetElement.append(element); + element.hidden = false; } function destroy() { - if (el.parentNode) { - el.parentNode.removeChild(el); + if (element.parentNode) { + element.remove(); } } diff --git a/web/src/lib/components/shared-components/profile-image-cropper.svelte b/web/src/lib/components/shared-components/profile-image-cropper.svelte index 0bac6ee7f1..6f71898311 100644 --- a/web/src/lib/components/shared-components/profile-image-cropper.svelte +++ b/web/src/lib/components/shared-components/profile-image-cropper.svelte @@ -26,18 +26,18 @@ const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) { + const context = canvas.getContext('2d'); + if (!context) { throw new Error('Could not get canvas context.'); } - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + context.drawImage(img, 0, 0); + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const data = imageData?.data; if (!data) { throw new Error('Could not get image data.'); } - for (let i = 0; i < data.length; i += 4) { - if (data[i + 3] < 255) { + for (let index = 0; index < data.length; index += 4) { + if (data[index + 3] < 255) { return true; } } @@ -62,8 +62,8 @@ message: 'Profile picture set.', timeout: 3000, }); - } catch (err) { - handleError(err, 'Error setting profile picture.'); + } catch (error) { + handleError(error, 'Error setting profile picture.'); } dispatch('close'); }; diff --git a/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte b/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte index ad9e205779..bef58fe250 100644 --- a/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte +++ b/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte @@ -34,7 +34,7 @@ const calculateSegments = (buckets: AssetBucket[]) => { let height = 0; - let prev: Segment; + let previous: Segment; return buckets.map((bucket) => { const segment = new Segment(); segment.count = bucket.assets.length; @@ -42,13 +42,13 @@ segment.timeGroup = bucket.bucketDate; segment.date = fromLocalDateTime(segment.timeGroup); - if (prev?.date.year !== segment.date.year && height > MIN_YEAR_LABEL_DISTANCE) { - prev.hasLabel = true; + if (previous?.date.year !== segment.date.year && height > MIN_YEAR_LABEL_DISTANCE) { + previous.hasLabel = true; height = 0; } height += segment.height; - prev = segment; + previous = segment; return segment; }); }; diff --git a/web/src/lib/components/shared-components/search-bar/search-bar.svelte b/web/src/lib/components/shared-components/search-bar/search-bar.svelte index 1d26a368fc..bc0b370cb5 100644 --- a/web/src/lib/components/shared-components/search-bar/search-bar.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-bar.svelte @@ -26,14 +26,14 @@ $savedSearchTerms = $savedSearchTerms.filter((item) => item !== value); saveSearchTerm(value); - const params = new URLSearchParams({ + const parameters = new URLSearchParams({ q: searchValue, smart: smartSearch, }); showBigSearchBar = false; $isSearchEnabled = false; - goto(`${AppRoute.SEARCH}?${params}`, { invalidateAll: true }); + goto(`${AppRoute.SEARCH}?${parameters}`, { invalidateAll: true }); } const clearSearchTerm = (searchTerm: string) => { @@ -140,7 +140,7 @@ </div> {/if} - {#each $savedSearchTerms as savedSearchTerm, i (i)} + {#each $savedSearchTerms as savedSearchTerm, index (index)} <div class="flex w-full items-center justify-between text-sm text-black hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-500/10" > diff --git a/web/src/lib/components/shared-components/version-announcement-box.svelte b/web/src/lib/components/shared-components/version-announcement-box.svelte index b2c535cd65..d0415f54ba 100644 --- a/web/src/lib/components/shared-components/version-announcement-box.svelte +++ b/web/src/lib/components/shared-components/version-announcement-box.svelte @@ -26,8 +26,8 @@ } showModal = true; - } catch (err) { - console.error('Error [VersionAnnouncementBox]:', err); + } catch (error) { + console.error('Error [VersionAnnouncementBox]:', error); } }; </script> diff --git a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte index 43eafd6b5e..df24ee7d53 100644 --- a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte +++ b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte @@ -55,7 +55,7 @@ }; const isExpired = (expiresAt: string) => { - const now = new Date().getTime(); + const now = Date.now(); const expiration = new Date(expiresAt).getTime(); return now > expiration; diff --git a/web/src/lib/components/user-settings-page/device-card.svelte b/web/src/lib/components/user-settings-page/device-card.svelte index eec3a9e2cd..cb6f39e798 100644 --- a/web/src/lib/components/user-settings-page/device-card.svelte +++ b/web/src/lib/components/user-settings-page/device-card.svelte @@ -34,9 +34,9 @@ <Icon path={mdiAndroid} size="40" /> {:else if device.deviceOS === 'iOS' || device.deviceOS === 'Mac OS'} <Icon path={mdiApple} size="40" /> - {:else if device.deviceOS.indexOf('Safari') !== -1} + {:else if device.deviceOS.includes('Safari')} <Icon path={mdiAppleSafari} size="40" /> - {:else if device.deviceOS.indexOf('Windows') !== -1} + {:else if device.deviceOS.includes('Windows')} <Icon path={mdiMicrosoftWindows} size="40" /> {:else if device.deviceOS === 'Linux'} <Icon path={mdiLinux} size="40" /> diff --git a/web/src/lib/components/user-settings-page/device-list.svelte b/web/src/lib/components/user-settings-page/device-list.svelte index 7cef9985a9..b0f1be38c0 100644 --- a/web/src/lib/components/user-settings-page/device-list.svelte +++ b/web/src/lib/components/user-settings-page/device-list.svelte @@ -73,9 +73,9 @@ {#if otherDevices.length > 0} <div class="mb-6"> <h3 class="mb-2 text-xs font-medium text-immich-primary dark:text-immich-dark-primary">OTHER DEVICES</h3> - {#each otherDevices as device, i} + {#each otherDevices as device, index} <DeviceCard {device} on:delete={() => (deleteDevice = device)} /> - {#if i !== otherDevices.length - 1} + {#if index !== otherDevices.length - 1} <hr class="my-3" /> {/if} {/each} diff --git a/web/src/lib/components/user-settings-page/library-list.svelte b/web/src/lib/components/user-settings-page/library-list.svelte index fd287fde16..ead581ddaf 100644 --- a/web/src/lib/components/user-settings-page/library-list.svelte +++ b/web/src/lib/components/user-settings-page/library-list.svelte @@ -56,8 +56,8 @@ updateLibraryIndex = null; showContextMenu = false; - for (let i = 0; i < dropdownOpen.length; i++) { - dropdownOpen[i] = false; + for (let index = 0; index < dropdownOpen.length; index++) { + dropdownOpen[index] = false; } }; @@ -87,9 +87,9 @@ dropdownOpen.length = libraries.length; - for (let i = 0; i < libraries.length; i++) { - await refreshStats(i); - dropdownOpen[i] = false; + for (let index = 0; index < libraries.length; index++) { + await refreshStats(index); + dropdownOpen[index] = false; } } diff --git a/web/src/lib/components/user-settings-page/partner-selection-modal.svelte b/web/src/lib/components/user-settings-page/partner-selection-modal.svelte index 2d9600646f..09beeb78c5 100644 --- a/web/src/lib/components/user-settings-page/partner-selection-modal.svelte +++ b/web/src/lib/components/user-settings-page/partner-selection-modal.svelte @@ -22,16 +22,14 @@ // exclude partners from the list of users available for selection const { data: partners } = await api.partnerApi.getPartners({ direction: 'shared-by' }); - const partnerIds = partners.map((partner) => partner.id); - availableUsers = users.filter((user) => !partnerIds.includes(user.id)); + const partnerIds = new Set(partners.map((partner) => partner.id)); + availableUsers = users.filter((user) => !partnerIds.has(user.id)); }); const selectUser = (user: UserResponseDto) => { - if (selectedUsers.includes(user)) { - selectedUsers = selectedUsers.filter((selectedUser) => selectedUser.id !== user.id); - } else { - selectedUsers = [...selectedUsers, user]; - } + selectedUsers = selectedUsers.includes(user) + ? selectedUsers.filter((selectedUser) => selectedUser.id !== user.id) + : [...selectedUsers, user]; }; </script> diff --git a/web/src/lib/components/user-settings-page/user-api-key-list.svelte b/web/src/lib/components/user-settings-page/user-api-key-list.svelte index 2555554119..90a776cc09 100644 --- a/web/src/lib/components/user-settings-page/user-api-key-list.svelte +++ b/web/src/lib/components/user-settings-page/user-api-key-list.svelte @@ -129,11 +129,13 @@ </tr> </thead> <tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray"> - {#each keys as key, i} + {#each keys as key, index} {#key key.id} <tr class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${ - i % 2 == 0 ? 'bg-immich-gray dark:bg-immich-dark-gray/75' : 'bg-immich-bg dark:bg-immich-dark-gray/50' + index % 2 == 0 + ? 'bg-immich-gray dark:bg-immich-dark-gray/75' + : 'bg-immich-bg dark:bg-immich-dark-gray/50' }`} > <td class="w-1/3 text-ellipsis px-4 text-sm">{key.name}</td> diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index 6d4fe6db4f..00c9195608 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -90,7 +90,7 @@ export class AssetStore { setTimeout(() => { this.pendingChanges.push(...changes); this.processPendingChanges(); - }, 1_000); + }, 1000); } connect() { @@ -124,19 +124,22 @@ export class AssetStore { processPendingChanges = throttle(() => { for (const { type, value } of this.pendingChanges) { switch (type) { - case 'add': + case 'add': { this.addAsset(value); break; + } - case 'trash': + case 'trash': { if (!this.options.isTrashed) { this.removeAsset(value); } break; + } - case 'delete': + case 'delete': { this.removeAsset(value); break; + } } } @@ -174,7 +177,7 @@ export class AssetStore { }; }); - this.timelineHeight = this.buckets.reduce((acc, b) => acc + b.bucketHeight, 0); + this.timelineHeight = this.buckets.reduce((accumulator, b) => accumulator + b.bucketHeight, 0); this.emit(false); @@ -199,7 +202,7 @@ export class AssetStore { bucket.position = position; - if (bucket.assets.length !== 0) { + if (bucket.assets.length > 0) { this.emit(false); return; } @@ -324,7 +327,9 @@ export class AssetStore { } async getRandomAsset(): Promise<AssetResponseDto | null> { - let index = Math.floor(Math.random() * this.buckets.reduce((acc, bucket) => acc + bucket.bucketCount, 0)); + let index = Math.floor( + Math.random() * this.buckets.reduce((accumulator, bucket) => accumulator + bucket.bucketCount, 0), + ); for (const bucket of this.buckets) { if (index < bucket.bucketCount) { await this.loadBucket(bucket.bucketDate, BucketPosition.Unknown); @@ -356,17 +361,17 @@ export class AssetStore { } removeAsset(id: string) { - for (let i = 0; i < this.buckets.length; i++) { - const bucket = this.buckets[i]; - for (let j = 0; j < bucket.assets.length; j++) { - const asset = bucket.assets[j]; + for (let index = 0; index < this.buckets.length; index++) { + const bucket = this.buckets[index]; + for (let index_ = 0; index_ < bucket.assets.length; index_++) { + const asset = bucket.assets[index_]; if (asset.id !== id) { continue; } - bucket.assets.splice(j, 1); + bucket.assets.splice(index_, 1); if (bucket.assets.length === 0) { - this.buckets.splice(i, 1); + this.buckets.splice(index, 1); } this.emit(true); @@ -422,14 +427,14 @@ export class AssetStore { this.assets = this.buckets.flatMap(({ assets }) => assets); const assetToBucket: Record<string, AssetLookup> = {}; - for (let i = 0; i < this.buckets.length; i++) { - const bucket = this.buckets[i]; - if (bucket.assets.length !== 0) { + for (let index = 0; index < this.buckets.length; index++) { + const bucket = this.buckets[index]; + if (bucket.assets.length > 0) { bucket.bucketCount = bucket.assets.length; } - for (let j = 0; j < bucket.assets.length; j++) { - const asset = bucket.assets[j]; - assetToBucket[asset.id] = { bucket, bucketIndex: i, assetIndex: j }; + for (let index_ = 0; index_ < bucket.assets.length; index_++) { + const asset = bucket.assets[index_]; + assetToBucket[asset.id] = { bucket, bucketIndex: index, assetIndex: index_ }; } } this.assetToBucket = assetToBucket; diff --git a/web/src/lib/stores/download.ts b/web/src/lib/stores/download.ts index 7dd13b18cf..a37b351b44 100644 --- a/web/src/lib/stores/download.ts +++ b/web/src/lib/stores/download.ts @@ -10,7 +10,7 @@ export interface DownloadProgress { export const downloadAssets = writable<Record<string, DownloadProgress>>({}); export const isDownloading = derived(downloadAssets, ($downloadAssets) => { - if (Object.keys($downloadAssets).length == 0) { + if (Object.keys($downloadAssets).length === 0) { return false; } diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index 6ce3a791d2..09154f5b12 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -15,10 +15,8 @@ export const handleToggleTheme = () => { }; const initTheme = (): ThemeSetting => { - if (browser) { - if (!window.matchMedia('(prefers-color-scheme: dark)').matches) { - return { value: Theme.LIGHT, system: false }; - } + if (browser && !window.matchMedia('(prefers-color-scheme: dark)').matches) { + return { value: Theme.LIGHT, system: false }; } return { value: Theme.DARK, system: false }; }; @@ -30,13 +28,9 @@ export const colorTheme = persisted<ThemeSetting>('color-theme', initialTheme, { serializer: { parse: (text: string): ThemeSetting => { const parsedText: ThemeSetting = JSON.parse(text); - if (Object.values(Theme).includes(parsedText.value)) { - return parsedText; - } else { - return initTheme(); - } + return Object.values(Theme).includes(parsedText.value) ? parsedText : initTheme(); }, - stringify: (obj) => JSON.stringify(obj), + stringify: (object) => JSON.stringify(object), }, }); @@ -44,7 +38,7 @@ export const colorTheme = persisted<ThemeSetting>('color-theme', initialTheme, { export const locale = persisted<string | undefined>('locale', undefined, { serializer: { parse: (text) => text, - stringify: (obj) => obj ?? '', + stringify: (object) => object ?? '', }, }); diff --git a/web/src/lib/stores/upload.ts b/web/src/lib/stores/upload.ts index 37733fd9c5..09031a9169 100644 --- a/web/src/lib/stores/upload.ts +++ b/web/src/lib/stores/upload.ts @@ -65,8 +65,8 @@ function createUploadStore() { }); }; - const updateAsset = (id: string, partialObj: Partial<UploadAsset>) => { - updateAssetMap(id, (v) => ({ ...v, ...partialObj })); + const updateAsset = (id: string, partialObject: Partial<UploadAsset>) => { + updateAssetMap(id, (v) => ({ ...v, ...partialObject })); }; const removeUploadAsset = (id: string) => { diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index cc8ebdad0c..2062eddb34 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -55,8 +55,8 @@ export const openWebsocketConnection = async () => { .on('on_config_update', () => loadConfig()) .on('on_new_release', (data) => websocketStore.onRelease.set(data)) .on('error', (e) => console.log('Websocket Error', e)); - } catch (e) { - console.log('Cannot connect to websocket ', e); + } catch (error) { + console.log('Cannot connect to websocket', error); } }; diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts index 3148143c0e..28b01252c8 100644 --- a/web/src/lib/utils/actions.ts +++ b/web/src/lib/utils/actions.ts @@ -19,7 +19,7 @@ export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: message: `${force ? 'Permanently deleted' : 'Trashed'} ${ids.length} assets`, type: NotificationType.Info, }); - } catch (e) { - handleError(e, 'Error deleting assets'); + } catch (error) { + handleError(error, 'Error deleting assets'); } }; diff --git a/web/src/lib/utils/asset-utils.spec.ts b/web/src/lib/utils/asset-utils.spec.ts index 5b68fd6995..8f753ca625 100644 --- a/web/src/lib/utils/asset-utils.spec.ts +++ b/web/src/lib/utils/asset-utils.spec.ts @@ -29,7 +29,7 @@ describe('get file extension from filename', () => { describe('get asset filename', () => { it('returns the filename including file extension', () => { - [ + for (const { asset, result } of [ { asset: { originalFileName: 'filename', @@ -51,8 +51,8 @@ describe('get asset filename', () => { }, result: 'new-filename.txt.jpg', }, - ].forEach(({ asset, result }) => { + ]) { expect(getAssetFilename(asset as AssetResponseDto)).toEqual(result); - }); + } }); }); diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index f41dd790b4..04bbf0b04d 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -36,9 +36,9 @@ export const downloadBlob = (data: Blob, filename: string) => { anchor.href = url; anchor.download = filename; - document.body.appendChild(anchor); + document.body.append(anchor); anchor.click(); - document.body.removeChild(anchor); + anchor.remove(); URL.revokeObjectURL(url); }; @@ -57,14 +57,14 @@ export const downloadArchive = async (fileName: string, options: DownloadInfoDto // TODO: prompt for big download // const total = downloadInfo.totalSize; - for (let i = 0; i < downloadInfo.archives.length; i++) { - const archive = downloadInfo.archives[i]; - const suffix = downloadInfo.archives.length === 1 ? '' : `+${i + 1}`; + for (let index = 0; index < downloadInfo.archives.length; index++) { + const archive = downloadInfo.archives[index]; + const suffix = downloadInfo.archives.length === 1 ? '' : `+${index + 1}`; const archiveName = fileName.replace('.zip', `${suffix}-${DateTime.now().toFormat('yyyy-LL-dd-HH-mm-ss')}.zip`); let downloadKey = `${archiveName} `; if (downloadInfo.archives.length > 1) { - downloadKey = `${archiveName} (${i + 1}/${downloadInfo.archives.length})`; + downloadKey = `${archiveName} (${index + 1}/${downloadInfo.archives.length})`; } const abort = new AbortController(); @@ -81,12 +81,12 @@ export const downloadArchive = async (fileName: string, options: DownloadInfoDto ); downloadBlob(data, archiveName); - } catch (e) { - handleError(e, 'Unable to download files'); + } catch (error) { + handleError(error, 'Unable to download files'); downloadManager.clear(downloadKey); return; } finally { - setTimeout(() => downloadManager.clear(downloadKey), 5_000); + setTimeout(() => downloadManager.clear(downloadKey), 5000); } } }; @@ -140,11 +140,11 @@ export const downloadFile = async (asset: AssetResponseDto) => { }); downloadBlob(data, filename); - } catch (e) { - handleError(e, `Error downloading ${filename}`); + } catch (error) { + handleError(error, `Error downloading ${filename}`); downloadManager.clear(downloadKey); } finally { - setTimeout(() => downloadManager.clear(downloadKey), 5_000); + setTimeout(() => downloadManager.clear(downloadKey), 5000); } } }; @@ -155,7 +155,7 @@ export const downloadFile = async (asset: AssetResponseDto) => { */ export function getFilenameExtension(filename: string): string { const lastIndex = Math.max(0, filename.lastIndexOf('.')); - const startIndex = (lastIndex || Infinity) + 1; + const startIndex = (lastIndex || Number.POSITIVE_INFINITY) + 1; return filename.slice(startIndex).toLowerCase(); } @@ -182,10 +182,8 @@ export function getAssetRatio(asset: AssetResponseDto) { let height = asset.exifInfo?.exifImageHeight || 235; let width = asset.exifInfo?.exifImageWidth || 235; const orientation = Number(asset.exifInfo?.orientation); - if (orientation) { - if (isRotated90CW(orientation) || isRotated270CW(orientation)) { - [width, height] = [height, width]; - } + if (orientation && (isRotated90CW(orientation) || isRotated270CW(orientation))) { + [width, height] = [height, width]; } return { width, height }; } @@ -204,21 +202,22 @@ export function isWebCompatibleImage(asset: AssetResponseDto): boolean { export const getAssetType = (type: AssetTypeEnum) => { switch (type) { - case 'IMAGE': + case 'IMAGE': { return 'Photo'; - case 'VIDEO': + } + case 'VIDEO': { return 'Video'; - default: + } + default: { return 'Asset'; + } } }; export const getSelectedAssets = (assets: Set<AssetResponseDto>, user: UserResponseDto | null): string[] => { - const ids = Array.from(assets) - .filter((a) => !a.isExternal && user && a.ownerId === user.id) - .map((a) => a.id); + const ids = [...assets].filter((a) => !a.isExternal && user && a.ownerId === user.id).map((a) => a.id); - const numberOfIssues = Array.from(assets).filter((a) => a.isExternal || (user && a.ownerId !== user.id)).length; + const numberOfIssues = [...assets].filter((a) => a.isExternal || (user && a.ownerId !== user.id)).length; if (numberOfIssues > 0) { notificationController.show({ message: `Can't change metadata of ${numberOfIssues} asset${numberOfIssues > 1 ? 's' : ''}`, diff --git a/web/src/lib/utils/byte-converter.ts b/web/src/lib/utils/byte-converter.ts index 15d775dc1d..9fc5eb6471 100644 --- a/web/src/lib/utils/byte-converter.ts +++ b/web/src/lib/utils/byte-converter.ts @@ -11,7 +11,7 @@ export function convertToBytes(size: number, unit: string): number { let bytes = 0; if (unit === 'GiB') { - bytes = size * 1073741824; + bytes = size * 1_073_741_824; } return bytes; @@ -30,7 +30,7 @@ export function convertFromBytes(bytes: number, unit: string): number { let size = 0; if (unit === 'GiB') { - size = bytes / 1073741824; + size = bytes / 1_073_741_824; } return size; diff --git a/web/src/lib/utils/byte-units.ts b/web/src/lib/utils/byte-units.ts index 4a41a23bc5..c30fede2c7 100644 --- a/web/src/lib/utils/byte-units.ts +++ b/web/src/lib/utils/byte-units.ts @@ -22,7 +22,7 @@ export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, stri } } - remainder = parseFloat(remainder.toFixed(maxPrecision)); + remainder = Number.parseFloat(remainder.toFixed(maxPrecision)); return [remainder, units[magnitude]]; } diff --git a/web/src/lib/utils/context-menu.ts b/web/src/lib/utils/context-menu.ts index 364beeaf82..e4c26d962f 100644 --- a/web/src/lib/utils/context-menu.ts +++ b/web/src/lib/utils/context-menu.ts @@ -5,12 +5,15 @@ export const getContextMenuPosition = (event: MouseEvent, align: Align = 'middle const box = ((currentTarget || target) as HTMLElement)?.getBoundingClientRect(); if (box) { switch (align) { - case 'middle': + case 'middle': { return { x: box.x + box.width / 2, y: box.y + box.height / 2 }; - case 'top-left': + } + case 'top-left': { return { x: box.x, y: box.y }; - case 'top-right': + } + case 'top-right': { return { x: box.x + box.width, y: box.y }; + } } } diff --git a/web/src/lib/utils/executor-queue.ts b/web/src/lib/utils/executor-queue.ts index 0816befd6d..0744427cfc 100644 --- a/web/src/lib/utils/executor-queue.ts +++ b/web/src/lib/utils/executor-queue.ts @@ -26,7 +26,9 @@ export class ExecutorQueue { const v = concurrency - this.running; if (v > 0) { - [...new Array(this._concurrency)].forEach(() => this.tryRun()); + for (let i = 0; i < v; i++) { + this.tryRun(); + } } } @@ -38,8 +40,8 @@ export class ExecutorQueue { this.running++; const result = task(); resolve(await result); - } catch (e) { - reject(e); + } catch (error) { + reject(error); } finally { this.taskFinished(); } diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index 5e47b156f0..2e039abbd4 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -17,7 +17,7 @@ const getExtensions = async () => { return _extensions; }; -export const openFileUploadDialog = async (albumId: string | undefined = undefined) => { +export const openFileUploadDialog = async (albumId?: string | undefined) => { const extensions = await getExtensions(); return new Promise<(string | undefined)[]>((resolve, reject) => { @@ -27,7 +27,7 @@ export const openFileUploadDialog = async (albumId: string | undefined = undefin fileSelector.type = 'file'; fileSelector.multiple = true; fileSelector.accept = extensions.join(','); - fileSelector.onchange = async (e: Event) => { + fileSelector.addEventListener('change', async (e: Event) => { const target = e.target as HTMLInputElement; if (!target.files) { return; @@ -35,12 +35,12 @@ export const openFileUploadDialog = async (albumId: string | undefined = undefin const files = Array.from(target.files); resolve(fileUploadHandler(files, albumId)); - }; + }); fileSelector.click(); - } catch (e) { - console.log('Error selecting file', e); - reject(e); + } catch (error) { + console.log('Error selecting file', error); + reject(error); } }); }; @@ -50,7 +50,7 @@ export const fileUploadHandler = async (files: File[], albumId: string | undefin const promises = []; for (const file of files) { const name = file.name.toLowerCase(); - if (extensions.some((ext) => name.endsWith(ext))) { + if (extensions.some((extension) => name.endsWith(extension))) { uploadAssetsStore.addNewUploadAsset({ id: getDeviceAssetId(file), file, albumId }); promises.push(uploadExecutionQueue.addTask(() => fileUploader(file, albumId))); } diff --git a/web/src/lib/utils/person.ts b/web/src/lib/utils/person.ts index e26d6c6936..e608a06a0c 100644 --- a/web/src/lib/utils/person.ts +++ b/web/src/lib/utils/person.ts @@ -6,27 +6,21 @@ export const searchNameLocal = ( slice: number, personId?: string, ): PersonResponseDto[] => { - return name.indexOf(' ') >= 0 + return name.includes(' ') ? people .filter((person: PersonResponseDto) => { - if (personId) { - return person.name.toLowerCase().startsWith(name.toLowerCase()) && person.id !== personId; - } else { - return person.name.toLowerCase().startsWith(name.toLowerCase()); - } + return personId + ? person.name.toLowerCase().startsWith(name.toLowerCase()) && person.id !== personId + : person.name.toLowerCase().startsWith(name.toLowerCase()); }) .slice(0, slice) : people .filter((person: PersonResponseDto) => { const nameParts = person.name.split(' '); - if (personId) { - return ( - nameParts.some((splitName) => splitName.toLowerCase().startsWith(name.toLowerCase())) && - person.id !== personId - ); - } else { - return nameParts.some((splitName) => splitName.toLowerCase().startsWith(name.toLowerCase())); - } + return personId + ? nameParts.some((splitName) => splitName.toLowerCase().startsWith(name.toLowerCase())) && + person.id !== personId + : nameParts.some((splitName) => splitName.toLowerCase().startsWith(name.toLowerCase())); }) .slice(0, slice); }; diff --git a/web/src/lib/utils/time-to-seconds.spec.ts b/web/src/lib/utils/time-to-seconds.spec.ts index 2b0f31edba..3e6cb0839a 100644 --- a/web/src/lib/utils/time-to-seconds.spec.ts +++ b/web/src/lib/utils/time-to-seconds.spec.ts @@ -14,7 +14,7 @@ describe('converting time to seconds', () => { }); it('parses hhh:mm:ss.SSS correctly', () => { - expect(timeToSeconds('100:02:03.456')).toBeCloseTo(360123.456); + expect(timeToSeconds('100:02:03.456')).toBeCloseTo(360_123.456); }); it('ignores ignores double milliseconds hh:mm:ss.SSS.SSSSSS', () => { diff --git a/web/src/routes/(user)/albums/+page.svelte b/web/src/routes/(user)/albums/+page.svelte index 55f64be50f..77b9efeafb 100644 --- a/web/src/routes/(user)/albums/+page.svelte +++ b/web/src/routes/(user)/albums/+page.svelte @@ -227,11 +227,8 @@ }; const handleChangeListMode = () => { - if ($albumViewSettings.view === AlbumViewMode.Cover) { - $albumViewSettings.view = AlbumViewMode.List; - } else { - $albumViewSettings.view = AlbumViewMode.Cover; - } + $albumViewSettings.view = + $albumViewSettings.view === AlbumViewMode.Cover ? AlbumViewMode.List : AlbumViewMode.Cover; }; </script> @@ -285,14 +282,14 @@ </div> </LinkButton> </div> - {#if $albums.length !== 0} + {#if $albums.length > 0} <!-- Album Card --> {#if $albumViewSettings.view === AlbumViewMode.Cover} <div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))]"> - {#each $albums as album, idx (album.id)} + {#each $albums as album, index (album.id)} <a data-sveltekit-preload-data="hover" href="{AppRoute.ALBUMS}/{album.id}" animate:flip={{ duration: 200 }}> <AlbumCard - preload={idx < 20} + preload={index < 20} {album} on:showalbumcontextmenu={(e) => showAlbumContextMenu(e.detail, album)} /> diff --git a/web/src/routes/(user)/albums/[albumId]/+page.svelte b/web/src/routes/(user)/albums/[albumId]/+page.svelte index f15ff6411c..2769aeb806 100644 --- a/web/src/routes/(user)/albums/[albumId]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId]/+page.svelte @@ -111,14 +111,10 @@ const { selectedAssets: timelineSelected } = timelineInteractionStore; $: isOwned = $user.id == album.ownerId; - $: isAllUserOwned = Array.from($selectedAssets).every((asset) => asset.ownerId === $user.id); - $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite); + $: isAllUserOwned = [...$selectedAssets].every((asset) => asset.ownerId === $user.id); + $: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite); $: { - if (isShowActivity) { - assetGridWidth = globalWidth - (globalWidth < 768 ? 360 : 460); - } else { - assetGridWidth = globalWidth; - } + assetGridWidth = isShowActivity ? globalWidth - (globalWidth < 768 ? 360 : 460) : globalWidth; } $: showActivityStatus = album.sharedUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0); @@ -157,7 +153,7 @@ message: `Activity is ${album.isActivityEnabled ? 'enabled' : 'disabled'}`, }); } catch (error) { - handleError(error, `Can't ${!album.isActivityEnabled ? 'enable' : 'disable'} activity`); + handleError(error, `Can't ${album.isActivityEnabled ? 'disable' : 'enable'} activity`); } }; @@ -224,10 +220,11 @@ } const ctrl = event.ctrlKey; switch (event.key) { - case 'Enter': + case 'Enter': { if (ctrl && event.target === textArea) { textArea.blur(); } + } } }; @@ -302,7 +299,7 @@ }; const handleAddAssets = async () => { - const assetIds = Array.from($timelineSelected).map((asset) => asset.id); + const assetIds = [...$timelineSelected].map((asset) => asset.id); try { const { data: results } = await api.albumApi.addAssetsToAlbum({ @@ -352,7 +349,7 @@ const { data } = await api.albumApi.addUsersToAlbum({ id: album.id, addUsersDto: { - sharedUserIds: Array.from(users).map(({ id }) => id), + sharedUserIds: [...users].map(({ id }) => id), }, }); @@ -373,8 +370,8 @@ try { await refreshAlbum(); viewMode = album.sharedUsers.length > 1 ? ViewMode.SELECT_USERS : ViewMode.VIEW; - } catch (e) { - handleError(e, 'Error deleting share users'); + } catch (error) { + handleError(error, 'Error deleting share users'); } }; diff --git a/web/src/routes/(user)/albums/__tests__/albums.bloc.spec.ts b/web/src/routes/(user)/albums/__tests__/albums.bloc.spec.ts index 0aa48d532e..d95cc297fe 100644 --- a/web/src/routes/(user)/albums/__tests__/albums.bloc.spec.ts +++ b/web/src/routes/(user)/albums/__tests__/albums.bloc.spec.ts @@ -20,7 +20,9 @@ describe('Albums BLoC', () => { afterEach(() => { const notifications = get(notificationController.notificationList); - notifications.forEach((notification) => notificationController.removeNotificationById(notification.id)); + for (const notification of notifications) { + notificationController.removeNotificationById(notification.id); + } }); it('inits with provided albums', () => { diff --git a/web/src/routes/(user)/albums/albums.bloc.ts b/web/src/routes/(user)/albums/albums.bloc.ts index f1860ceeb7..11bea9e11d 100644 --- a/web/src/routes/(user)/albums/albums.bloc.ts +++ b/web/src/routes/(user)/albums/albums.bloc.ts @@ -3,10 +3,10 @@ import { notificationController, NotificationType } from '$lib/components/shared import { type AlbumResponseDto, api } from '@api'; import { derived, get, writable } from 'svelte/store'; -type AlbumsProps = { albums: AlbumResponseDto[] }; +type AlbumsProperties = { albums: AlbumResponseDto[] }; -export const useAlbums = (props: AlbumsProps) => { - const albums = writable([...props.albums]); +export const useAlbums = (properties: AlbumsProperties) => { + const albums = writable([...properties.albums]); const contextMenuPosition = writable<OnShowContextMenuDetail>({ x: 0, y: 0 }); const contextMenuTargetAlbum = writable<AlbumResponseDto | undefined>(); const isShowContextMenu = derived(contextMenuTargetAlbum, ($selectedAlbum) => !!$selectedAlbum); diff --git a/web/src/routes/(user)/archive/+page.svelte b/web/src/routes/(user)/archive/+page.svelte index 0d3714a021..6505ea111e 100644 --- a/web/src/routes/(user)/archive/+page.svelte +++ b/web/src/routes/(user)/archive/+page.svelte @@ -24,7 +24,7 @@ const assetInteractionStore = createAssetInteractionStore(); const { isMultiSelectState, selectedAssets } = assetInteractionStore; - $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite); + $: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite); </script> {#if $isMultiSelectState} diff --git a/web/src/routes/(user)/favorites/+page.svelte b/web/src/routes/(user)/favorites/+page.svelte index 4005a5d81c..3f49b95c60 100644 --- a/web/src/routes/(user)/favorites/+page.svelte +++ b/web/src/routes/(user)/favorites/+page.svelte @@ -26,7 +26,7 @@ const assetInteractionStore = createAssetInteractionStore(); const { isMultiSelectState, selectedAssets } = assetInteractionStore; - $: isAllArchive = Array.from($selectedAssets).every((asset) => asset.isArchived); + $: isAllArchive = [...$selectedAssets].every((asset) => asset.isArchived); </script> <!-- Multiselection mode app bar --> diff --git a/web/src/routes/(user)/partners/[userId]/+page.svelte b/web/src/routes/(user)/partners/[userId]/+page.svelte index ef60e7f24e..6d867adab6 100644 --- a/web/src/routes/(user)/partners/[userId]/+page.svelte +++ b/web/src/routes/(user)/partners/[userId]/+page.svelte @@ -19,7 +19,7 @@ const assetStore = new AssetStore({ userId: data.partner.id, isArchived: false, withStacked: true }); const assetInteractionStore = createAssetInteractionStore(); - const { isMultiSelectState, selectedAssets } = assetInteractionStore; + const { isMultiSelectState, selectedAssets, clearMultiselect } = assetInteractionStore; onDestroy(() => { assetInteractionStore.clearMultiselect(); @@ -28,7 +28,7 @@ <main class="grid h-screen bg-immich-bg pt-18 dark:bg-immich-dark-bg"> {#if $isMultiSelectState} - <AssetSelectControlBar assets={$selectedAssets} clearSelect={assetInteractionStore.clearMultiselect}> + <AssetSelectControlBar assets={$selectedAssets} clearSelect={clearMultiselect}> <CreateSharedLink /> <AssetSelectContextMenu icon={mdiPlus} title="Add"> <AddToAlbum /> diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index 16a9be0f1a..004742946c 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -90,9 +90,10 @@ return; } switch (event.key) { - case 'Escape': + case 'Escape': { handleCloseClick(); return; + } } }; @@ -288,10 +289,8 @@ } return; } - if (!force) { - if (people.length < maximumLengthSearchPeople && searchName.startsWith(searchWord)) { - return; - } + if (!force && people.length < maximumLengthSearchPeople && searchName.startsWith(searchWord)) { + return; } const timeout = setTimeout(() => (isSearchingPeople = true), timeBeforeShowLoadingSpinner); @@ -417,7 +416,7 @@ </FullScreenModal> {/if} -<UserPageLayout title="People" description={countTotalPeople !== 0 ? `(${countTotalPeople.toString()})` : undefined}> +<UserPageLayout title="People" description={countTotalPeople === 0 ? undefined : `(${countTotalPeople.toString()})`}> <svelte:fragment slot="buttons"> {#if countTotalPeople > 0} <div class="flex gap-2 items-center justify-center"> @@ -445,11 +444,11 @@ {#if countVisiblePeople > 0} <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-9 gap-1"> - {#each people as person, idx (person.id)} + {#each people as person, index (person.id)} {#if !person.isHidden && (searchName ? searchedPeopleLocal.some((searchedPerson) => searchedPerson.id === person.id) : true)} <PeopleCard {person} - preload={idx < 20} + preload={index < 20} on:change-name={() => handleChangeName(person)} on:set-birth-date={() => handleSetBirthDate(person)} on:merge-people={() => handleMergePeople(person)} @@ -519,7 +518,7 @@ screenHeight={innerHeight} > <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-9 gap-1"> - {#each people as person, idx (person.id)} + {#each people as person, index (person.id)} <button class="relative" on:click={() => (person.isHidden = !person.isHidden)} @@ -527,7 +526,7 @@ on:mouseleave={() => (eyeColorMap[person.id] = 'white')} > <ImageThumbnail - preload={searchName !== '' || idx < 20} + preload={searchName !== '' || index < 20} bind:hidden={person.isHidden} shadow url={api.getPeopleThumbnailUrl(person.id)} diff --git a/web/src/routes/(user)/people/[personId]/+page.svelte b/web/src/routes/(user)/people/[personId]/+page.svelte index 18fc06db0f..3d96387659 100644 --- a/web/src/routes/(user)/people/[personId]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/+page.svelte @@ -108,14 +108,14 @@ isSearchingPeople = false; }; - $: isAllArchive = Array.from($selectedAssets).every((asset) => asset.isArchived); - $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite); + $: isAllArchive = [...$selectedAssets].every((asset) => asset.isArchived); + $: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite); $: $onPersonThumbnail === data.person.id && (thumbnailData = api.getPeopleThumbnailUrl(data.person.id) + `?now=${Date.now()}`); $: { if (people) { - suggestedPeople = !name ? [] : searchNameLocal(name, people, 5, data.person.id); + suggestedPeople = name ? searchNameLocal(name, people, 5, data.person.id) : []; } } @@ -158,7 +158,7 @@ }); const handleUnmerge = () => { - $assetStore.removeAssets(Array.from($selectedAssets).map((a) => a.id)); + $assetStore.removeAssets([...$selectedAssets].map((a) => a.id)); assetInteractionStore.clearMultiselect(); viewMode = ViewMode.VIEW_ASSETS; }; @@ -352,7 +352,7 @@ {#if viewMode === ViewMode.UNASSIGN_ASSETS} <UnMergeFaceSelector - assetIds={Array.from($selectedAssets).map((a) => a.id)} + assetIds={[...$selectedAssets].map((a) => a.id)} personAssets={data.person} on:close={() => (viewMode = ViewMode.VIEW_ASSETS)} on:confirm={handleUnmerge} diff --git a/web/src/routes/(user)/photos/+page.svelte b/web/src/routes/(user)/photos/+page.svelte index 913c4d038e..f1dc364624 100644 --- a/web/src/routes/(user)/photos/+page.svelte +++ b/web/src/routes/(user)/photos/+page.svelte @@ -31,7 +31,7 @@ const assetInteractionStore = createAssetInteractionStore(); const { isMultiSelectState, selectedAssets } = assetInteractionStore; - $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite); + $: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite); const handleEscape = () => { if ($showAssetViewer) { diff --git a/web/src/routes/(user)/search/+page.svelte b/web/src/routes/(user)/search/+page.svelte index f49c7a0c58..42f9af0f0f 100644 --- a/web/src/routes/(user)/search/+page.svelte +++ b/web/src/routes/(user)/search/+page.svelte @@ -56,7 +56,7 @@ } if (!$showAssetViewer) { switch (event.key) { - case 'Escape': + case 'Escape': { if (isMultiSelectionMode) { selectedAssets = new Set(); return; @@ -66,6 +66,7 @@ } $preventRaceConditionSearchBar = false; return; + } } } }; @@ -96,8 +97,8 @@ let selectedAssets: Set<AssetResponseDto> = new Set(); $: isMultiSelectionMode = selectedAssets.size > 0; - $: isAllArchived = Array.from(selectedAssets).every((asset) => asset.isArchived); - $: isAllFavorite = Array.from(selectedAssets).every((asset) => asset.isFavorite); + $: isAllArchived = [...selectedAssets].every((asset) => asset.isArchived); + $: isAllFavorite = [...selectedAssets].every((asset) => asset.isFavorite); $: searchResultAssets = data.results?.assets.items; const onAssetDelete = (assetId: string) => { @@ -140,14 +141,14 @@ <section class="relative mb-12 bg-immich-bg pt-32 dark:bg-immich-dark-bg"> <section class="immich-scrollbar relative overflow-y-auto"> - {#if albums && albums.length} + {#if albums && albums.length > 0} <section> <div class="ml-6 text-4xl font-medium text-black/70 dark:text-white/80">ALBUMS</div> <div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))]"> - {#each albums as album, idx (album.id)} + {#each albums as album, index (album.id)} <a data-sveltekit-preload-data="hover" href={`albums/${album.id}`} animate:flip={{ duration: 200 }}> <AlbumCard - preload={idx < 20} + preload={index < 20} {album} isSharingView={false} showItemCount={false} diff --git a/web/src/routes/(user)/share/[key]/+page.svelte b/web/src/routes/(user)/share/[key]/+page.svelte index d45d1b06fc..d7bdb4d1c0 100644 --- a/web/src/routes/(user)/share/[key]/+page.svelte +++ b/web/src/routes/(user)/share/[key]/+page.svelte @@ -11,8 +11,8 @@ import { user } from '$lib/stores/user.store'; export let data: PageData; - let { sharedLink, passwordRequired, sharedLinkKey: key } = data; - let { title, description } = data.meta; + let { sharedLink, passwordRequired, sharedLinkKey: key, meta } = data; + let { title, description } = meta; let isOwned = $user ? $user.id === sharedLink?.userId : false; let password = ''; diff --git a/web/src/routes/(user)/share/[key]/+page.ts b/web/src/routes/(user)/share/[key]/+page.ts index 938e46d149..47b52f1f88 100644 --- a/web/src/routes/(user)/share/[key]/+page.ts +++ b/web/src/routes/(user)/share/[key]/+page.ts @@ -1,8 +1,8 @@ import { getAuthUser } from '$lib/utils/auth'; import { api, ThumbnailFormat } from '@api'; -import { error } from '@sveltejs/kit'; import type { AxiosError } from 'axios'; import type { PageLoad } from './$types'; +import { error as throwError } from '@sveltejs/kit'; export const load = (async ({ params }) => { const { key } = params; @@ -24,10 +24,10 @@ export const load = (async ({ params }) => { : '/feature-panel.png', }, }; - } catch (e) { + } catch (error) { // handle unauthorized error // TODO this doesn't allow for 404 shared links anymore - if ((e as AxiosError).response?.status === 401) { + if ((error as AxiosError).response?.status === 401) { return { passwordRequired: true, sharedLinkKey: key, @@ -37,7 +37,7 @@ export const load = (async ({ params }) => { }; } - error(404, { + throwError(404, { message: 'Invalid shared link', }); } diff --git a/web/src/routes/(user)/sharing/+page.svelte b/web/src/routes/(user)/sharing/+page.svelte index 3471fadca9..c244d5b815 100644 --- a/web/src/routes/(user)/sharing/+page.svelte +++ b/web/src/routes/(user)/sharing/+page.svelte @@ -28,13 +28,13 @@ }); goto(`${AppRoute.ALBUMS}/${newAlbum.id}`); - } catch (e) { + } catch (error) { notificationController.show({ message: 'Error creating album, check console for more details', type: NotificationType.Error, }); - console.log('Error [createAlbum] ', e); + console.log('Error [createAlbum]', error); } }; </script> @@ -94,9 +94,9 @@ <div> <!-- Share Album List --> <div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))]"> - {#each data.sharedAlbums as album, idx (album.id)} + {#each data.sharedAlbums as album, index (album.id)} <a data-sveltekit-preload-data="hover" href={`albums/${album.id}`} animate:flip={{ duration: 200 }}> - <AlbumCard preload={idx < 20} {album} isSharingView showContextMenu={false} /> + <AlbumCard preload={index < 20} {album} isSharingView showContextMenu={false} /> </a> {/each} </div> diff --git a/web/src/routes/(user)/trash/+page.svelte b/web/src/routes/(user)/trash/+page.svelte index d5e22a79ca..7cd37e328f 100644 --- a/web/src/routes/(user)/trash/+page.svelte +++ b/web/src/routes/(user)/trash/+page.svelte @@ -43,8 +43,8 @@ message: `Empty trash initiated. Refresh the page to see the changes`, type: NotificationType.Info, }); - } catch (e) { - handleError(e, 'Error emptying trash'); + } catch (error) { + handleError(error, 'Error emptying trash'); } }; @@ -56,8 +56,8 @@ message: `Restore trash initiated. Refresh the page to see the changes`, type: NotificationType.Info, }); - } catch (e) { - handleError(e, 'Error restoring trash'); + } catch (error) { + handleError(error, 'Error restoring trash'); } }; </script> diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte index b7cdcb0dad..e71278fd42 100644 --- a/web/src/routes/admin/jobs-status/+page.svelte +++ b/web/src/routes/admin/jobs-status/+page.svelte @@ -22,7 +22,7 @@ onMount(async () => { await load(); - timer = setInterval(load, 5_000); + timer = setInterval(load, 5000); }); onDestroy(() => { diff --git a/web/src/routes/admin/repair/+page.svelte b/web/src/routes/admin/repair/+page.svelte index 4295c60941..35666aa202 100644 --- a/web/src/routes/admin/repair/+page.svelte +++ b/web/src/routes/admin/repair/+page.svelte @@ -44,7 +44,7 @@ downloadManager.add(downloadKey, blob.size); downloadManager.update(downloadKey, blob.size); downloadBlob(blob, downloadKey); - setTimeout(() => downloadManager.clear(downloadKey), 5_000); + setTimeout(() => downloadManager.clear(downloadKey), 5000); } if (orphans.length > 0) { @@ -53,7 +53,7 @@ downloadManager.add(downloadKey, blob.size); downloadManager.update(downloadKey, blob.size); downloadBlob(blob, downloadKey); - setTimeout(() => downloadManager.clear(downloadKey), 5_000); + setTimeout(() => downloadManager.clear(downloadKey), 5000); } }; @@ -130,9 +130,9 @@ try { const chunkSize = 10; - const filenames = [...extras.filter(({ checksum }) => !checksum).map(({ filename }) => filename)]; - for (let i = 0; i < filenames.length; i += chunkSize) { - count += await loadAndMatch(filenames.slice(i, i + chunkSize)); + const filenames = extras.filter(({ checksum }) => !checksum).map(({ filename }) => filename); + for (let index = 0; index < filenames.length; index += chunkSize) { + count += await loadAndMatch(filenames.slice(index, index + chunkSize)); } } catch (error) { handleError(error, 'Unable to check items'); @@ -218,7 +218,7 @@ <tr class="flex w-full place-items-center p-2 md:p-5"> <th class="w-full text-sm place-items-center font-medium flex justify-between" colspan="2"> <div class="px-3"> - <p>MATCHES {matches.length ? `(${matches.length})` : ''}</p> + <p>MATCHES {matches.length > 0 ? `(${matches.length})` : ''}</p> <p class="text-gray-600 dark:text-gray-300 mt-1">These files are matched by their checksums</p> </div> </th> @@ -252,7 +252,7 @@ <tr class="flex w-full place-items-center p-1 md:p-5"> <th class="w-full text-sm font-medium justify-between place-items-center flex" colspan="2"> <div class="px-3"> - <p>OFFLINE PATHS {orphans.length ? `(${orphans.length})` : ''}</p> + <p>OFFLINE PATHS {orphans.length > 0 ? `(${orphans.length})` : ''}</p> <p class="text-gray-600 dark:text-gray-300 mt-1"> These files are the results of manually deletion of the default upload library </p> @@ -290,7 +290,7 @@ <tr class="flex w-full place-items-center p-2 md:p-5"> <th class="w-full text-sm font-medium place-items-center flex justify-between" colspan="2"> <div class="px-3"> - <p>UNTRACKS FILES {extras.length ? `(${extras.length})` : ''}</p> + <p>UNTRACKS FILES {extras.length > 0 ? `(${extras.length})` : ''}</p> <p class="text-gray-600 dark:text-gray-300 mt-1"> These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index c13fd96d30..e91809c063 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -38,7 +38,7 @@ downloadManager.add(downloadKey, blob.size); downloadManager.update(downloadKey, blob.size); downloadBlob(blob, downloadKey); - setTimeout(() => downloadManager.clear(downloadKey), 5_000); + setTimeout(() => downloadManager.clear(downloadKey), 5000); }; const settings = [ diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index 792838b6d4..812814c835 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -31,7 +31,7 @@ }); const isDeleted = (user: UserResponseDto): boolean => { - return user.deletedAt != null; + return user.deletedAt != undefined; }; const deleteDateFormat: Intl.DateTimeFormatOptions = { @@ -41,7 +41,7 @@ }; const getDeleteDate = (user: UserResponseDto): string => { - let deletedAt = new Date(user.deletedAt ? user.deletedAt : Date.now()); + let deletedAt = new Date(user.deletedAt ?? Date.now()); deletedAt.setDate(deletedAt.getDate() + 7); return deletedAt.toLocaleString($locale, deleteDateFormat); }; @@ -188,13 +188,13 @@ </thead> <tbody class="block max-h-[320px] w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray"> {#if allUsers} - {#each allUsers as immichUser, i} + {#each allUsers as immichUser, index} <tr class="flex h-[80px] overflow-hidden w-full place-items-center text-center dark:text-immich-dark-fg {isDeleted( immichUser, ) ? 'bg-red-300 dark:bg-red-900' - : i % 2 == 0 + : index % 2 == 0 ? 'bg-immich-gray dark:bg-immich-dark-gray/75' : 'bg-immich-bg dark:bg-immich-dark-gray/50'}" > diff --git a/web/src/routes/auth/onboarding/+page.svelte b/web/src/routes/auth/onboarding/+page.svelte index f8f91b679e..fbdd24fcc9 100644 --- a/web/src/routes/auth/onboarding/+page.svelte +++ b/web/src/routes/auth/onboarding/+page.svelte @@ -22,8 +22,8 @@ $: { const stepState = $page.url.searchParams.get('step'); - const tempIndex = onboardingSteps.findIndex((step) => step.name === stepState); - index = tempIndex >= 0 ? tempIndex : 0; + const temporaryIndex = onboardingSteps.findIndex((step) => step.name === stepState); + index = temporaryIndex >= 0 ? temporaryIndex : 0; } const handleDoneClicked = async () => { diff --git a/web/src/test-data/factories/album-factory.ts b/web/src/test-data/factories/album-factory.ts index 8cb92b50a2..37e838287b 100644 --- a/web/src/test-data/factories/album-factory.ts +++ b/web/src/test-data/factories/album-factory.ts @@ -7,7 +7,7 @@ export const albumFactory = Sync.makeFactory<AlbumResponseDto>({ albumName: Sync.each(() => faker.commerce.product()), description: '', albumThumbnailAssetId: null, - assetCount: Sync.each((i) => i % 5), + assetCount: Sync.each((index) => index % 5), assets: [], createdAt: Sync.each(() => faker.date.past().toISOString()), updatedAt: Sync.each(() => faker.date.past().toISOString()), diff --git a/web/vite.config.js b/web/vite.config.js index e1cf9ea75e..ea49b8cc47 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -1,5 +1,5 @@ import { sveltekit } from '@sveltejs/kit/vite'; -import path from 'path'; +import path from 'node:path'; const upstream = { target: process.env.IMMICH_SERVER_URL || 'http://immich-server:3001/', @@ -14,6 +14,7 @@ const config = { resolve: { alias: { 'xmlhttprequest-ssl': './node_modules/engine.io-client/lib/xmlhttprequest.js', + // eslint-disable-next-line unicorn/prefer-module '@test-data': path.resolve(__dirname, './src/test-data'), '@api': path.resolve('./src/api'), },