diff --git a/cli/.eslintrc.cjs b/cli/.eslintrc.cjs index 17a0a2dd6c2c2..ca36d31bf7f31 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 c9afe6e467349..9c218f78b99e7 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 6c18605661000..9e32061e3d0f1 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 17fc6541bc5c8..f026e374df2b2 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 { - 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 8369bff9348b2..6582b37956ff4 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 bfe94a89929b4..3ad0fcf3b8648 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 { 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 ee49a7074a58c..9276a4721065c 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 { @@ -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 e33764693a5dd..6899251eea53e 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)?(?\d+)\.(?\d+)\.(?\d+)/i; + const regex = /v?(?\d+)\.(?\d+)\.(?\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 b9761c6bd3a08..d1e7f780e3f3a 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 09872b3adfe2a..52b2ae082cb2e 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 ddb161c7ece77..1657938765556 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 4576ca4b6faef..3742f4c192748 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 2e46281fe6d31..f1e6564d8745f 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 8470fb9c688ba..c01dc1567dc95 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 12c36e3462a8f..481c878481a78 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 fe9d972239eae..8602701072748 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) { 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(); @@ -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 15482acaeb304..69386f561ebce 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 { diff --git a/server/src/domain/album/album-response.dto.ts b/server/src/domain/album/album-response.dto.ts index 671922408e8a6..1a266abac4d78 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 898e3f52638ff..b14779a802829 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 = albumMetadataForIds.reduce( - (obj: Record, { 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 = {}; + 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 2c4b6147afac7..0eef1981ce3dc 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 { - 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 3cbe2068b6def..a6b2cde3e88eb 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(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 087a5ebcf275e..e73858c31155b 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 { @@ -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 d70e5963c4c50..94a9f8a42d868 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 cb0f8399a1d7b..f4d0226b47c64 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 1466d5fdb240f..887b72e2cda5c 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, filename: string) => items.has(filename) || items.has(fullPath(filename)); + const hasFile = (items: Set, 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 ef3ee64b98918..c04bbc2630aa0 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 fd527ee0d9843..ff4ea4303288f 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 { @@ -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 4c490ffc55884..5af576a73b378 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 84cd4d8ee7d6e..4ec4b1124ca05 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 5589eb15a62d7..227595e04f6f6 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)?(?\d+)(?:\.(?\d+))?(?:[\.-](?\d+))?/i; + const regex = /v?(?\d+)(?:\.(?\d+))?(?:[.-](?\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 = { '.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 = Object.fromEntries( - Object.entries(image).filter(([key]) => profileExtensions.includes(key)), + Object.entries(image).filter(([key]) => profileExtensions.has(key)), ); const video: Record = { @@ -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 79a1913b8ee9c..ba5942cea44a1 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(collection: Array | Set, 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(collection: Array | Set, size: number): T[][] { export const setUnion = (...sets: Set[]): Set => { 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 = (...sets: Set[]): Set => { export const setDifference = (setA: Set, ...sets: Set[]): Set => { 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 = (set: Set, subset: Set): 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 0b28942705101..03bd6fee60f21 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 { @@ -82,12 +84,12 @@ export class DownloadService { const paths: Record = {}; 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 4abeb309b6a15..58cda724f814c 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 a804cf658b2aa..25055f1f317c9 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 4117f4129e3d6..1af343318b8e0 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 c064cd1b13a4d..dc36c9d23b628 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 { 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 6108ebf8da27d..68f861d7e2d55 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 6166a6d5cf07a..ab3e43ec9f779 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 9a1e11893f828..9a3a4bfed1fb6 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 00d8412c8d5e4..12ad57006d085 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 = (value: T): NonNullable | 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 7a9cf182b4755..a3f9a9f3df244 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 { @@ -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 { @@ -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 d5b7a27d7a4b0..e1937524afd27 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 39c7eb943939d..576f94c49106f 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 496081ddb21f0..07d0afca6bb8e 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; + getExtensionVersion(extensionName: string): Promise; getPostgresVersion(): Promise; createExtension(extension: DatabaseExtension): Promise; runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise; diff --git a/server/src/domain/repositories/media.repository.ts b/server/src/domain/repositories/media.repository.ts index 72da627bf5a02..60135e62dcc0f 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 8a01c73d51e67..c55aaf7ecdb5e 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 8695c26e0d3d0..932a865d04f22 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(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(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 1f1b51055b096..e097509e6abab 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 7da045a18a289..51d26b2c3d8ec 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 b2b488138f860..54e6f60521460 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 { - 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 { @@ -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 { 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 { 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 { @@ -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 3867d0885a29d..66c31b985151b 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 = { }; 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 a49f0347aa49e..6e17ca64e99b8 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 cbed4a06c95d7..d696982540f93 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 fef954baa025f..9456fd66b1f8b 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 8a33c7061a171..0a20e5cc2ac98 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({ 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[] = []; 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 9a5862db05563..191480b2b7f14 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({ 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 261f8f9a169da..5bf597e35dac6 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 f7f06c41778fd..38f1de1bcb783 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 { @@ -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 e6dff1655ce42..15800b9933c1a 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 b19ee5e84ff95..691bc7de49379 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 78743a0439697..13ae149b4e269 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 bdb8f74ed7401..7855b1ed6b253 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 { const users = await this.userRepository.getList({ withDeleted: !isAll }); - return users.map(mapUser); + return users.map((user) => mapUser(user)); } async get(userId: string): Promise { @@ -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 af36c590c9ceb..d19ddf4338eff 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 { - 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 { 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 ec68c98a1e26b..0688a65dd690a 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 8d3046c3a3222..d5fde4a625bf7 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 efad71dc896b2..6d59647cbf8d1 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(); 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 { @@ -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 85f0689a8ca6c..bd07d107b134d 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(); - const userAgent = UAParser(req.headers['user-agent']); +export const GetLoginDetails = createParamDecorator((data, context: ExecutionContext): LoginDetails => { + const request = context.switchToHttp().getRequest(); + 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(); + const request = context.switchToHttp().getRequest(); - const authDto = await this.authService.validate(req.headers, req.query as Record); + const authDto = await this.authService.validate(request.headers, request.query as Record); 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 0b3a18577c1b4..be82ae4dc86ad 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 0dd984a02d046..938f6f470872a 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(obj: T): T { - if (!obj || typeof obj !== 'object' || Array.isArray(obj)) { - return obj; +function sortKeys(target: T): T { + if (!target || typeof target !== 'object' || Array.isArray(target)) { + return target; } const result: Partial = {}; - const keys = Object.keys(obj).sort() as Array; + const keys = Object.keys(target).sort() as Array; 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 38cf8f23dcc20..15018c10de917 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 { 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 b7fd0fe021ea5..678e4a4f3cc69 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 25d4bdca46fe1..86045433d5764 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 { - 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 d94761d44a8be..52cc447e8e74a 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 { (error: null, result: T): void; } -const callbackify = async (fn: (...args: any[]) => T, callback: Callback) => { +const callbackify = async (target: (...arguments_: any[]) => T, callback: Callback) => { 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): Promise> { - const ctx = context.switchToHttp(); + const context_ = context.switchToHttp(); const route = this.reflect.get(PATH_METADATA, context.getClass()); const handler: RequestHandler | null = this.getHandler(route as Route); if (handler) { await new Promise((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); + private filename(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) { + return callbackify( + () => this.assetService.getUploadFilename(asRequest(request, file)), + callback as Callback, + ); } - private destination(req: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) { - return callbackify(() => this.assetService.getUploadFolder(asRequest(req, file)), callback as Callback); + private destination(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) { + return callbackify(() => this.assetService.getUploadFolder(asRequest(request, file)), callback as Callback); } - private handleFile(req: AuthRequest, file: Express.Multer.File, callback: Callback>) { + private handleFile(request: AuthRequest, file: Express.Multer.File, callback: Callback>) { (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 5cad3125700f9..9e6cccd19873a 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 90ca9fc818fb6..f72f333344f49 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 4dc821cd57fb3..585d058e03e30 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 91608472f7b57..1036df2afaccb 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 53a6f780bfae8..4d394adaf1e7b 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 9f8a72cff3432..a952f1646d5d2 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 f275b51713d68..cb6469195e948 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 aa66ba2dc8019..2d3fd795dbd8f 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 { // 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 226803cfb948d..95a227b693010 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 | FindOptionsWhere[] = {}; 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 | FindOptionsWhere[] = {}; 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 23edf8541165a..ec9eb005bf6a3 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 a21bf6253e88c..f445ed850b0ab 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((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 c9b44845d4559..2ae18432b2a8e 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 88f3a316ef0ec..8160ff84404de 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 { - 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(getQueueToken(queue), { strict: false }); + return this.moduleReference.get(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 71a6995188b35..4542c657793a1 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 884c24bf9b242..bb65dd25c8baa 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(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 { diff --git a/server/src/infra/repositories/metadata.repository.ts b/server/src/infra/repositories/metadata.repository.ts index d916795bcb55f..83c05597a2450 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 195fe5a5b41c8..85423b74ddeb4 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 4f9c52a66e4dc..ab43ff6f913f8 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 { diff --git a/server/src/infra/repositories/system-config.repository.ts b/server/src/infra/repositories/system-config.repository.ts index 4ab35b4d635da..82d0b8c8be849 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 { - return readFile(filename, { encoding: 'utf-8' }); + return readFile(filename, { encoding: 'utf8' }); } saveAll(items: SystemConfigEntity[]): Promise { diff --git a/server/src/infra/repositories/user.repository.ts b/server/src/infra/repositories/user.repository.ts index 5d55eea1f61d9..640eda0ee475c 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 { - 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 { diff --git a/server/src/infra/sql-generator/index.ts b/server/src/infra/sql-generator/index.ts index b4b0978a23cd3..348762d9571e8 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 78c3df148e51d..6f3c298c08447 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 c0e83130776f5..896f9ae5e0e77 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 | 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 c43d6ea461363..198b0f0877a67 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 fd9ffd5d58c73..b9644fb49a37c 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 19aba8f76ad94..47f95b8aabecc 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 4eb8884b1a931..cd6e81d2a2663 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 67ad5fff34ba8..077239d8a3dc4 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(emitter: EventEmitter, event: string): Promise { 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 fcc52df8f4233..36f646af639e5 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 ee6b767ef00e8..30dfec1669812 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({ diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts index d37a4af6e03d6..f34e6b06b58d3 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 => getPostgresVersion: jest.fn().mockResolvedValue(new Version(14, 0, 0)), createExtension: jest.fn().mockImplementation(() => Promise.resolve()), runMigrations: jest.fn(), - withLock: jest.fn().mockImplementation((_, func: () => Promise) => func()), + withLock: jest.fn().mockImplementation((_, function_: () => Promise) => function_()), isBusy: jest.fn(), wait: jest.fn(), }; diff --git a/server/tsconfig.json b/server/tsconfig.json index d86cba04c5014..6d89fe70884d2 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 9277676ac5d49..de62060e0f56d 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 835b0451e3dfb..93c04c47b68a6 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 fa7934d1a7aa6..9e4ccf460aeb3 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 387c754b25383..78228aee1f1a1 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) { - const searchParams = new URLSearchParams(); - for (const key in params) { - const value = params[key]; + private createUrl(path: string, parameters?: Record) { + 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) { + public getAssetFileUrl(...[assetId, isThumb, isWeb]: ApiParameters) { const path = `/asset/file/${assetId}`; return this.createUrl(path, { isThumb, isWeb, key: this.getKey() }); } - public getAssetThumbnailUrl(...[assetId, format]: ApiParams) { + public getAssetThumbnailUrl(...[assetId, format]: ApiParameters) { const path = `/asset/thumbnail/${assetId}`; return this.createUrl(path, { format, key: this.getKey() }); } - public getProfileImageUrl(...[userId]: ApiParams) { + public getProfileImageUrl(...[userId]: ApiParameters) { 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 a830c947d0a86..96baf2f3aa16e 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>; +export type ApiFp = (configuration: Configuration) => Record any>; export type OmitLast = 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 20444bb29c906..dfb93f703362c 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 d0904ad94d383..75d8ab6b8138c 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 34604e852c21e..2efd2c1bf60dc 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 19227a3abdcd4..95525ed9d3baf 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 98e202336a1af..3f7ddf7614d18 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) => { @@ -47,7 +43,10 @@ const reset = async (configKeys: Array) => { 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) => { - 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 ba5d0b24084fb..506cd042ba426 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]; } 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 5b080c2328d8b..d79f60a2ac430 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 026a5b4788de8..5e2d7b781bfe2 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 9bdf97a211d98..bbcd2f5c6a1f5 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 a9e94c4dd8b40..34b87e8df4452 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(); 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 23cf5959c5b75..9777cd1f5bfff 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 144c2dcce56e5..d05760c390cc5 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 e2c1968b29cac..f098fbf69a6dc 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; }; 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 8e77df9d9546d..60e082d439f7f 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 diff --git a/web/src/lib/components/asset-viewer/activity-viewer.svelte b/web/src/lib/components/asset-viewer/activity-viewer.svelte index b9c280ff92ee1..723a2fa7fb575 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}
-
@@ -244,7 +244,7 @@ {/if} {#if reaction.user.id === user.id || albumOwnerId === user.id}
-
diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 058b5f42e108f..0218ee7754f22 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 30f3ed0cdb239..fea5676b54cb9 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 4013568287e32..be398ee07bee5 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 177c5ba151223..cf7c16b3d8d4b 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; + let copyImageToClipboard: (source: string) => Promise; 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 1be7e8ad21ab5..c7aa0b6d83028 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 0cea5be9fbe49..de540b3208a97 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}
diff --git a/web/src/lib/components/elements/dropdown.svelte b/web/src/lib/components/elements/dropdown.svelte index b69b191f2567e..57757dafdbc72 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 ec1b61433234d..cb02ce5bb2e7a 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((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 cb5022d23d5cf..ab77a0df036e4 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 @@ }} > 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 5ae4bcc775185..5794e0c67dfb0 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 94aba63f5bfec..04fd47c273026 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(peopleWithFaces.length); - selectedPersonToReassign = new Array(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 2d3ab934101b2..b0434648c0fdc 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 218cd427fbcae..1116b87cf64bb 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 3d31573499943..8659cdcd058d1 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} {/if} -{#if editExclusionPattern != null} +{#if editExclusionPattern != undefined} 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 @@
(paused = !paused)} /> - {#each currentMemory.assets as _, i} + {#each currentMemory.assets as _, index} 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 8b4bded170585..e89e6376d834e 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 fc3739c4e60b2..731856212ed6f 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 296197a710dd2..28d683363e9af 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 6e700807af034..b3e68d30349be 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} 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 ac44514498194..667de9682a1a5 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 f4e7f685ee403..3619db950e91f 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 8ca73958a336d..2a70a0f476fa2 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 cf9c32818ec16..48b33719e5c00 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 f37d021c9a11a..1389cb76d5f99 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 4efbbda532978..5121be9ce314b 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 1cd3e0abab3c5..c14ea37882b0f 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'); } }; 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 ef50f28c8987b..ceaaec8cb46b0 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 07e759fcd706a..cf560dbbec448 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 53bc5440d4e9e..059ce61ed3c0f 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 @@
- {#each Array(100) as _} + {#each Array.from({ length: 100 }) as _}
{/each}
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 cb9def8ab5fe7..8410f34aa9f56 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, }); diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte index 5ddab9eaa3e00..be48dced22044 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}
- {#each $memoryStore as memory, i (memory.title)} + {#each $memoryStore as memory, index (memory.title)}