Compare commits

...

2 Commits

Author SHA1 Message Date
Daniel Dietzler 9d273d9f4f fix: linting 2026-07-08 15:07:00 +02:00
renovate[bot] 8bb30e0780 chore(deps): update dependency eslint-plugin-unicorn to v70 2026-07-08 15:06:36 +02:00
264 changed files with 1252 additions and 1350 deletions
+19 -19
View File
@@ -29,9 +29,6 @@
"editor.formatOnSave": true,
"tailwindCSS.lint.suggestCanonicalClasses": "ignore"
},
"svelte.plugin.svelte.compilerWarnings": {
"state_referenced_locally": "ignore"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
@@ -43,37 +40,40 @@
"eslint.useFlatConfig": true,
"eslint.validate": ["javascript", "typescript", "svelte"],
"eslint.workingDirectories": [
{ "directory": "cli", "changeProcessCWD": true },
{ "directory": "e2e", "changeProcessCWD": true },
{ "directory": "server", "changeProcessCWD": true },
{ "directory": "web", "changeProcessCWD": true }
{ "changeProcessCWD": true, "directory": "cli" },
{ "changeProcessCWD": true, "directory": "e2e" },
{ "changeProcessCWD": true, "directory": "server" },
{ "changeProcessCWD": true, "directory": "web" }
],
"files.watcherExclude": {
"**/.jj/**": true,
"**/.git/**": true,
"**/node_modules/**": true,
"**/build/**": true,
"**/dist/**": true,
"**/.svelte-kit/**": true
},
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
"*.js": "${capture}.spec.js,${capture}.mock.js",
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
"package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb, bun.lock, pnpm-workspace.yaml, .pnpmfile.cjs"
},
"files.watcherExclude": {
"**/.git/**": true,
"**/.jj/**": true,
"**/.svelte-kit/**": true,
"**/build/**": true,
"**/dist/**": true,
"**/node_modules/**": true
},
"js/ts.preferences.importModuleSpecifier": "non-relative",
"search.exclude": {
"**/node_modules": true,
"**/.svelte-kit": true,
"**/build": true,
"**/dist": true,
"**/.svelte-kit": true,
"**/node_modules": true,
"**/open-api/typescript-sdk/src": true
},
"svelte.enable-ts-plugin": true,
"svelte.plugin.svelte.compilerWarnings": {
"state_referenced_locally": "ignore"
},
"tailwindCSS.experimental.configFile": {
"web/src/app.css": "web/src/**"
},
"js/ts.preferences.importModuleSpecifier": "non-relative",
"vitest.maximumConfigs": 10
}
+9 -1
View File
@@ -41,9 +41,17 @@ export default typescriptEslint.config([
'@typescript-eslint/no-floating-promises': 'error',
'unicorn/prefer-module': 'off',
'unicorn/import-style': 'off',
'unicorn/consistent-boolean-name': 'off',
'unicorn/no-non-function-verb-prefix': 'off',
'unicorn/no-unreadable-for-of-expression': 'off',
'unicorn/max-nested-calls': 'off',
'unicorn/prefer-uint8array-base64': 'off',
'unicorn/isolated-functions': 'off',
'unicorn/prefer-promise-with-resolvers': 'off',
'unicorn/no-declarations-before-early-exit': 'off',
curly: 2,
'prettier/prettier': 0,
'unicorn/prevent-abbreviations': 'off',
'unicorn/name-replacements': 'off',
'unicorn/filename-case': 'off',
'unicorn/no-null': 'off',
'unicorn/prefer-top-level-await': 'off',
+1 -1
View File
@@ -40,7 +40,7 @@
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^64.0.0",
"eslint-plugin-unicorn": "^70.0.0",
"exiftool-vendored": "^35.0.0",
"globals": "^17.0.0",
"luxon": "^3.4.4",
@@ -118,7 +118,7 @@ describe('/admin/database-backups', () => {
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
cookie = headers['set-cookie'][0].split(';', 1)[0];
await expect
.poll(
@@ -224,7 +224,7 @@ describe('/admin/database-backups', () => {
});
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
cookie = headers['set-cookie'][0].split(';', 1)[0];
await expect
.poll(
@@ -295,7 +295,7 @@ describe('/admin/database-backups', () => {
});
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
cookie = headers['set-cookie'][0].split(';', 1)[0];
await expect
.poll(
@@ -85,7 +85,7 @@ describe('/admin/maintenance', () => {
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
cookie = headers['set-cookie'][0].split(';', 1)[0];
expect(cookie).toEqual(
expect.stringMatching(/^immich_maintenance_token=[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*$/),
);
@@ -149,7 +149,7 @@ describe('/admin/maintenance', () => {
const { status, body } = await request(app)
.post('/admin/maintenance/login')
.send({
token: cookie!.split('=')[1].trim(),
token: cookie!.split('=', 2)[1].trim(),
});
expect(status).toBe(201);
expect(body).toEqual(
@@ -27,7 +27,7 @@ test.describe('Maintenance', () => {
test('maintenance shows no options to users until they authenticate', async ({ page }) => {
const setCookie = await utils.enterMaintenance(admin.accessToken);
const cookie = setCookie
?.map((cookie) => cookie.split(';')[0].split('='))
?.map((cookie) => cookie.split(';', 1)[0].split('='))
?.find(([name]) => name === 'immich_maintenance_token');
expect(cookie).toBeTruthy();
@@ -120,6 +120,7 @@ describe('/albums', () => {
}),
]);
// eslint-disable-next-line unicorn/no-unreadable-array-destructuring
[user2Albums[0]] = await Promise.all([
getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }),
deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }),
+3 -3
View File
@@ -781,7 +781,7 @@ describe('/asset', () => {
exifImageWidth: 4032,
exifImageHeight: 3024,
latitude: 41.2203,
longitude: -96.071_625,
longitude: -96.071625,
make: 'Apple',
model: 'iPhone 7',
lensModel: 'iPhone 7 back camera 3.99mm f/1.8',
@@ -973,9 +973,9 @@ describe('/asset', () => {
fileSizeInByte: 31_175_472,
focalLength: 18.3,
iso: 100,
latitude: 36.613_24,
latitude: 36.61324,
lensModel: '18.3mm F2.8',
longitude: -121.897_85,
longitude: -121.89785,
make: 'RICOH IMAGING COMPANY, LTD.',
model: 'RICOH GR III',
orientation: '1',
+10 -10
View File
@@ -75,7 +75,7 @@ describe('/map', () => {
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
lon: expect.closeTo(-108.400968),
state: 'Colorado',
},
{
@@ -83,7 +83,7 @@ describe('/map', () => {
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
lon: expect.closeTo(-96.071625),
state: 'Nebraska',
},
]);
@@ -123,7 +123,7 @@ describe('/map', () => {
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
lon: expect.closeTo(-108.400968),
state: 'Colorado',
},
{
@@ -131,7 +131,7 @@ describe('/map', () => {
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
lon: expect.closeTo(-96.071625),
state: 'Nebraska',
},
]);
@@ -188,20 +188,20 @@ describe('/map', () => {
const reverseGeocodeTestCases = [
{
name: 'Vaucluse',
lat: -33.858_977_058_663_13,
lon: 151.278_490_730_270_48,
lat: -33.85897705866313,
lon: 151.27849073027048,
results: [{ city: 'Vaucluse', state: 'New South Wales', country: 'Australia' }],
},
{
name: 'Ravenhall',
lat: -37.765_732_399_174_75,
lon: 144.752_453_164_883_3,
lat: -37.76573239917475,
lon: 144.7524531648833,
results: [{ city: 'Ravenhall', state: 'Victoria', country: 'Australia' }],
},
{
name: 'Scarborough',
lat: -31.894_346_156_789_997,
lon: 115.757_617_103_904_64,
lat: -31.894346156789997,
lon: 115.75761710390464,
results: [{ city: 'Scarborough', state: 'Western Australia', country: 'Australia' }],
},
];
+1 -1
View File
@@ -44,7 +44,7 @@ const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) =>
});
// login
const response1 = await redirect(url.replace(authServer.internal, authServer.external));
const response1 = await redirect(url.replace(authServer.internal, () => authServer.external));
const response2 = await request(authServer.external + response1.location)
.post('')
.set('Cookie', response1.cookies)
+17 -17
View File
@@ -86,23 +86,23 @@ describe('/search', () => {
// note: the coordinates here are not the actual coordinates of the images and are random for most of them
const coordinates = [
{ latitude: 48.853_41, longitude: 2.3488 }, // paris
{ latitude: 35.6895, longitude: 139.691_71 }, // tokyo
{ latitude: 52.524_37, longitude: 13.410_53 }, // berlin
{ latitude: 1.314_663_1, longitude: 103.845_409_3 }, // singapore
{ latitude: 41.013_84, longitude: 28.949_66 }, // istanbul
{ latitude: 5.556_02, longitude: -0.1969 }, // accra
{ latitude: 37.544_270_6, longitude: -4.727_752_8 }, // andalusia
{ latitude: 23.133_02, longitude: -82.383_04 }, // havana
{ latitude: 41.694_11, longitude: 44.833_68 }, // tbilisi
{ latitude: 31.222_22, longitude: 121.458_06 }, // shanghai
{ latitude: 48.85341, longitude: 2.3488 }, // paris
{ latitude: 35.6895, longitude: 139.69171 }, // tokyo
{ latitude: 52.52437, longitude: 13.41053 }, // berlin
{ latitude: 1.3146631, longitude: 103.8454093 }, // singapore
{ latitude: 41.01384, longitude: 28.94966 }, // istanbul
{ latitude: 5.55602, longitude: -0.1969 }, // accra
{ latitude: 37.5442706, longitude: -4.7277528 }, // andalusia
{ latitude: 23.13302, longitude: -82.38304 }, // havana
{ latitude: 41.69411, longitude: 44.83368 }, // tbilisi
{ latitude: 31.22222, longitude: 121.45806 }, // shanghai
{ latitude: 38.9711, longitude: -109.7137 }, // thompson springs
{ latitude: 40.714_27, longitude: -74.005_97 }, // new york
{ latitude: 47.040_57, longitude: 9.068_04 }, // glarus
{ latitude: 32.771_52, longitude: -89.116_73 }, // philadelphia
{ latitude: 31.634_16, longitude: -7.999_94 }, // marrakesh
{ latitude: 38.523_735_4, longitude: -78.488_619_4 }, // tanners ridge
{ latitude: 59.938_63, longitude: 30.314_13 }, // st. petersburg
{ latitude: 40.71427, longitude: -74.00597 }, // new york
{ latitude: 47.04057, longitude: 9.06804 }, // glarus
{ latitude: 32.77152, longitude: -89.11673 }, // philadelphia
{ latitude: 31.63416, longitude: -7.99994 }, // marrakesh
{ latitude: 38.5237354, longitude: -78.4886194 }, // tanners ridge
{ latitude: 59.93863, longitude: 30.31413 }, // st. petersburg
{ latitude: 0, longitude: 0 }, // null island
];
@@ -111,7 +111,7 @@ describe('/search', () => {
);
await Promise.all(updates);
for (const [i] of coordinates.entries()) {
for (const i of coordinates.keys()) {
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: assets[i].id });
}
+2 -2
View File
@@ -31,7 +31,7 @@ describe(`immich login`, () => {
it('should login and save auth.yml with 600', async () => {
const admin = await utils.adminSetup();
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
const { stdout, stderr, exitCode } = await immichCli(['login', app, `${key.secret}`]);
const { stdout, stderr, exitCode } = await immichCli(['login', app, key.secret]);
expect(stdout.split('\n')).toEqual([
'Logging in to http://127.0.0.1:2285/api',
'Logged in as admin@immich.cloud',
@@ -48,7 +48,7 @@ describe(`immich login`, () => {
it('should login without /api in the url', async () => {
const admin = await utils.adminSetup();
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), `${key.secret}`]);
const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), key.secret]);
expect(stdout.split('\n')).toEqual([
'Logging in to http://127.0.0.1:2285',
'Discovered API at http://127.0.0.1:2285/api',
+6 -2
View File
@@ -119,7 +119,9 @@ describe(`immich upload`, () => {
const baseDir = `/tmp/upload/`;
const testPaths = Object.keys(files).map((filePath) => `${baseDir}/${filePath}`);
testPaths.map((filePath) => utils.createImageFile(filePath));
for (const filePath of testPaths) {
utils.createImageFile(filePath);
}
const commandLine = paths.map((argument) => `${baseDir}/${argument}`);
@@ -135,7 +137,9 @@ describe(`immich upload`, () => {
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(expectedCount);
testPaths.map((filePath) => utils.removeImageFile(filePath));
for (const filePath of testPaths) {
utils.removeImageFile(filePath);
}
});
}
});
+1 -1
View File
@@ -21,7 +21,7 @@ export const randomImageFromString = async (
let seedNumber = 0;
for (let i = 0; i < seed.length; i++) {
seedNumber = (seedNumber << 5) - seedNumber + (seed.codePointAt(i) ?? 0);
seedNumber = seedNumber & seedNumber; // Convert to 32bit integer
seedNumber &= seedNumber; // Convert to 32bit integer
}
return randomImage(new SeededRandom(Math.abs(seedNumber)), { width, height });
};
@@ -64,7 +64,7 @@ export function generateAsset(
const asset: MockTimelineAsset = {
id: assetId,
ownerId,
ratio: Number.parseFloat(ratio.split(':')[0]) / Number.parseFloat(ratio.split(':')[1]),
ratio: Number(ratio.split(':', 1)[0]) / Number(ratio.split(':', 2)[1]),
thumbhash: generateThumbhash(rng),
localDateTime: date.toISOString(),
fileCreatedAt: date.toISOString(),
@@ -214,7 +214,7 @@ export function generateTimelineData(config: TimelineConfig): MockTimelineData {
}
// Create a mock album from random assets
const allAssets = [...buckets.values()].flat();
const allAssets = buckets.values().toArray().flat();
// Select 10-30 random assets for the album (or all assets if less than 10)
const albumSize = Math.min(allAssets.length, globalRng.nextInt(10, 31));
@@ -172,11 +172,7 @@ function shouldIncludeAsset(
if (isArchived !== undefined && actuallyArchived !== isArchived) {
return false;
}
if (isFavorite !== undefined && actuallyFavorited !== isFavorite) {
return false;
}
return true;
return isFavorite === undefined || actuallyFavorited === isFavorite;
}
/**
* Get summary for all buckets (mimics getTimeBuckets API)
@@ -361,7 +357,7 @@ export function getAsset(
owner?: UserResponseDto,
): AssetResponseDto | undefined {
// Search through all buckets for the asset
const buckets = [...timelineData.buckets.values()];
const buckets = timelineData.buckets.values().toArray();
for (const assets of buckets) {
const asset = assets.find((a) => a.id === assetId);
if (asset) {
@@ -395,7 +391,7 @@ export function getAlbum(
// Get the actual asset objects from the timeline data
const albumAssets: AssetResponseDto[] = [];
const allAssets = [...timelineData.buckets.values()].flat();
const allAssets = timelineData.buckets.values().toArray().flat();
for (const assetId of album.assetIds) {
const assetConfig = allAssets.find((a) => a.id === assetId);
@@ -143,7 +143,7 @@ export function validateTimelineConfig(config: TimelineConfig): void {
}
// Validate seed if provided
if (config.seed !== undefined && (config.seed < 0 || !Number.isInteger(config.seed))) {
if (config.seed !== undefined && (config.seed < 0 || !Number.isSafeInteger(config.seed))) {
throw new Error('Seed must be a non-negative integer');
}
+1 -4
View File
@@ -153,11 +153,8 @@ export function getMockAsset(
const isInDifferentPeriod = (date1: DateTime, date2: DateTime): boolean => {
if (unit === 'day') {
return !date1.startOf('day').equals(date2.startOf('day'));
} else if (unit === 'month') {
return date1.year !== date2.year || date1.month !== date2.month;
} else {
return date1.year !== date2.year;
}
return unit === 'month' ? date1.year !== date2.year || date1.month !== date2.month : date1.year !== date2.year;
};
if (direction === 'next') {
+2 -1
View File
@@ -40,7 +40,8 @@ export const setupTimelineMockApiRoutes = async (
contentType: 'application/json',
json: getTimeBuckets(timelineRestData, isTrashed, isArchived, isFavorite, albumId, changes),
});
} else if (pathname === '/api/timeline/bucket') {
}
if (pathname === '/api/timeline/bucket') {
const timeBucket = url.searchParams.get('timeBucket');
if (!timeBucket) {
return route.continue();
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable unicorn/no-this-outside-of-class */
import type { AssetResponseDto } from '@immich/sdk';
import { expect, Page } from '@playwright/test';
+3
View File
@@ -72,6 +72,7 @@ export const thumbnailUtils = {
},
async queryThumbnailInViewport(page: Page, collector: (assetId: string) => boolean) {
const assetIds: string[] = [];
// eslint-disable-next-line unicorn/no-this-outside-of-class
for (const thumb of await this.locator(page).all()) {
const box = await thumb.boundingBox();
if (box) {
@@ -151,6 +152,7 @@ export const timelineUtils = {
page.evaluate(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return document.querySelector('#asset-grid').scrollTop;
});
await expect.poll(queryTop).toBeGreaterThan(0);
@@ -177,6 +179,7 @@ export const assetViewerUtils = {
page.evaluate(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line unicorn/no-optional-chaining-on-undeclared-variable
return document.activeElement?.dataset?.asset;
});
await expect(poll(page, activeElement, (result) => result === assetId)).resolves.toBe(assetId);
+6 -3
View File
@@ -1,3 +1,4 @@
/* eslint-disable unicorn/no-top-level-assignment-in-function */
import {
AssetMediaCreateDto,
AssetMediaResponseDto,
@@ -177,7 +178,7 @@ export const utils = {
resetDatabase: async (tables?: string[]) => {
client = await utils.connectDatabase();
tables = tables || [
tables ||= [
// TODO e2e test for deleting a stack, since it is quite complex
'stack',
'library',
@@ -304,7 +305,7 @@ export const utils = {
},
adminSetup: async (options?: AdminSetupOptions) => {
options = options || { onboarding: true };
options ||= { onboarding: true };
await signUpAdmin({ signUpDto: signupDto.admin });
const response = await login({ loginCredentialDto: loginDto.admin });
@@ -545,6 +546,7 @@ export const utils = {
{
headers: asBearerAuth(accessToken),
fetch: (...args: Parameters<typeof fetch>) =>
// eslint-disable-next-line unicorn/no-invalid-argument-count, unicorn/prefer-await
fetch(...args).then((response) => {
setCookie = response.headers.getSetCookie();
return response;
@@ -674,7 +676,7 @@ export const utils = {
cliLogin: async (accessToken: string) => {
const key = await utils.createApiKey(accessToken, [Permission.All]);
await immichCli(['login', app, `${key.secret}`]);
await immichCli(['login', app, key.secret]);
return key.secret;
},
@@ -706,6 +708,7 @@ export const utils = {
},
};
// eslint-disable-next-line unicorn/no-top-level-side-effects
utils.initSdk();
if (!existsSync(`${testAssetDir}/albums`)) {
+1
View File
@@ -10,6 +10,7 @@
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"target": "es2023",
"lib": ["dom", "ESNext"],
"sourceMap": true,
"outDir": "./dist",
"incremental": true,
+7 -1
View File
@@ -40,10 +40,16 @@ export default typescriptEslint.config([
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'error',
'unicorn/prefer-module': 'off',
'unicorn/prevent-abbreviations': 'off',
'unicorn/name-replacements': 'off',
'unicorn/no-unreadable-for-of-expression': 'off',
'unicorn/no-declarations-before-early-exit': 'off',
'unicorn/no-process-exit': 'off',
'unicorn/import-style': 'off',
'unicorn/consistent-class-member-order': 'off',
curly: 2,
// prefer the typescript-eslint type-aware version
'unicorn/require-array-sort-compare': 'off',
'@typescript-eslint/require-array-sort-compare': 'error',
'prettier/prettier': 0,
'object-shorthand': ['error', 'always'],
},
+1 -1
View File
@@ -33,7 +33,7 @@
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^64.0.0",
"eslint-plugin-unicorn": "^70.0.0",
"globals": "^17.0.0",
"mock-fs": "^5.2.0",
"prettier": "^3.7.4",
+1 -1
View File
@@ -39,7 +39,7 @@ describe('uploadFiles', () => {
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
const testFilePath = path.join(testDir, 'test.png');
const testFileData = 'test';
const baseUrl = 'http://example.com';
const baseUrl = 'https://example.com';
const apiKey = 'key';
const retry = 3;
+3 -2
View File
@@ -54,6 +54,7 @@ class UploadFile extends File {
super([], basename(filepath));
}
// @ts-expect-error size is already a property on the new File interface
get size() {
return this._size;
}
@@ -433,7 +434,7 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
throw new Error(await response.text());
}
return response.json();
return response.json() as Promise<AssetMediaResponseDto>;
};
export const findSidecar = (filepath: string): string | undefined => {
@@ -570,7 +571,7 @@ const updateAlbums = async (assets: Asset[], options: UploadOptionsDto) => {
albumUpdateProgress.start(assets.length, 0);
try {
for (const [albumId, assets] of albumToAssets.entries()) {
for (const [albumId, assets] of albumToAssets) {
for (const assetBatch of chunk(assets, Math.min(1000 * concurrency, 65_000))) {
await addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetBatch } });
albumUpdateProgress.increment(assetBatch.length);
+1 -4
View File
@@ -52,10 +52,7 @@ export class Queue<T, R> {
}
get tasks(): Task<T, R>[] {
const tasks: Task<T, R>[] = [];
for (const task of this.store.values()) {
tasks.push(task);
}
const tasks: Task<T, R>[] = this.store.values().toArray();
return tasks;
}
+10 -6
View File
@@ -39,6 +39,7 @@ export const s = (count: number) => (count === 1 ? '' : 's');
let _apiKey: ApiKeyResponseDto;
export const requirePermissions = async (permissions: Permission[]) => {
if (!_apiKey) {
// eslint-disable-next-line unicorn/no-top-level-assignment-in-function
_apiKey = await getMyApiKey();
}
@@ -67,8 +68,9 @@ Please make sure your API key has the correct permissions.`,
export const connect = async (url: string, key: string) => {
const wellKnownUrl = new URL('.well-known/immich', url);
try {
const wellKnown = await fetch(wellKnownUrl).then((response) => response.json());
const endpoint = new URL(wellKnown.api.endpoint, url).toString();
// eslint-disable-next-line unicorn/prefer-await
const wellKnown = (await fetch(wellKnownUrl).then((response) => response.json())) as { api: { endpoint: string } };
const endpoint = new URL(wellKnown.api.endpoint, url).href;
if (endpoint !== url) {
console.debug(`Discovered API at ${endpoint}`);
}
@@ -178,7 +180,7 @@ export const crawl = async (options: CrawlOptions): Promise<string[]> => {
const searchPatterns = patterns.map((pattern) => {
let escapedPattern = pattern.replaceAll("'", "[']").replaceAll('"', '["]').replaceAll('`', '[`]');
if (recursive) {
escapedPattern = escapedPattern + '/**';
escapedPattern += '/**';
}
return `${escapedPattern}/*.{${extensions.join(',')}}`;
});
@@ -238,10 +240,12 @@ export class Batcher<T = unknown> {
}
private clearDebounceTimer() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = undefined;
if (!this.debounceTimer) {
return;
}
clearTimeout(this.debounceTimer);
this.debounceTimer = undefined;
}
add(item: T) {
+1
View File
@@ -10,6 +10,7 @@
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"target": "es2023",
"lib": ["ESNext"],
"sourceMap": true,
"outDir": "./dist",
"incremental": true,
+25 -172
View File
@@ -34,7 +34,7 @@ importers:
dependencies:
'@docusaurus/core':
specifier: ~3.10.0
version: 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(esbuild@0.28.1)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
version: 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
'@docusaurus/preset-classic':
specifier: ~3.10.0
version: 3.10.1(@algolia/client-search@5.54.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(typescript@6.0.3)
@@ -58,7 +58,7 @@ importers:
version: 10.5.0(postcss@8.5.15)
docusaurus-lunr-search:
specifier: ^3.3.2
version: 3.6.0(@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(esbuild@0.28.1)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
version: 3.6.0(@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
lunr:
specifier: ^2.3.9
version: 2.3.9
@@ -151,8 +151,8 @@ importers:
specifier: ^5.1.3
version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.5.0(jiti@2.7.0)))(eslint@10.5.0(jiti@2.7.0))(prettier@3.8.4)
eslint-plugin-unicorn:
specifier: ^64.0.0
version: 64.0.0(eslint@10.5.0(jiti@2.7.0))
specifier: ^70.0.0
version: 70.0.0(eslint@10.5.0(jiti@2.7.0))
exiftool-vendored:
specifier: ^35.0.0
version: 35.21.0
@@ -260,8 +260,8 @@ importers:
specifier: ^5.1.3
version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.5.0(jiti@2.7.0)))(eslint@10.5.0(jiti@2.7.0))(prettier@3.8.4)
eslint-plugin-unicorn:
specifier: ^64.0.0
version: 64.0.0(eslint@10.5.0(jiti@2.7.0))
specifier: ^70.0.0
version: 70.0.0(eslint@10.5.0(jiti@2.7.0))
globals:
specifier: ^17.0.0
version: 17.6.0
@@ -729,8 +729,8 @@ importers:
specifier: ^5.1.3
version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.5.0(jiti@2.7.0)))(eslint@10.5.0(jiti@2.7.0))(prettier@3.8.4)
eslint-plugin-unicorn:
specifier: ^64.0.0
version: 64.0.0(eslint@10.5.0(jiti@2.7.0))
specifier: ^70.0.0
version: 70.0.0(eslint@10.5.0(jiti@2.7.0))
globals:
specifier: ^17.0.0
version: 17.6.0
@@ -991,8 +991,8 @@ importers:
specifier: ^3.12.4
version: 3.19.0(eslint@10.5.0(jiti@2.7.0))(svelte@5.56.3(@typescript-eslint/types@8.61.0))
eslint-plugin-unicorn:
specifier: ^64.0.0
version: 64.0.0(eslint@10.5.0(jiti@2.7.0))
specifier: ^70.0.0
version: 70.0.0(eslint@10.5.0(jiti@2.7.0))
factory.ts:
specifier: ^1.4.1
version: 1.4.2
@@ -6509,10 +6509,6 @@ packages:
resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==}
engines: {node: '>= 10.0'}
clean-regexp@1.0.0:
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
engines: {node: '>=4'}
clean-stack@2.2.0:
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
engines: {node: '>=6'}
@@ -7288,6 +7284,10 @@ packages:
detect-europe-js@0.1.2:
resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==}
detect-indent@7.0.2:
resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==}
engines: {node: '>=12.20'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -7643,11 +7643,11 @@ packages:
svelte:
optional: true
eslint-plugin-unicorn@64.0.0:
resolution: {integrity: sha512-rNZwalHh8i0UfPlhNwg5BTUO1CMdKNmjqe+TgzOTZnpKoi8VBgsW7u9qCHIdpxEzZ1uwrJrPF0uRb7l//K38gA==}
engines: {node: ^20.10.0 || >=21.0.0}
eslint-plugin-unicorn@70.0.0:
resolution: {integrity: sha512-uAF9xMcVvvhTfvusCgogJ1wh4To3q2KhVMw3i1Apf/ILTbxsCjscvraAZACsEurb7no2fdXblD3whcbVnjw5zg==}
engines: {node: '>=22'}
peerDependencies:
eslint: '>=9.38.0'
eslint: '>=10.4'
eslint-scope@5.1.1:
resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
@@ -11042,10 +11042,6 @@ packages:
regenerate@1.4.2:
resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
regexp-tree@0.1.27:
resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==}
hasBin: true
regexpu-core@6.4.0:
resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==}
engines: {node: '>=4'}
@@ -14402,49 +14398,6 @@ snapshots:
- uglify-js
- webpack-cli
'@docusaurus/bundler@3.10.1(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)':
dependencies:
'@babel/core': 7.29.7
'@docusaurus/babel': 3.10.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/cssnano-preset': 3.10.1
'@docusaurus/logger': 3.10.1
'@docusaurus/types': 3.10.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.10.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
babel-loader: 9.2.1(@babel/core@7.29.7)(webpack@5.107.2(postcss@8.5.15))
clean-css: 5.3.3
copy-webpack-plugin: 11.0.0(webpack@5.107.2(postcss@8.5.15))
css-loader: 6.11.0(webpack@5.107.2(postcss@8.5.15))
css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(esbuild@0.28.1)(webpack@5.107.2(postcss@8.5.15))
cssnano: 6.1.2(postcss@8.5.15)
file-loader: 6.2.0(webpack@5.107.2(postcss@8.5.15))
html-minifier-terser: 7.2.0
mini-css-extract-plugin: 2.10.2(webpack@5.107.2(postcss@8.5.15))
null-loader: 4.0.1(webpack@5.107.2(postcss@8.5.15))
postcss: 8.5.15
postcss-loader: 7.3.4(postcss@8.5.15)(typescript@6.0.3)(webpack@5.107.2(postcss@8.5.15))
postcss-preset-env: 10.6.1(postcss@8.5.15)
terser-webpack-plugin: 5.6.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.15)(webpack@5.107.2(postcss@8.5.15))
tslib: 2.8.1
url-loader: 4.1.1(file-loader@6.2.0(webpack@5.107.2(postcss@8.5.15)))(webpack@5.107.2(postcss@8.5.15))
webpack: 5.107.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(html-minifier-terser@7.2.0)(postcss@8.5.15)
webpackbar: 7.0.0(webpack@5.107.2(postcss@8.5.15))
transitivePeerDependencies:
- '@minify-html/node'
- '@parcel/css'
- '@rspack/core'
- '@swc/core'
- '@swc/css'
- '@swc/html'
- csso
- esbuild
- lightningcss
- react
- react-dom
- supports-color
- typescript
- uglify-js
- webpack-cli
'@docusaurus/bundler@3.10.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)':
dependencies:
'@babel/core': 7.29.7
@@ -14488,75 +14441,6 @@ snapshots:
- uglify-js
- webpack-cli
'@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(esbuild@0.28.1)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)':
dependencies:
'@docusaurus/babel': 3.10.1(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/bundler': 3.10.1(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
'@docusaurus/logger': 3.10.1
'@docusaurus/mdx-loader': 3.10.1(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils': 3.10.1(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-common': 3.10.1(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@docusaurus/utils-validation': 3.10.1(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.7)
boxen: 6.2.1
chalk: 4.1.2
chokidar: 3.6.0
cli-table3: 0.6.5
combine-promises: 1.2.0
commander: 5.1.0
core-js: 3.49.0
detect-port: 1.6.1
escape-html: 1.0.3
eta: 2.2.0
eval: 0.1.8
execa: 5.1.1
fs-extra: 11.3.5
html-tags: 3.3.1
html-webpack-plugin: 5.6.7(webpack@5.107.2(postcss@8.5.15))
leven: 3.1.0
lodash: 4.18.1
open: 8.4.2
p-map: 4.0.0
prompts: 2.4.2
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)'
react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.7)'
react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.7))(webpack@5.107.2(postcss@8.5.15))
react-router: 5.3.4(react@19.2.7)
react-router-config: 5.1.1(react-router@5.3.4(react@19.2.7))(react@19.2.7)
react-router-dom: 5.3.4(react@19.2.7)
semver: 7.8.4
serve-handler: 6.1.7
tinypool: 1.1.1
tslib: 2.8.1
update-notifier: 6.0.2
webpack: 5.107.2(postcss@8.5.15)
webpack-bundle-analyzer: 4.10.2
webpack-dev-server: 5.2.5(tslib@2.8.1)(webpack@5.107.2(postcss@8.5.15))
webpack-merge: 6.0.1
transitivePeerDependencies:
- '@minify-html/node'
- '@parcel/css'
- '@rspack/core'
- '@swc/core'
- '@swc/css'
- '@swc/html'
- bufferutil
- clean-css
- cssnano
- csso
- debug
- esbuild
- html-minifier-terser
- lightningcss
- postcss
- supports-color
- typescript
- uglify-js
- utf-8-validate
- webpack-cli
'@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)':
dependencies:
'@docusaurus/babel': 3.10.1(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
@@ -19613,10 +19497,6 @@ snapshots:
dependencies:
source-map: 0.6.1
clean-regexp@1.0.0:
dependencies:
escape-string-regexp: 1.0.5
clean-stack@2.2.0: {}
cli-boxes@3.0.0: {}
@@ -19977,19 +19857,6 @@ snapshots:
optionalDependencies:
webpack: 5.107.2(postcss@8.5.15)
css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(esbuild@0.28.1)(webpack@5.107.2(postcss@8.5.15)):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
cssnano: 6.1.2(postcss@8.5.15)
jest-worker: 29.7.0
postcss: 8.5.15
schema-utils: 4.3.3
serialize-javascript: 6.0.2
webpack: 5.107.2(postcss@8.5.15)
optionalDependencies:
clean-css: 5.3.3
esbuild: 0.28.1
css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.107.2(postcss@8.5.15)):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
@@ -20399,6 +20266,8 @@ snapshots:
detect-europe-js@0.1.2: {}
detect-indent@7.0.2: {}
detect-libc@2.1.2: {}
detect-node@2.1.0: {}
@@ -20467,9 +20336,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
docusaurus-lunr-search@3.6.0(@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(esbuild@0.28.1)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
docusaurus-lunr-search@3.6.0(@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
'@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(esbuild@0.28.1)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
'@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
autocomplete.js: 0.37.1
clsx: 2.1.1
gauge: 3.0.2
@@ -20890,14 +20759,15 @@ snapshots:
transitivePeerDependencies:
- ts-node
eslint-plugin-unicorn@64.0.0(eslint@10.5.0(jiti@2.7.0)):
eslint-plugin-unicorn@70.0.0(eslint@10.5.0(jiti@2.7.0)):
dependencies:
'@babel/helper-validator-identifier': 7.29.7
'@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0))
browserslist: 4.28.2
change-case: 5.4.4
ci-info: 4.4.0
clean-regexp: 1.0.0
core-js-compat: 3.49.0
detect-indent: 7.0.2
eslint: 10.5.0(jiti@2.7.0)
find-up-simple: 1.0.1
globals: 17.6.0
@@ -20905,7 +20775,6 @@ snapshots:
is-builtin-module: 5.0.0
jsesc: 3.1.0
pluralize: 8.0.0
regexp-tree: 0.1.27
regjsparser: 0.13.2
semver: 7.8.4
strip-indent: 4.1.1
@@ -24890,8 +24759,6 @@ snapshots:
regenerate@1.4.2: {}
regexp-tree@0.1.27: {}
regexpu-core@6.4.0:
dependencies:
regenerate: 1.4.2
@@ -26068,20 +25935,6 @@ snapshots:
esbuild: 0.28.1
lightningcss: 1.32.0
terser-webpack-plugin@5.6.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.15)(webpack@5.107.2(postcss@8.5.15)):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
jest-worker: 27.5.1
schema-utils: 4.3.3
terser: 5.48.0
webpack: 5.107.2(postcss@8.5.15)
optionalDependencies:
clean-css: 5.3.3
cssnano: 6.1.2(postcss@8.5.15)
esbuild: 0.28.1
html-minifier-terser: 7.2.0
postcss: 8.5.15
terser-webpack-plugin@5.6.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(html-minifier-terser@7.2.0)(postcss@8.5.15)(webpack@5.107.2(postcss@8.5.15)):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
+19 -1
View File
@@ -39,7 +39,7 @@ export default typescriptEslint.config([
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'error',
'unicorn/prevent-abbreviations': 'off',
'unicorn/name-replacements': 'off',
'unicorn/filename-case': 'off',
'unicorn/no-null': 'off',
'unicorn/prefer-top-level-await': 'off',
@@ -49,6 +49,24 @@ export default typescriptEslint.config([
'unicorn/prefer-structured-clone': 'off',
'unicorn/no-for-loop': 'off',
'unicorn/no-array-sort': 'off',
'unicorn/no-unreadable-for-of-expression': 'off',
'unicorn/no-break-in-nested-loop': 'off',
'unicorn/no-top-level-assignment-in-function': 'off',
'unicorn/prefer-uint8array-base64': 'off',
'unicorn/max-nested-calls': 'off',
'unicorn/no-declarations-before-early-exit': 'off',
'unicorn/no-unreadable-object-destructuring': 'off',
// maybe we do want to enable this later. TBD
'unicorn/prefer-await': 'off',
'unicorn/consistent-class-member-order': 'off',
'unicorn/class-reference-in-static-methods': ['error', { preferThis: false, preferSuper: false }],
'unicorn/no-unsafe-property-key': 'off',
'unicorn/consistent-boolean-name': 'off',
'unicorn/no-computed-property-existence-check': 'off',
'unicorn/no-non-function-verb-prefix': 'off',
// prefer the typescript-eslint type-aware version
'unicorn/require-array-sort-compare': 'off',
'@typescript-eslint/require-array-sort-compare': 'error',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/switch-exhaustiveness-check': ['error', { considerDefaultExhaustiveForUnions: true }],
+1 -1
View File
@@ -151,7 +151,7 @@
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^64.0.0",
"eslint-plugin-unicorn": "^70.0.0",
"globals": "^17.0.0",
"mock-fs": "^5.2.0",
"pngjs": "^7.0.0",
+1
View File
@@ -63,6 +63,7 @@ const commonImports = [
const bullImports = [BullModule.forRoot(bull.config), BullModule.registerQueue(...bull.queues)];
// eslint-disable-next-line unicorn/no-top-level-side-effects
configureUserAgent();
export class BaseModule implements OnModuleInit, OnModuleDestroy {
+5 -2
View File
@@ -23,6 +23,7 @@ const handleError = (label: string, error: Error | any) => {
console.error(`${label} error: ${error}`);
};
// eslint-disable-next-line unicorn/no-exports-in-scripts
export class SqlLogger {
queries: string[] = [];
errors: Array<{ error: string | Error; query: string }> = [];
@@ -109,10 +110,12 @@ class SqlGenerator {
const instance = this.app.get<Repository>(Repository);
// normal repositories
data.push(...(await this.runTargets(instance, `${Repository.name}`)));
data.push(...(await this.runTargets(instance, Repository.name)));
// nested repositories
if (Repository.name === AccessRepository.name || Repository.name === SyncRepository.name) {
// probably a bug that this fails linting?
// eslint-disable-next-line unicorn/prefer-object-iterable-methods
for (const key of Object.keys(instance)) {
const subInstance = (instance as any)[key];
data.push(...(await this.runTargets(subInstance, `${Repository.name}.${key}`)));
@@ -127,7 +130,7 @@ class SqlGenerator {
for (const key of this.getPropertyNames(instance)) {
const target = instance[key];
if (!(typeof target === 'function')) {
if (typeof target !== 'function') {
continue;
}
@@ -36,7 +36,7 @@ export class ChangeMediaLocationCommand extends CommandRunner {
{},
);
const success = await this.service.migrateFilePaths({
const isSuccess = await this.service.migrateFilePaths({
oldValue,
newValue,
confirm: async ({ sourceFolder, targetFolder }) => {
@@ -65,7 +65,7 @@ export class ChangeMediaLocationCommand extends CommandRunner {
...
)`;
console.log(`\n ${success ? successMessage : 'No rows were updated'}\n`);
console.log(`\n ${isSuccess ? successMessage : 'No rows were updated'}\n`);
await this.showSamplePaths('after');
} catch (error) {
@@ -131,7 +131,7 @@ export class AssetMediaController {
const [_, reqSearch] = req.url.split('?');
const redirSearchParams = new URLSearchParams(reqSearch);
redirSearchParams.delete('size');
return res.redirect('original' + '?' + redirSearchParams.toString());
return res.redirect('original?' + redirSearchParams.toString());
}
const viewThumbnailRes = await this.service.viewThumbnail(auth, id, dto);
@@ -32,7 +32,9 @@ describe(DownloadController.name, () => {
it('should be an authenticated route', async () => {
const stream = new Readable({
read() {
// eslint-disable-next-line unicorn/no-this-outside-of-class
this.push('test');
// eslint-disable-next-line unicorn/no-this-outside-of-class
this.push(null);
},
});
@@ -65,12 +65,14 @@ export class MaintenanceController {
@GetLoginDetails() loginDetails: LoginDetails,
@Res({ passthrough: true }) res: Response,
): Promise<void> {
if (dto.action !== MaintenanceAction.End) {
const { jwt } = await this.service.startMaintenance(dto, auth.user.name);
return respondWithCookie(res, undefined, {
isSecure: loginDetails.isSecure,
values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }],
});
if (dto.action === MaintenanceAction.End) {
return;
}
const { jwt } = await this.service.startMaintenance(dto, auth.user.name);
return respondWithCookie(res, undefined, {
isSecure: loginDetails.isSecure,
values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }],
});
}
}
@@ -14,10 +14,10 @@ function validConfig() {
notifications: { smtp: { from: string; transport: { host: string } } };
server: { externalDomain: string };
};
config.oauth.mobileRedirectUri = config.oauth.mobileRedirectUri || 'https://example.com';
config.server.externalDomain = config.server.externalDomain || 'https://example.com';
config.notifications.smtp.from = config.notifications.smtp.from || 'noreply@example.com';
config.notifications.smtp.transport.host = config.notifications.smtp.transport.host || 'localhost';
config.oauth.mobileRedirectUri ||= 'https://example.com';
config.server.externalDomain ||= 'https://example.com';
config.notifications.smtp.from ||= 'noreply@example.com';
config.notifications.smtp.transport.host ||= 'localhost';
return config;
}
@@ -33,7 +33,7 @@ describe(TimelineController.name, () => {
expect(service.getTimeBuckets).toHaveBeenCalledWith(
undefined,
expect.objectContaining({
bbox: { west: 11.075_683, south: 49.416_711, east: 11.117_589, north: 49.454_875 },
bbox: { west: 11.075683, south: 49.416711, east: 11.117589, north: 49.454875 },
}),
);
});
+8 -8
View File
@@ -202,20 +202,20 @@ export class StorageCore {
let move = await this.moveRepository.getByEntity(entityId, pathType);
if (move) {
this.logger.log(`Attempting to finish incomplete move: ${move.oldPath} => ${move.newPath}`);
const oldPathExists = await this.storageRepository.checkFileExists(move.oldPath);
const newPathExists = await this.storageRepository.checkFileExists(move.newPath);
const newPathCheck = newPathExists ? move.newPath : null;
const actualPath = oldPathExists ? move.oldPath : newPathCheck;
const isOldPathExists = await this.storageRepository.checkFileExists(move.oldPath);
const isNewPathExists = await this.storageRepository.checkFileExists(move.newPath);
const newPathCheck = isNewPathExists ? move.newPath : null;
const actualPath = isOldPathExists ? move.oldPath : newPathCheck;
if (!actualPath) {
this.logger.warn('Unable to complete move. File does not exist at either location.');
return;
}
const fileAtNewLocation = actualPath === move.newPath;
this.logger.log(`Found file at ${fileAtNewLocation ? 'new' : 'old'} location`);
const isFileAtNewLocation = actualPath === move.newPath;
this.logger.log(`Found file at ${isFileAtNewLocation ? 'new' : 'old'} location`);
if (
fileAtNewLocation &&
isFileAtNewLocation &&
!(await this.verifyNewPathContentsMatchesExpected(move.oldPath, move.newPath, assetInfo))
) {
this.logger.fatal(
@@ -349,7 +349,7 @@ export class StorageCore {
}
static getNestedPath(folder: StorageFolder, ownerId: string, filename: string): string {
return join(this.getNestedFolder(folder, ownerId, filename), filename);
return join(StorageCore.getNestedFolder(folder, ownerId, filename), filename);
}
static getTempPathInDir(dir: string): string {
+3 -2
View File
@@ -54,9 +54,8 @@ function chunks<T>(collection: Array<T> | Set<T>, size: number): Array<Array<T>>
result.push(chunk);
}
return result;
} else {
return _.chunk(collection, size);
}
return _.chunk(collection, size);
}
/**
@@ -82,11 +81,13 @@ export function Chunked(
(Array.isArray(argument) && argument.length <= chunkSize) ||
(argument instanceof Set && argument.size <= chunkSize)
) {
// eslint-disable-next-line unicorn/no-this-outside-of-class
return originalMethod.apply(this, arguments_);
}
return Promise.all(
chunks(argument, chunkSize).map((chunk) => {
// eslint-disable-next-line unicorn/no-this-outside-of-class
return Reflect.apply(originalMethod, this, [
...arguments_.slice(0, parameterIndex),
chunk,
+1 -1
View File
@@ -175,7 +175,7 @@ const peopleFromFaces = (faces?: MaybeDehydrated<AssetFace>[]): PersonResponseDt
}
}
return [...peopleMap.values()];
return peopleMap.values().toArray();
};
const mapStack = (entity: { stack?: Stack | null }) => {
+1 -4
View File
@@ -92,10 +92,7 @@ export class LibraryResponseDto extends createZodDto(LibraryResponseSchema) {}
export class LibraryStatsResponseDto extends createZodDto(LibraryStatsResponseSchema) {}
export function mapLibrary(entity: Library): LibraryResponseDto {
let assetCount = 0;
if (entity.assets) {
assetCount = entity.assets.length;
}
const assetCount = entity.assets ? entity.assets.length : 0;
return {
id: entity.id,
ownerId: entity.ownerId,
+6 -5
View File
@@ -68,9 +68,9 @@ class Workers {
const { database } = new ConfigRepository().getEnv();
const kysely = new Kysely<DB>(getKyselyConfig(database.config));
let locked = false;
while (!locked) {
locked = await kysely.connection().execute(async (conn) => {
let isLocked = false;
while (!isLocked) {
isLocked = await kysely.connection().execute(async (conn) => {
const { rows } = await sql<{
pg_try_advisory_lock: boolean;
}>`SELECT pg_try_advisory_lock(${DatabaseLock.MaintenanceOperation})`.execute(conn);
@@ -110,6 +110,7 @@ class Workers {
});
kill = (signal) => void worker.kill(signal);
// eslint-disable-next-line unicorn/prefer-hoisting-branch-code
anyWorker = worker;
} else {
const worker = new Worker(workerFile);
@@ -151,9 +152,9 @@ class Workers {
if (exitCode !== 0) {
console.error(`${name} worker exited with code ${exitCode}`);
if (this.workers[ImmichWorker.Api] && name !== ImmichWorker.Api) {
if (Object.hasOwn(this.workers, ImmichWorker.Api) && name !== ImmichWorker.Api) {
console.error('Killing api process');
void this.workers[ImmichWorker.Api].kill('SIGTERM');
void this.workers[ImmichWorker.Api]!.kill('SIGTERM');
}
}
@@ -42,10 +42,12 @@ export class MaintenanceHealthRepository {
worker.on('error', (error) => reject(new Error(`Server health check failed, process threw: ${error}`)));
setTimeout(() => {
if (worker.exitCode === null) {
reject(new Error('Server health check failed, took too long to start.'));
worker.kill('SIGTERM');
if (worker.exitCode !== null) {
return;
}
reject(new Error('Server health check failed, took too long to start.'));
worker.kill('SIGTERM');
}, 180_000);
});
}
@@ -171,8 +171,8 @@ export class MaintenanceWorkerService {
const candidates = ['/data', '/usr/src/app/upload'];
for (const candidate of candidates) {
const exists = this.storageRepository.existsSync(candidate);
if (exists) {
const isExists = this.storageRepository.existsSync(candidate);
if (isExists) {
targets.push(candidate);
}
}
@@ -295,8 +295,8 @@ export class MaintenanceWorkerService {
}
async runRestoreDatabase(action: SetMaintenanceModeDto) {
const lock = await this.databaseRepository.tryLock(DatabaseLock.MaintenanceOperation);
if (!lock) {
const isLock = await this.databaseRepository.tryLock(DatabaseLock.MaintenanceOperation);
if (!isLock) {
return;
}
+4 -1
View File
@@ -130,7 +130,10 @@ class AlbumAccess {
.where('shared_link.albumId', 'in', [...albumIds])
.execute()
.then(
(sharedLinks) => new Set(sharedLinks.flatMap((sharedLink) => (sharedLink.albumId ? [sharedLink.albumId] : []))),
(sharedLinks) =>
new Set(
sharedLinks.filter((sharedLink) => sharedLink.albumId).map((sharedLink) => sharedLink.albumId),
) as Set<string>,
);
}
}
+1 -1
View File
@@ -314,7 +314,7 @@ export class AlbumRepository {
albumUsers: AlbumUserCreateDto[],
authUserId: string,
) {
if (!albumUsers.some((u) => u.role === AlbumUserRole.Owner)) {
if (albumUsers.every((u) => u.role !== AlbumUserRole.Owner)) {
throw new Error('Album must have an owner');
}
+6 -13
View File
@@ -192,9 +192,8 @@ export class DatabaseRepository {
) {
probes[indexName] = this.targetProbeCount(targetLists);
return this.reindexVectors(indexName, { lists: targetLists });
} else {
probes[indexName] = this.targetProbeCount(lists);
}
probes[indexName] = this.targetProbeCount(lists);
}),
);
break;
@@ -228,7 +227,7 @@ export class DatabaseRepository {
if (table === 'smart_search') {
await sql`ALTER TABLE ${sql.raw(table)} DROP CONSTRAINT IF EXISTS dim_size_constraint`.execute(tx);
}
if (!rows.some((row) => row.columnName === 'embedding')) {
if (rows.every((row) => row.columnName !== 'embedding')) {
this.logger.warn(`Column 'embedding' does not exist in table '${table}', truncating and adding column.`);
await sql`TRUNCATE TABLE ${sql.raw(table)}`.execute(tx);
await sql`ALTER TABLE ${sql.raw(table)} ADD COLUMN embedding real[] NOT NULL`.execute(tx);
@@ -349,11 +348,9 @@ export class DatabaseRepository {
private targetListCount(count: number) {
if (count < 128_000) {
return 1;
} else if (count < 2_048_000) {
return 1 << (32 - Math.clz32(count / 1000));
} else {
return 1 << (33 - Math.clz32(Math.sqrt(count)));
}
// eslint-disable-next-line unicorn/prefer-minimal-ternary
return count < 2_048_000 ? 1 << (32 - Math.clz32(count / 1000)) : 1 << (33 - Math.clz32(Math.sqrt(count)));
}
private targetProbeCount(lists: number) {
@@ -378,9 +375,7 @@ export class DatabaseRepository {
for (const result of results ?? []) {
if (result.status === 'Success') {
this.logger.log(`Migration "${result.migrationName}" succeeded`);
}
if (result.status === 'Error') {
} else if (result.status === 'Error') {
this.logger.warn(`Migration "${result.migrationName}" failed`);
}
}
@@ -485,9 +480,7 @@ export class DatabaseRepository {
for (const result of results ?? []) {
if (result.status === 'Success') {
this.logger.log(`Reverted migration "${result.migrationName}"`);
}
if (result.status === 'Error') {
} else if (result.status === 'Error') {
this.logger.warn(`Failed to revert migration "${result.migrationName}"`);
}
}
+2 -2
View File
@@ -213,11 +213,11 @@ export class EventRepository {
private addHandler<T extends EmitEvent>(item: Item<T>): void {
const event = item.event;
if (!this.emitHandlers[event]) {
if (!Object.hasOwn(this.emitHandlers, event)) {
this.emitHandlers[event] = [];
}
this.emitHandlers[event].push(item);
this.emitHandlers[event]!.push(item);
}
emit<T extends EmitEvent>(event: T, ...args: ArgsOf<T>): Promise<void> {
+13 -11
View File
@@ -54,11 +54,11 @@ export class JobRepository {
const label = `${Service.name}.${handler.name}`;
// one handler per job
if (this.handlers[jobName]) {
if (Object.hasOwn(this.handlers, jobName)) {
const jobKey = getKeyByValue(JobName, jobName);
const errorMessage = `Failed to add job handler for ${label}`;
this.logger.error(
`${errorMessage}. JobName.${jobKey} is already handled by ${this.handlers[jobName].label}.`,
`${errorMessage}. JobName.${jobKey} is already handled by ${this.handlers[jobName]!.label}.`,
);
throw new ImmichStartupError(errorMessage);
}
@@ -104,24 +104,26 @@ export class JobRepository {
}
teardown() {
if (this.workerWatcher) {
clearInterval(this.workerWatcher);
this.workerWatcher = undefined;
if (!this.workerWatcher) {
return;
}
clearInterval(this.workerWatcher);
this.workerWatcher = undefined;
}
private async checkWorkers() {
let present: boolean;
let isPresent: boolean;
try {
const suffix = `:w:${ImmichWorker.Microservices}`;
const workers = await this.getQueue(QueueName.BackgroundTask).getWorkers();
present = workers.some((worker) => worker.rawname?.endsWith(suffix));
isPresent = workers.some((worker) => worker.rawname?.endsWith(suffix));
} catch {
return;
}
if (this.microservicesPresent !== present) {
if (present) {
if (this.microservicesPresent !== isPresent) {
if (isPresent) {
this.logger.log('Microservices worker connected.');
} else {
this.logger.warn(
@@ -129,7 +131,7 @@ export class JobRepository {
);
}
}
this.microservicesPresent = present;
this.microservicesPresent = isPresent;
}
async run({ name, data }: JobItem) {
@@ -212,7 +214,7 @@ export class JobRepository {
// need to use add() instead of addBulk() for jobId/deduplication to take effect
promises.push(this.getQueue(queueName).add(item.name, item.data, job.options));
} else {
itemsByQueue[queueName] = itemsByQueue[queueName] || [];
itemsByQueue[queueName] ||= [];
itemsByQueue[queueName].push(job);
}
}
@@ -26,7 +26,7 @@ describe(LoggingRepository.name, () => {
const logger = new MyConsoleLogger(clsMock, { color: true });
expect(logger.formatContext('context')).toBe('\u001B[33m[Api:context]\u001B[39m ');
expect(logger.formatContext('context')).toBe('\u{1B}[33m[Api:context]\u{1B}[39m ');
});
it('should not use colors when color is false', () => {
@@ -67,7 +67,7 @@ export class MyConsoleLogger extends ConsoleLogger {
};
private withColor(text: string, color: LogColor) {
return this.isColorEnabled ? `\u001B[${color}m${text}\u001B[39m` : text;
return this.isColorEnabled ? `\u{1B}[${color}m${text}\u{1B}[39m` : text;
}
}
@@ -80,17 +80,17 @@ export class LoggingRepository {
@Inject(ClsService) cls: ClsService | undefined,
@Inject(ConfigRepository) configRepository: ConfigRepository | undefined,
) {
let noColor = false;
let isNoColor = false;
let logFormat = LogFormat.Console;
if (configRepository) {
const env = configRepository.getEnv();
noColor = env.noColor;
isNoColor = env.noColor;
logFormat = env.logFormat ?? logFormat;
}
this.logger = new MyConsoleLogger(cls, {
context: LoggingRepository.name,
json: logFormat === LogFormat.Json,
color: !noColor,
color: !isNoColor,
});
}
@@ -130,19 +130,19 @@ export class MachineLearningRepository {
}
private async check(url: string) {
let healthy = false;
let isHealthy = false;
try {
const response = await fetch(new URL('ping', url), {
signal: AbortSignal.timeout(this.config.availabilityChecks.timeout),
});
if (response.ok) {
healthy = true;
isHealthy = true;
}
} catch {
// nothing to do here
}
this.setHealthy(url, healthy);
this.setHealthy(url, isHealthy);
}
private setHealthy(url: string, healthy: boolean) {
+3 -2
View File
@@ -291,8 +291,8 @@ export class MapRepository {
id: Number.parseInt(lineSplit[0]),
name: lineSplit[1],
alternateNames: lineSplit[3],
latitude: Number.parseFloat(lineSplit[4]),
longitude: Number.parseFloat(lineSplit[5]),
latitude: Number(lineSplit[4]),
longitude: Number(lineSplit[5]),
countryCode: lineSplit[8],
admin1Code: lineSplit[10],
admin2Code: lineSplit[11],
@@ -308,6 +308,7 @@ export class MapRepository {
.insertInto('geodata_places')
.values(bufferGeodata)
.execute()
.then(() => {
count += curLength;
if (count % 10_000 === 0) {
+3 -3
View File
@@ -46,9 +46,6 @@ const probe = (input: string, options: string[]): Promise<FfprobeData> =>
const execFile = promisify(execFileCb);
sharp.concurrency(0);
sharp.cache({ files: 0 });
const pascalCase = (str: string) => _.upperFirst(_.camelCase(str.toLowerCase()));
type ProgressEvent = {
@@ -69,6 +66,8 @@ export type ExtractResult = {
export class MediaRepository {
constructor(private logger: LoggingRepository) {
this.logger.setContext(MediaRepository.name);
sharp.concurrency(0);
sharp.cache({ files: 0 });
}
/**
@@ -427,6 +426,7 @@ export class MediaRepository {
}
private parseFloat(value: string | number | undefined): number {
// eslint-disable-next-line unicorn/prefer-number-coercion
return Number.parseFloat(value as string) || 0;
}
@@ -108,6 +108,7 @@ export class MetadataRepository {
readTags(path: string): Promise<ImmichTags> {
const options: ReadTaskOptions | undefined = mimeTypes.isVideo(path) ? { readArgs: ['-ee'] } : undefined;
return this.exiftool.read(path, options).catch((error) => {
this.logger.warn(`Error reading exif data (${path}): ${error}\n${error?.stack}`);
return {};
+2 -2
View File
@@ -68,7 +68,7 @@ export class OAuthRepository {
params.code_challenge_method = 'S256';
}
const url = buildAuthorizationUrl(client, params).toString();
const url = buildAuthorizationUrl(client, params).href;
return { url, state, codeVerifier };
}
@@ -173,7 +173,7 @@ export class OAuthRepository {
// Validate specific Logout Token claims (RFC 8963):
// "events" claim must exist and contain the backchannel-logout event
const events = payload.events as Record<string, any> | undefined;
if (!events || !events['http://schemas.openid.net/event/backchannel-logout']) {
if (!events || !events['https://schemas.openid.net/event/backchannel-logout']) {
throw new Error('Missing backchannel-logout event claim');
}
@@ -7,7 +7,7 @@ export class ProcessRepository {
spawn = spawn;
spawnDuplexStream(command: string, args?: readonly string[], options?: SpawnOptionsWithoutStdio): Duplex {
let stdinClosed = false;
let isStdinClosed = false;
let drainCallback: undefined | (() => void);
const process = this.spawn(command, args, options);
@@ -15,7 +15,7 @@ export class ProcessRepository {
// duplex -> stdin
write(chunk, encoding, callback) {
// drain the input if process dies
if (stdinClosed) {
if (isStdinClosed) {
return callback();
}
@@ -36,7 +36,7 @@ export class ProcessRepository {
},
final(callback) {
if (stdinClosed) {
if (isStdinClosed) {
callback();
} else {
process.stdin.end(callback);
@@ -55,19 +55,19 @@ export class ProcessRepository {
duplex.on('resume', () => process.stdout.resume());
// end handling
let stdoutClosed = false;
let isStdoutClosed = false;
function close(error?: Error) {
stdinClosed = true;
isStdinClosed = true;
if (error) {
duplex.destroy(error);
} else if (stdoutClosed && typeof process.exitCode === 'number') {
} else if (isStdoutClosed && typeof process.exitCode === 'number') {
duplex.push(null);
}
}
process.stdout.on('close', () => {
stdoutClosed = true;
isStdoutClosed = true;
close();
});
@@ -35,7 +35,7 @@ const exec = promisify(execCallback);
const maybeFirstLine = async (command: string): Promise<string> => {
try {
const { stdout } = await exec(command);
return stdout.trim().split('\n')[0] || '';
return stdout.trim().split('\n', 1)[0] || '';
} catch {
return '';
}
@@ -111,6 +111,7 @@ export class ServerInfoRepository {
const lockfile: BuildLockfile | undefined = await readFile(resourcePaths.lockFile)
.then((buffer) => JSON.parse(buffer.toString()))
.catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`));
const [nodejsVersion, ffmpegVersion, magickVersion, exiftoolVersion] = await Promise.all([
@@ -95,10 +95,12 @@ export const bootstrapTelemetry = (port: number) => {
};
export const teardownTelemetry = async () => {
if (instance) {
await instance.shutdown();
instance = undefined;
if (!instance) {
return;
}
await instance.shutdown();
instance = undefined;
};
@Injectable()
@@ -153,7 +155,7 @@ export class TelemetryRepository {
}
const method = descriptor.value;
const propertyName = snakeCase(String(propName));
const propertyName = snakeCase(propName);
const metricName = `${snakeCase(className).replaceAll(/_(?=(repository)|(controller)|(provider)|(service)|(module))/g, '.')}.${propertyName}.duration`;
const histogram = this.metricService.getHistogram(metricName, {
@@ -165,6 +167,7 @@ export class TelemetryRepository {
descriptor.value = function (...args: any[]) {
const start = performance.now();
// eslint-disable-next-line unicorn/no-this-outside-of-class
const result = method.apply(this, args);
void Promise.resolve(result)
+1 -1
View File
@@ -46,7 +46,7 @@ export class UserRepository {
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BOOLEAN] })
get(userId: string, options: UserFindOptions) {
options = options || {};
options ||= {};
return this.db
.selectFrom('user')
+3 -3
View File
@@ -44,7 +44,7 @@ export class ActivityService extends BaseService {
};
let activity: Activity | undefined;
let duplicate = false;
let isDuplicate = false;
if (dto.type === ReactionType.LIKE) {
delete dto.comment;
@@ -54,7 +54,7 @@ export class ActivityService extends BaseService {
assetId: dto.assetId ?? null,
isLiked: true,
});
duplicate = !!activity;
isDuplicate = !!activity;
}
if (!activity) {
@@ -65,7 +65,7 @@ export class ActivityService extends BaseService {
});
}
return { duplicate, value: mapActivity(activity) };
return { duplicate: isDuplicate, value: mapActivity(activity) };
}
async delete(auth: AuthDto, id: string): Promise<void> {
+3 -3
View File
@@ -226,7 +226,7 @@ export class AlbumService extends BaseService {
const events: { id: string; recipients: string[] }[] = [];
for (const albumId of allowedAlbumIds) {
const existingAssetIds = await this.albumRepository.getAssetIds(albumId, [...allowedAssetIds]);
const notPresentAssetIds = [...allowedAssetIds].filter((id) => !existingAssetIds.has(id));
const notPresentAssetIds = [...allowedAssetIds.difference(existingAssetIds)];
if (notPresentAssetIds.length === 0) {
continue;
}
@@ -288,7 +288,7 @@ export class AlbumService extends BaseService {
throw new BadRequestException('Cannot add another owner');
}
const exists = album.albumUsers.find(({ user: { id } }) => id === userId);
const exists = album.albumUsers.some(({ user: { id } }) => id === userId);
if (exists) {
continue;
}
@@ -303,7 +303,7 @@ export class AlbumService extends BaseService {
await this.eventRepository.emit('AlbumInvite', { id, userId, senderName: auth.user.name });
}
return this.findOrFail(id, auth.user.id, { withAssets: true }).then(mapAlbum);
return mapAlbum(await this.findOrFail(id, auth.user.id, { withAssets: true }));
}
async removeUser(auth: AuthDto, id: string, userId: string | 'me'): Promise<void> {
+1 -1
View File
@@ -29,7 +29,7 @@ export const render = (index: string, meta: OpenGraphTags) => {
${imageUrl ? `<meta name="twitter:image" content="${imageUrl}" />` : ''}`;
return index.replace('<!-- metadata:tags -->', tags);
return index.replace('<!-- metadata:tags -->', () => tags);
};
@Injectable()
@@ -163,8 +163,8 @@ const assetEntity = Object.freeze({
duration: null,
files: [] as AssetFile[],
exifInfo: {
latitude: 49.533_547,
longitude: 10.703_075,
latitude: 49.533547,
longitude: 10.703075,
},
livePhotoVideoId: null,
} as MapAsset);
+10 -8
View File
@@ -280,15 +280,17 @@ export class AssetService extends BaseService {
let chunk: Array<{ id: string; isOffline: boolean }> = [];
const queueChunk = async () => {
if (chunk.length > 0) {
await this.jobRepository.queueAll(
chunk.map(({ id, isOffline }) => ({
name: JobName.AssetDelete,
data: { id, deleteOnDisk: !isOffline },
})),
);
chunk = [];
if (chunk.length === 0) {
return;
}
await this.jobRepository.queueAll(
chunk.map(({ id, isOffline }) => ({
name: JobName.AssetDelete,
data: { id, deleteOnDisk: !isOffline },
})),
);
chunk = [];
};
const assets = this.assetJobRepository.streamForDeletedJob(trashedBefore);
+3 -3
View File
@@ -653,13 +653,13 @@ describe(AuthService.name, () => {
describe('getMobileRedirect', () => {
it('should pass along the query params', () => {
expect(sut.getMobileRedirect('http://immich.app?code=123&state=456')).toEqual(
expect(sut.getMobileRedirect('https://immich.app?code=123&state=456')).toEqual(
'app.immich:///oauth-callback?code=123&state=456',
);
});
it('should work if called without query params', () => {
expect(sut.getMobileRedirect('http://immich.app')).toEqual('app.immich:///oauth-callback?');
expect(sut.getMobileRedirect('https://immich.app')).toEqual('app.immich:///oauth-callback?');
});
});
@@ -979,7 +979,7 @@ describe(AuthService.name, () => {
});
expect(mocks.oauth.getProfilePicture).toHaveBeenCalledWith(profile.picture);
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
Buffer.from(pictureBytes.buffer),
Buffer.from(pictureBytes.buffer, pictureBytes.byteOffset, pictureBytes.byteLength),
expect.objectContaining({ format: 'webp', processInvalidImages: false }),
expect.stringContaining(`/data/profile/${user.id}/${fileId}.webp`),
);
+6 -6
View File
@@ -65,9 +65,9 @@ export class AuthService extends BaseService {
const user = await this.userRepository.getByEmail(dto.email, { withPassword: true });
// Always run bcrypt so response time is constant regardless of whether the email
// is registered, preventing timing-based user enumeration.
const authenticated = this.cryptoRepository.compareBcrypt(dto.password, user?.password ?? LOGIN_DUMMY_HASH);
const isAuthenticated = this.cryptoRepository.compareBcrypt(dto.password, user?.password ?? LOGIN_DUMMY_HASH);
if (!user || !user.password || !authenticated) {
if (!user || !user.password || !isAuthenticated) {
this.logger.warn(`Failed login attempt for user ${dto.email} from ip address ${details.clientIp}`);
throw new UnauthorizedException('Incorrect email or password');
}
@@ -124,8 +124,8 @@ export class AuthService extends BaseService {
async changePassword(auth: AuthDto, dto: ChangePasswordDto): Promise<UserAdminResponseDto> {
const { password, newPassword } = dto;
const user = await this.userRepository.getForChangePassword(auth.user.id);
const valid = this.validateSecret(password, user.password);
if (!valid) {
const isValid = this.validateSecret(password, user.password);
if (!isValid) {
throw new BadRequestException('Wrong password');
}
@@ -271,7 +271,7 @@ export class AuthService extends BaseService {
}
getMobileRedirect(url: string) {
return `${MOBILE_REDIRECT}?${url.split('?')[1] || ''}`;
return `${MOBILE_REDIRECT}?${url.split('?', 2)[1] || ''}`;
}
async authorize(dto: OAuthConfigDto) {
@@ -625,7 +625,7 @@ export class AuthService extends BaseService {
url: string,
) {
if (mobileOverrideEnabled && mobileRedirectUri) {
return url.replace(/app\.immich:\/+oauth-callback/, mobileRedirectUri);
return url.replace(/app\.immich:\/+oauth-callback/, () => mobileRedirectUri);
}
return url;
}
+1 -1
View File
@@ -247,7 +247,7 @@ export class BaseService {
ctx.workflowRepository,
);
service.logger.setContext(this.name);
service.logger.setContext(BaseService.name);
return service as T;
}
+7 -12
View File
@@ -45,7 +45,6 @@ export class CliService extends BaseService {
if (!filesSet.has(name) && rowsSet.has(name)) {
migrations.push({ name, status: 'deleted' });
continue;
}
}
@@ -87,9 +86,9 @@ export class CliService extends BaseService {
}
async disableMaintenanceMode(): Promise<{ alreadyDisabled: boolean }> {
const currentState = await this.systemMetadataRepository
.get(SystemMetadataKey.MaintenanceMode)
.then((state) => state ?? { isMaintenanceMode: false as const });
const currentState = (await this.systemMetadataRepository.get(SystemMetadataKey.MaintenanceMode)) ?? {
isMaintenanceMode: false as const,
};
if (!currentState.isMaintenanceMode) {
return {
@@ -114,9 +113,9 @@ export class CliService extends BaseService {
username: 'cli-admin',
};
const state = await this.systemMetadataRepository
.get(SystemMetadataKey.MaintenanceMode)
.then((state) => state ?? { isMaintenanceMode: false as const });
const state = (await this.systemMetadataRepository.get(SystemMetadataKey.MaintenanceMode)) ?? {
isMaintenanceMode: false as const,
};
if (state.isMaintenanceMode) {
return {
@@ -182,11 +181,7 @@ export class CliService extends BaseService {
this.userRepository.getFileSamples(),
]);
const paths = [];
for (const person of people) {
paths.push(person.thumbnailPath);
}
const paths = Array.from(people, (person) => person.thumbnailPath);
for (const user of users) {
paths.push(user.profileImagePath);
+14 -10
View File
@@ -143,7 +143,7 @@ export class DatabaseBackupService {
databaseUsername = parsedUrl.username || parsedUrl.searchParams.get('user');
url = parsedUrl.toString();
url = parsedUrl.href;
}
// assume typical values if we can't parse URL or not present
@@ -214,6 +214,7 @@ export class DatabaseBackupService {
bin: `/usr/lib/postgresql/${databaseMajorVersion}/bin/${bin}`,
args,
databaseUsername,
// eslint-disable-next-line unicorn/prefer-minimal-ternary
databasePassword: isUrlConnection ? new URL(databaseConfig.url).password : databaseConfig.password,
databaseVersion,
databaseMajorVersion,
@@ -228,7 +229,7 @@ export class DatabaseBackupService {
this.logger.log(`Database Backup Starting. Database Version: ${databaseMajorVersion}`);
const filename = `${filenamePrefix}immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz`;
const filename = `${filenamePrefix}immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ', 1)[0]}.sql.gz`;
const backupFilePath = path.join(StorageCore.getBaseFolder(StorageFolder.Backups), filename);
const temporaryFilePath = `${backupFilePath}.tmp`;
@@ -249,6 +250,7 @@ export class DatabaseBackupService {
this.logger.error(`Database Backup Failure: ${error}`);
await this.storageRepository
.unlink(temporaryFilePath)
.catch((error) => this.logger.error(`Failed to delete failed backup file: ${error}`));
throw error;
}
@@ -354,7 +356,7 @@ export class DatabaseBackupService {
): Promise<void> {
this.logger.debug(`Database Restore Started`);
let complete = false;
let isComplete = false;
try {
if (!isValidDatabaseBackupName(filename)) {
throw new Error('Invalid backup file format!');
@@ -399,7 +401,7 @@ export class DatabaseBackupService {
});
const [progressSource, progressSink] = createSqlProgressStreams((progress) => {
if (complete) {
if (isComplete) {
return;
}
@@ -437,7 +439,7 @@ export class DatabaseBackupService {
});
const [progressSource, progressSink] = createSqlProgressStreams((progress) => {
if (complete) {
if (isComplete) {
return;
}
@@ -453,7 +455,7 @@ export class DatabaseBackupService {
this.logger.error(`Database Restore Failure: ${error}`);
throw error;
} finally {
complete = true;
isComplete = true;
}
this.logger.log(`Database Restore Success`);
@@ -507,7 +509,7 @@ function createSqlProgressStreams(cb: (progress: number) => void) {
const STDIN_START_MARKER = new TextEncoder().encode('FROM stdin');
const STDIN_END_MARKER = new TextEncoder().encode(String.raw`\.`);
let readingStdin = false;
let isReadingStdin = false;
let sequenceIdx = 0;
let linesSent = 0;
@@ -532,19 +534,19 @@ function createSqlProgressStreams(cb: (progress: number) => void) {
const source = new PassThrough({
transform(chunk, _encoding, callback) {
for (const byte of chunk) {
if (!readingStdin && byte === 10 && lastByte !== 10) {
if (!isReadingStdin && byte === 10 && lastByte !== 10) {
linesSent += 1;
}
lastByte = byte;
const sequence = readingStdin ? STDIN_END_MARKER : STDIN_START_MARKER;
const sequence = isReadingStdin ? STDIN_END_MARKER : STDIN_START_MARKER;
if (sequence[sequenceIdx] === byte) {
sequenceIdx += 1;
if (sequence.length === sequenceIdx) {
sequenceIdx = 0;
readingStdin = !readingStdin;
isReadingStdin = !isReadingStdin;
}
} else {
sequenceIdx = 0;
@@ -552,6 +554,7 @@ function createSqlProgressStreams(cb: (progress: number) => void) {
}
cbDebounced();
// eslint-disable-next-line unicorn/no-this-outside-of-class
this.push(chunk);
callback();
},
@@ -633,6 +636,7 @@ function createSqlOwnerTransformStream(databaseUsername: string) {
}
}
// eslint-disable-next-line unicorn/no-this-outside-of-class
this.push(result);
callback();
},
+3 -3
View File
@@ -220,15 +220,15 @@ export class DuplicateService extends BaseService {
if (idsToTrash.length > 0) {
// TODO: this is duplicated with AssetService.deleteAssets
const { trash } = await this.getConfig({ withCache: true });
const force = !trash.enabled;
const isForce = !trash.enabled;
await this.assetRepository.updateAll(idsToTrash, {
deletedAt: new Date(),
status: force ? AssetStatus.Deleted : AssetStatus.Trashed,
status: isForce ? AssetStatus.Deleted : AssetStatus.Trashed,
duplicateId: null,
});
await this.eventRepository.emit(force ? 'AssetDeleteAll' : 'AssetTrashAll', {
await this.eventRepository.emit(isForce ? 'AssetDeleteAll' : 'AssetTrashAll', {
assetIds: idsToTrash,
userId: auth.user.id,
});
+5 -3
View File
@@ -204,10 +204,12 @@ describe(HlsService.name, () => {
mocks.videoStream.getForMainPlaylist.mockResolvedValue(asset);
mocks.crypto.randomUUID.mockReturnValue(sessionId);
mocks.websocket.serverSend.mockImplementation((event, ...rest) => {
if (event === 'HlsSessionRequest') {
const { sessionId: id } = rest[0] as { sessionId: string };
queueMicrotask(() => sut.onSessionResult({ sessionId: id }));
if (event !== 'HlsSessionRequest') {
return;
}
const { sessionId: id } = rest[0] as { sessionId: string };
queueMicrotask(() => sut.onSessionResult({ sessionId: id }));
});
};
+24 -18
View File
@@ -312,12 +312,14 @@ export class IntegrityService extends BaseService {
this.logger.log(`Processing batch of ${items.length} reports to check if they are out of date.`);
const results = await Promise.all(
items.map(({ reportId, path }) =>
this.storageRepository
.stat(path)
.then(() => void 0)
.catch(() => reportId),
),
items.map(async ({ reportId, path }) => {
try {
await this.storageRepository.stat(path);
return;
} catch {
return reportId;
}
}),
);
const reportIds = results.filter(Boolean) as string[];
@@ -383,12 +385,14 @@ export class IntegrityService extends BaseService {
this.logger.log(`Processing batch of ${items.length} files to check if they are missing.`);
const results = await Promise.all(
items.map((item) =>
this.storageRepository
.stat(item.path)
.then(() => ({ ...item, exists: true }))
.catch(() => ({ ...item, exists: false })),
),
items.map(async (item) => {
try {
await this.storageRepository.stat(item.path);
return { ...item, exists: true };
} catch {
return { ...item, exists: false };
}
}),
);
const outdatedReports = results
@@ -420,12 +424,14 @@ export class IntegrityService extends BaseService {
this.logger.log(`Processing batch of ${paths.length} reports to check if they are out of date.`);
const results = await Promise.all(
paths.map(({ reportId, path }) =>
this.storageRepository
.stat(path)
.then(() => reportId)
.catch(() => void 0),
),
paths.map(async ({ reportId, path }) => {
try {
await this.storageRepository.stat(path);
return reportId;
} catch {
return;
}
}),
);
const reportIds = results.filter(Boolean) as string[];
+52 -42
View File
@@ -162,10 +162,12 @@ export class LibraryService extends BaseService {
}
async unwatch(id: string) {
if (this.watchers[id]) {
await this.watchers[id]();
delete this.watchers[id];
if (!Object.hasOwn(this.watchers, id)) {
return;
}
await this.watchers[id]();
delete this.watchers[id];
}
@OnEvent({ name: 'AppShutdown' })
@@ -252,18 +254,22 @@ export class LibraryService extends BaseService {
if (!library) {
this.logger.debug(`Library ${job.libraryId} not found, skipping file import`);
return JobStatus.Failed;
} else if (library.deletedAt) {
}
if (library.deletedAt) {
this.logger.debug(`Library ${job.libraryId} is deleted, won't import assets into it`);
return JobStatus.Failed;
}
const assetImports: Insertable<AssetTable>[] = [];
await Promise.all(
job.paths.map((path) =>
this.processEntity(path, library.ownerId, job.libraryId)
.then((asset) => assetImports.push(asset))
.catch((error: any) => this.logger.error(`Error processing ${path} for library ${job.libraryId}: ${error}`)),
),
job.paths.map(async (path) => {
try {
const asset = await this.processEntity(path, library.ownerId, job.libraryId);
assetImports.push(asset);
} catch (error) {
this.logger.error(`Error processing ${path} for library ${job.libraryId}: ${error}`);
}
}),
);
const assetIds = await this.assetRepository.createAll(assetImports);
@@ -310,9 +316,9 @@ export class LibraryService extends BaseService {
return validation;
}
const access = await this.storageRepository.checkFileExists(importPath, R_OK);
const isAccess = await this.storageRepository.checkFileExists(importPath, R_OK);
if (!access) {
if (!isAccess) {
validation.message = 'Lacking read permission for folder';
return validation;
}
@@ -363,18 +369,20 @@ export class LibraryService extends BaseService {
await this.assetRepository.updateByLibraryId(libraryId, { deletedAt: new Date() });
let assetsFound = false;
let isAssetsFound = false;
let chunk: string[] = [];
const queueChunk = async () => {
if (chunk.length > 0) {
assetsFound = true;
this.logger.debug(`Queueing deletion of ${chunk.length} asset(s) in library ${libraryId}`);
await this.jobRepository.queueAll(
chunk.map((id) => ({ name: JobName.AssetDelete, data: { id, deleteOnDisk: false } })),
);
chunk = [];
if (chunk.length === 0) {
return;
}
isAssetsFound = true;
this.logger.debug(`Queueing deletion of ${chunk.length} asset(s) in library ${libraryId}`);
await this.jobRepository.queueAll(
chunk.map((id) => ({ name: JobName.AssetDelete, data: { id, deleteOnDisk: false } })),
);
chunk = [];
};
this.logger.debug(`Will delete all assets in library ${libraryId}`);
@@ -389,7 +397,7 @@ export class LibraryService extends BaseService {
await queueChunk();
if (!assetsFound) {
if (!isAssetsFound) {
this.logger.log(`Deleting library ${libraryId}`);
await this.libraryRepository.delete(libraryId);
}
@@ -514,7 +522,7 @@ export class LibraryService extends BaseService {
break;
}
case AssetSyncResult.CHECK_OFFLINE: {
const isInImportPath = job.importPaths.find((path) => asset.originalPath.startsWith(path));
const isInImportPath = job.importPaths.some((path) => asset.originalPath.startsWith(path));
if (!isInImportPath) {
this.logger.verbose(
@@ -736,28 +744,30 @@ export class LibraryService extends BaseService {
let count = 0;
const queueChunk = async () => {
if (chunk.length > 0) {
count += chunk.length;
await this.jobRepository.queue({
name: JobName.LibrarySyncAssets,
data: {
libraryId: library.id,
importPaths: library.importPaths,
exclusionPatterns: library.exclusionPatterns,
assetIds: chunk.map((id) => id),
progressCounter: count,
totalAssets: assetCount,
},
});
chunk = [];
const completePercentage = ((100 * count) / assetCount).toFixed(1);
this.logger.log(
`Queued check of ${count} of ${assetCount} (${completePercentage} %) existing asset(s) so far in library ${library.id}`,
);
if (chunk.length === 0) {
return;
}
count += chunk.length;
await this.jobRepository.queue({
name: JobName.LibrarySyncAssets,
data: {
libraryId: library.id,
importPaths: library.importPaths,
exclusionPatterns: library.exclusionPatterns,
assetIds: chunk.map((id) => id),
progressCounter: count,
totalAssets: assetCount,
},
});
chunk = [];
const completePercentage = ((100 * count) / assetCount).toFixed(1);
this.logger.log(
`Queued check of ${count} of ${assetCount} (${completePercentage} %) existing asset(s) so far in library ${library.id}`,
);
};
this.logger.log(`Scanning library ${library.id} for assets missing from disk...`);
@@ -26,6 +26,7 @@ export class MaintenanceService extends BaseService {
getMaintenanceMode(): Promise<MaintenanceModeState> {
return this.systemMetadataRepository
.get(SystemMetadataKey.MaintenanceMode)
.then((state) => state ?? { isMaintenanceMode: false });
}
+26 -22
View File
@@ -76,8 +76,11 @@ export class MediaService extends BaseService {
jobs = [];
};
const fullsizeEnabled = config.image.fullsize.enabled;
for await (const asset of this.assetJobRepository.streamForThumbnailJob({ force, fullsizeEnabled })) {
const isFullsizeEnabled = config.image.fullsize.enabled;
for await (const asset of this.assetJobRepository.streamForThumbnailJob({
force,
fullsizeEnabled: isFullsizeEnabled,
})) {
if (force || !asset.isEdited) {
jobs.push({ name: JobName.AssetGenerateThumbnails, data: { id: asset.id } });
}
@@ -272,13 +275,14 @@ export class MediaService extends BaseService {
}
private async extractOriginalImage(asset: ThumbnailAsset, image: SystemConfig['image'], useEdits = false) {
const extractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName);
const extracted = extractEmbedded ? await this.extractImage(asset.originalPath, image.preview.size) : null;
const generateFullsize =
const isExtractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName);
const extracted = isExtractEmbedded ? await this.extractImage(asset.originalPath, image.preview.size) : null;
const isGenerateFullsize =
((image.fullsize.enabled || asset.exifInfo.projectionType === 'EQUIRECTANGULAR') &&
!mimeTypes.isWebSupportedImage(asset.originalPath)) ||
useEdits;
const convertFullsize = generateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`));
const isConvertFullsize =
isGenerateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`));
const thumbSource = extracted ? extracted.buffer : asset.originalPath;
const { data, info, colorspace } = await this.decodeImage(
@@ -286,7 +290,7 @@ export class MediaService extends BaseService {
// only specify orientation to extracted images which don't have EXIF orientation data
// or it can double rotate the image
extracted ? asset.exifInfo : { ...asset.exifInfo, orientation: null },
convertFullsize ? undefined : image.preview.size,
isConvertFullsize ? undefined : image.preview.size,
);
let isTransparent = false;
@@ -299,8 +303,8 @@ export class MediaService extends BaseService {
data,
info,
colorspace,
convertFullsize,
generateFullsize,
convertFullsize: isConvertFullsize,
generateFullsize: isGenerateFullsize,
isTransparent,
};
}
@@ -620,20 +624,20 @@ export class MediaService extends BaseService {
return JobStatus.Failed;
}
let partialFallbackSuccess = false;
let isPartialFallbackSuccess = false;
if (ffmpeg.accelDecode) {
try {
this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()}-accelerated encoding and software decoding`);
ffmpeg = { ...ffmpeg, accelDecode: false };
const command = BaseConfig.create(ffmpeg, this.videoInterfaces).getCommand(target, videoStream, audioStream);
await this.mediaRepository.transcode(input, output, command);
partialFallbackSuccess = true;
isPartialFallbackSuccess = true;
} catch (error: any) {
this.logger.error(`Error occurred during transcoding: ${error.message}`);
}
}
if (!partialFallbackSuccess) {
if (!isPartialFallbackSuccess) {
this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled`);
ffmpeg = { ...ffmpeg, accel: TranscodeHardwareAcceleration.Disabled };
const command = BaseConfig.create(ffmpeg, this.videoInterfaces).getCommand(target, videoStream, audioStream);
@@ -700,9 +704,9 @@ export class MediaService extends BaseService {
}
private isVideoTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream: VideoStreamInfo): boolean {
const scalingEnabled = ffmpegConfig.targetResolution !== 'original';
const isScalingEnabled = ffmpegConfig.targetResolution !== 'original';
const targetRes = Number.parseInt(ffmpegConfig.targetResolution);
const isLargerThanTargetRes = scalingEnabled && Math.min(stream.height, stream.width) > targetRes;
const isLargerThanTargetRes = isScalingEnabled && Math.min(stream.height, stream.width) > targetRes;
const maxBitrate = this.parseBitrateToBps(ffmpegConfig.maxBitrate);
const isLargerThanTargetBitrate = maxBitrate > 0 && stream.bitrate > maxBitrate;
@@ -757,13 +761,13 @@ export class MediaService extends BaseService {
}): boolean {
if (colorspace || profileDescription) {
return [colorspace, profileDescription].some((s) => s?.toLowerCase().includes('srgb'));
} else if (bitsPerSample) {
}
if (bitsPerSample) {
// assume sRGB for 8-bit images with no color profile or colorspace metadata
return bitsPerSample === 8;
} else {
// assume sRGB for images with no relevant metadata
return true;
}
// assume sRGB for images with no relevant metadata
return true;
}
private parseBitrateToBps(bitrateString: string) {
@@ -776,11 +780,11 @@ export class MediaService extends BaseService {
if (bitrateString.toLowerCase().endsWith('k')) {
return bitrateValue * 1000; // Kilobits per second to bits per second
} else if (bitrateString.toLowerCase().endsWith('m')) {
return bitrateValue * 1_000_000; // Megabits per second to bits per second
} else {
return bitrateValue;
}
if (bitrateString.toLowerCase().endsWith('m')) {
return bitrateValue * 1_000_000; // Megabits per second to bits per second
}
return bitrateValue;
}
private async shouldUseExtractedImage(extractedPathOrBuffer: string | Buffer, targetSize: number) {
+2 -2
View File
@@ -136,7 +136,7 @@ export class MemoryService extends BaseService {
const repos = { access: this.accessRepository, bulk: this.memoryRepository };
const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids });
const hasSuccess = results.find(({ success }) => success);
const hasSuccess = results.some(({ success }) => success);
if (hasSuccess) {
await this.memoryRepository.update(id, { updatedAt: new Date() });
}
@@ -154,7 +154,7 @@ export class MemoryService extends BaseService {
canAlwaysRemove: Permission.MemoryDelete,
});
const hasSuccess = results.find(({ success }) => success);
const hasSuccess = results.some(({ success }) => success);
if (hasSuccess) {
await this.memoryRepository.update(id, { id, updatedAt: new Date() });
}
+1 -1
View File
@@ -1396,7 +1396,7 @@ describe(MetadataService.name, () => {
expect(mocks.person.refreshFaces).toHaveBeenCalledWith(
[
expect.objectContaining({
boundingBoxX1: Math.floor((0.485_648_148_148_148_2 - 0.2 / 2) * 1000),
boundingBoxX1: Math.floor((0.4856481481481482 - 0.2 / 2) * 1000),
}),
],
[],
+11 -12
View File
@@ -117,9 +117,7 @@ const validateRange = (value: number | undefined, min: number, max: number): Non
};
const getLensModel = (exifTags: ImmichTags): string | null => {
const lensModel = String(
exifTags.LensID ?? exifTags.LensType ?? exifTags.LensSpec ?? exifTags.LensModel ?? '',
).trim();
const lensModel = (exifTags.LensID ?? exifTags.LensType ?? exifTags.LensSpec ?? exifTags.LensModel ?? '').trim();
if (lensModel === '----') {
return null;
}
@@ -286,7 +284,7 @@ export class MetadataService extends BaseService {
exifImageHeight: validate(height),
exifImageWidth: validate(width),
orientation: validate(exifTags.Orientation)?.toString() ?? null,
projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null,
projectionType: exifTags.ProjectionType ? exifTags.ProjectionType.toUpperCase() : null,
bitsPerSample: this.getBitsPerSample(exifTags),
colorspace: exifTags.ColorSpace === undefined ? null : String(exifTags.ColorSpace),
@@ -295,7 +293,7 @@ export class MetadataService extends BaseService {
exifTags.Make ?? exifTags.Device?.Manufacturer ?? exifTags.AndroidMake ?? (exifTags.DeviceManufacturer || null),
model:
exifTags.Model ?? exifTags.Device?.ModelName ?? exifTags.AndroidModel ?? (exifTags.DeviceModelName || null),
fps: video?.frameRate ?? validate(Number.parseFloat(exifTags.VideoFrameRate!)),
fps: video?.frameRate ?? validate(Number(exifTags.VideoFrameRate!)),
iso: validate(exifTags.ISO) as number,
exposureTime: exifTags.ExposureTime ?? null,
lensModel: getLensModel(exifTags),
@@ -326,7 +324,7 @@ export class MetadataService extends BaseService {
: undefined;
const videoData =
format?.formatName && format?.formatLongName && video?.codecName && video?.timeBase
format?.formatName && format.formatLongName && video?.codecName && video?.timeBase
? {
assetId: asset.id,
bitrate: video.bitrate,
@@ -362,8 +360,8 @@ export class MetadataService extends BaseService {
: undefined;
const isSidewards = exifTags.Orientation && this.isOrientationSidewards(exifTags.Orientation);
const assetWidth = isSidewards ? validate(height) : validate(width);
const assetHeight = isSidewards ? validate(width) : validate(height);
const assetWidth = validate(isSidewards ? height : width);
const assetHeight = validate(isSidewards ? width : height);
const tasks = new Tasks();
@@ -445,8 +443,8 @@ export class MetadataService extends BaseService {
let sidecarPath = null;
for (const candidate of this.getSidecarCandidates(asset)) {
const exists = await this.storageRepository.checkFileExists(candidate, constants.R_OK);
if (!exists) {
const isExists = await this.storageRepository.checkFileExists(candidate, constants.R_OK);
if (!isExists) {
continue;
}
@@ -806,8 +804,8 @@ export class MetadataService extends BaseService {
}
// write extracted motion video to disk, especially if the encoded-video folder has been deleted
const existsOnDisk = await this.storageRepository.checkFileExists(motionAsset.originalPath);
if (!existsOnDisk) {
const isExistsOnDisk = await this.storageRepository.checkFileExists(motionAsset.originalPath);
if (!isExistsOnDisk) {
this.storageCore.ensureFolders(motionAsset.originalPath);
await this.storageRepository.createFile(motionAsset.originalPath, video);
this.logger.log(`Wrote motion photo video to ${motionAsset.originalPath}`);
@@ -1087,6 +1085,7 @@ export class MetadataService extends BaseService {
private getDuration(tags: ImmichTags): number | null {
const duration = tags.Duration;
// eslint-disable-next-line unicorn/prefer-number-coercion
const seconds = typeof duration === 'number' ? duration : Number.parseFloat(duration as string);
return Number.isFinite(seconds) ? Math.round(Duration.fromObject({ seconds }).toMillis()) : null;
}
+5 -7
View File
@@ -120,7 +120,7 @@ export class PersonService extends BaseService {
await this.createNewFeaturePhoto([face.person.id]);
}
return await this.findOrFail(personId).then(mapPerson);
return mapPerson(await this.findOrFail(personId));
}
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
@@ -152,7 +152,7 @@ export class PersonService extends BaseService {
async getById(auth: AuthDto, id: string): Promise<PersonResponseDto> {
await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [id] });
return this.findOrFail(id).then(mapPerson);
return mapPerson(await this.findOrFail(id));
}
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
@@ -192,7 +192,7 @@ export class PersonService extends BaseService {
const { name, birthDate, isHidden, featureFaceAssetId: assetId, isFavorite, color } = dto;
// TODO: set by faceId directly
let faceId: string | undefined = undefined;
let faceId: string | undefined;
if (assetId) {
await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [assetId] });
const face = await this.personRepository.getForFeatureFaceUpdate({ personId: id, assetId });
@@ -655,14 +655,12 @@ export class PersonService extends BaseService {
topLeft = { x: topLeft.x * scaleFactor, y: topLeft.y * scaleFactor };
bottomRight = { x: bottomRight.x * scaleFactor, y: bottomRight.y * scaleFactor };
const {
points: [invertedTopLeft, invertedBottomRight],
} = transformPoints(
const [invertedTopLeft, invertedBottomRight] = transformPoints(
[topLeft, bottomRight],
edits,
{ width: asset.width, height: asset.height },
{ inverse: true },
);
).points;
// make sure topLeft is top-left and bottomRight is bottom-right
topLeft = {
+2 -7
View File
@@ -93,10 +93,7 @@ export class QueueService extends BaseService {
private updateConcurrency(config: SystemConfig) {
this.logger.debug(`Updating queue concurrency settings`);
for (const queueName of Object.values(QueueName)) {
let concurrency = 1;
if (this.isConcurrentQueue(queueName)) {
concurrency = config.job[queueName].concurrency;
}
const concurrency = this.isConcurrentQueue(queueName) ? config.job[queueName].concurrency : 1;
this.logger.debug(`Setting ${queueName} concurrency to ${concurrency}`);
this.jobRepository.setConcurrency(queueName, concurrency);
}
@@ -161,9 +158,7 @@ export class QueueService extends BaseService {
throw new BadRequestException(`The BackgroundTask queue cannot be paused`);
}
await this.jobRepository.pause(name);
}
if (dto.isPaused === false) {
} else if (dto.isPaused === false) {
await this.jobRepository.resume(name);
}
+7 -3
View File
@@ -77,7 +77,7 @@ export class ServerService extends BaseService {
serverInfo.diskAvailableRaw = diskInfo.available;
serverInfo.diskSizeRaw = diskInfo.total;
serverInfo.diskUseRaw = diskInfo.total - diskInfo.free;
serverInfo.diskUsagePercentage = Number.parseFloat(usagePercentage);
serverInfo.diskUsagePercentage = Number(usagePercentage);
return serverInfo;
}
@@ -190,8 +190,12 @@ export class ServerService extends BaseService {
throw new BadRequestException('Invalid license key');
}
const { licensePublicKey } = this.configRepository.getEnv();
const licenseValid = this.cryptoRepository.verifySha256(dto.licenseKey, dto.activationKey, licensePublicKey.server);
if (!licenseValid) {
const isLicenseValid = this.cryptoRepository.verifySha256(
dto.licenseKey,
dto.activationKey,
licensePublicKey.server,
);
if (!isLicenseValid) {
throw new BadRequestException('Invalid license key');
}
+2 -1
View File
@@ -20,6 +20,7 @@ export class SharedLinkService extends BaseService {
async getAll(auth: AuthDto, { id, albumId }: SharedLinkSearchDto): Promise<SharedLinkResponseDto[]> {
return this.sharedLinkRepository
.getAll({ userId: auth.user.id, id, albumId })
.then((links) => links.map((link) => mapSharedLink(link, { stripAssetMetadata: false })));
}
@@ -200,7 +201,7 @@ export class SharedLinkService extends BaseService {
const results: AssetIdsResponseDto[] = [];
for (const assetId of dto.assetIds) {
const wasRemoved = removedAssetIds.find((id) => id === assetId);
const wasRemoved = removedAssetIds.includes(assetId);
if (!wasRemoved) {
results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND });
continue;
+3 -3
View File
@@ -43,12 +43,12 @@ export class SmartInfoService extends BaseService {
const modelChange =
oldConfig && oldConfig.machineLearning.clip.modelName !== newConfig.machineLearning.clip.modelName;
const dimSizeChange = dbDimSize !== dimSize;
if (!modelChange && !dimSizeChange) {
const isDimSizeChange = dbDimSize !== dimSize;
if (!modelChange && !isDimSizeChange) {
return;
}
if (dimSizeChange) {
if (isDimSizeChange) {
this.logger.log(
`Dimension size of model ${newConfig.machineLearning.clip.modelName} is ${dimSize}, but database expects ${dbDimSize}.`,
);
+1 -1
View File
@@ -36,7 +36,7 @@ export class StackService extends BaseService {
async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise<StackResponseDto> {
await this.requireAccess({ auth, permission: Permission.StackUpdate, ids: [id] });
const stack = await this.findOrFail(id);
if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) {
if (dto.primaryAssetId && stack.assets.every(({ id }) => id !== dto.primaryAssetId)) {
throw new BadRequestException('Primary asset must be in the stack');
}
@@ -141,8 +141,8 @@ export class StorageTemplateService extends BaseService {
@OnJob({ name: JobName.StorageTemplateMigrationSingle, queue: QueueName.StorageTemplateMigration })
async handleMigrationSingle({ id }: JobOf<JobName.StorageTemplateMigrationSingle>): Promise<JobStatus> {
const config = await this.getConfig({ withCache: true });
const storageTemplateEnabled = config.storageTemplate.enabled;
if (!storageTemplateEnabled) {
const isStorageTemplateEnabled = config.storageTemplate.enabled;
if (!isStorageTemplateEnabled) {
return JobStatus.Skipped;
}
@@ -372,8 +372,8 @@ export class StorageTemplateService extends BaseService {
let duplicateCount = 0;
while (true) {
const exists = await this.storageRepository.checkFileExists(destination);
if (!exists) {
const isExists = await this.storageRepository.checkFileExists(destination);
if (!isExists) {
break;
}
+7 -7
View File
@@ -30,8 +30,8 @@ export class StorageService extends BaseService {
const candidates = ['/data', '/usr/src/app/upload'];
for (const candidate of candidates) {
const exists = this.storageRepository.existsSync(candidate);
if (exists) {
const isExists = this.storageRepository.existsSync(candidate);
if (isExists) {
targets.push(candidate);
}
}
@@ -56,7 +56,7 @@ export class StorageService extends BaseService {
flags.mountChecks = {};
}
let updated = false;
let isUpdated = false;
this.logger.log(`Verifying system mount folder checks, current state: ${JSON.stringify(flags)}`);
@@ -73,11 +73,11 @@ export class StorageService extends BaseService {
if (!flags.mountChecks[folder]) {
flags.mountChecks[folder] = true;
updated = true;
isUpdated = true;
}
}
if (updated) {
if (isUpdated) {
await this.systemMetadataRepository.set(SystemMetadataKey.SystemFlags, flags);
this.logger.log('Successfully enabled system mount folders checks');
}
@@ -166,7 +166,7 @@ export class StorageService extends BaseService {
const { folderPath, internalPath, externalPath } = this.getMountFilePaths(folder);
try {
this.storageRepository.mkdirSync(folderPath);
await this.storageRepository.createFile(internalPath, Buffer.from(`${Date.now()}`));
await this.storageRepository.createFile(internalPath, Buffer.from(Date.now().toString()));
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
this.logger.warn('Found existing mount file, skipping creation');
@@ -180,7 +180,7 @@ export class StorageService extends BaseService {
private async verifyWriteAccess(folder: StorageFolder) {
const { internalPath, externalPath } = this.getMountFilePaths(folder);
try {
await this.storageRepository.overwriteFile(internalPath, Buffer.from(`${Date.now()}`));
await this.storageRepository.overwriteFile(internalPath, Buffer.from(Date.now().toString()));
} catch (error) {
this.logger.error(`Failed to write ${internalPath}: ${error}`);
throw new ImmichStartupError(`Failed to write "${externalPath} - ${docsMessage}"`);
+11 -7
View File
@@ -192,7 +192,11 @@ export class SyncService extends BaseService {
[SyncRequestType.AssetOcrV1]: () => this.syncAssetOcrV1(options, response, checkpointMap, auth),
} as const;
for (const type of SYNC_TYPES_ORDER.filter((type) => dto.types.includes(type))) {
for (const type of SYNC_TYPES_ORDER) {
if (!dto.types.includes(type)) {
continue;
}
const handler = handlers[type as keyof typeof handlers];
await handler();
}
@@ -573,16 +577,16 @@ export class SyncService extends BaseService {
}
const creates = this.syncRepository.albumAsset.getCreates({ ...options, ack: createCheckpoint });
let first = true;
let isFirst = true;
for await (const { updateId, ...data } of creates) {
if (first) {
if (isFirst) {
send(response, {
type: SyncEntityType.SyncAckV1,
data: {},
ackType: SyncEntityType.AlbumAssetUpdateV2,
ids: [options.nowId],
});
first = false;
isFirst = false;
}
send(response, { type: createType, ids: [updateId], data: mapSyncAssetV2(data) });
}
@@ -644,16 +648,16 @@ export class SyncService extends BaseService {
}
const creates = this.syncRepository.albumAssetExif.getCreates({ ...options, ack: createCheckpoint });
let first = true;
let isFirst = true;
for await (const { updateId, ...data } of creates) {
if (first) {
if (isFirst) {
send(response, {
type: SyncEntityType.SyncAckV1,
data: {},
ackType: SyncEntityType.AlbumAssetExifUpdateV1,
ids: [options.nowId],
});
first = false;
isFirst = false;
}
send(response, { type: createType, ids: [updateId], data });
}
+10 -6
View File
@@ -108,10 +108,12 @@ export class TagService extends BaseService {
);
for (const { id: assetId, success } of results) {
if (success) {
await this.updateTags(assetId);
await this.eventRepository.emit('AssetTag', { assetId });
if (!success) {
continue;
}
await this.updateTags(assetId);
await this.eventRepository.emit('AssetTag', { assetId });
}
return results;
@@ -127,10 +129,12 @@ export class TagService extends BaseService {
);
for (const { id: assetId, success } of results) {
if (success) {
await this.updateTags(assetId);
await this.eventRepository.emit('AssetUntag', { assetId });
if (!success) {
continue;
}
await this.updateTags(assetId);
await this.eventRepository.emit('AssetUntag', { assetId });
}
return results;
+5 -3
View File
@@ -34,10 +34,12 @@ export class TelemetryService extends BaseService {
@OnEvent({ name: 'JobSuccess' })
onJobSuccess({ job, response }: ArgOf<'JobSuccess'>) {
if (response && Object.values(JobStatus).includes(response as JobStatus)) {
const jobMetric = `immich.jobs.${snakeCase(job.name)}.${response}`;
this.telemetryRepository.jobs.addToCounter(jobMetric, 1);
if (!(response && Object.values(JobStatus).includes(response as JobStatus))) {
return;
}
const jobMetric = `immich.jobs.${snakeCase(job.name)}.${response}`;
this.telemetryRepository.jobs.addToCounter(jobMetric, 1);
}
@OnEvent({ name: 'JobError' })
+7 -7
View File
@@ -27,7 +27,7 @@ export class TimelineService extends BaseService {
private async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
const { userId, ...options } = dto;
let userIds: string[] | undefined = undefined;
let userIds: string[] | undefined;
if (userId) {
userIds = [userId];
@@ -52,7 +52,7 @@ export class TimelineService extends BaseService {
if (dto.albumId) {
await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [dto.albumId] });
} else {
dto.userId = dto.userId || auth.user.id;
dto.userId ||= auth.user.id;
}
if (dto.userId) {
@@ -71,12 +71,12 @@ export class TimelineService extends BaseService {
}
if (dto.withPartners) {
const requestedLocked = dto.visibility === AssetVisibility.Locked;
const requestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined;
const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
const requestedTrash = dto.isTrashed === true;
const isRequestedLocked = dto.visibility === AssetVisibility.Locked;
const isRequestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined;
const isRequestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
const isRequestedTrash = dto.isTrashed === true;
if (requestedLocked || requestedArchived || requestedFavorite || requestedTrash) {
if (isRequestedLocked || isRequestedArchived || isRequestedFavorite || isRequestedTrash) {
throw new BadRequestException(
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
);
@@ -490,18 +490,15 @@ describe(TranscodingService.name, () => {
describe('FFmpeg seek per segment', () => {
const eiffelSeeks = [
0, 1.987_15, 3.994_372_222_222_222, 6.001_594_444_444_444, 8.008_816_666_666_666, 10.016_038_888_888_888,
12.023_261_111_111_111, 14.030_483_333_333_333, 16.037_705_555_555_554, 18.044_927_777_777_776,
20.052_149_999_999_997, 22.059_372_222_222_223,
0, 1.98715, 3.994372222222222, 6.001594444444444, 8.008816666666666, 10.016038888888888, 12.023261111111111,
14.030483333333333, 16.037705555555554, 18.044927777777776, 20.052149999999997, 22.059372222222223,
];
const waterfallSeeks = [
0, 1.994_642_826_321_467, 4.006_047_357_065_803, 6.017_451_887_810_139_5, 8.028_856_418_554_476,
10.040_260_949_298_812,
0, 1.994642826321467, 4.006047357065803, 6.0174518878101395, 8.028856418554476, 10.040260949298812,
];
const trainSeeks = [
0, 1.991_666_666_666_666_7, 3.991_666_666_666_666_7, 5.991_666_666_666_666, 7.991_666_666_666_666,
9.991_666_666_666_667, 11.991_666_666_666_667, 13.991_666_666_666_667, 15.991_666_666_666_667,
17.991_666_666_666_667, 19.991_666_666_666_667,
0, 1.9916666666666667, 3.9916666666666667, 5.991666666666666, 7.991666666666666, 9.991666666666667,
11.991666666666667, 13.991666666666667, 15.991666666666667, 17.991666666666667, 19.991666666666667,
];
const cases = [
...eiffelSeeks.map((expected, segmentIndex) => ({
+4 -4
View File
@@ -54,7 +54,7 @@ export class TranscodingService extends BaseService {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
return Promise.all([...this.sessions.values()].map(({ id }) => this.onSessionEnd({ sessionId: id })));
return Promise.all(this.sessions.values().map(({ id }) => this.onSessionEnd({ sessionId: id })));
}
@OnJob({ name: JobName.HlsSessionCleanup, queue: QueueName.BackgroundTask })
@@ -139,9 +139,9 @@ export class TranscodingService extends BaseService {
session.variantIndex ??= variantIndex;
session.startSegment ??= segmentIndex;
const curSegment = session.lastCompletedSegment === null ? session.startSegment : session.lastCompletedSegment + 1;
const needsRestart =
const isNeedsRestart =
session.variantIndex !== variantIndex || segmentIndex < session.startSegment || segmentIndex > curSegment + 1;
if (needsRestart) {
if (isNeedsRestart) {
this.stopTranscode(session);
session.variantIndex = variantIndex;
session.startSegment = segmentIndex;
@@ -372,7 +372,7 @@ export class TranscodingService extends BaseService {
private removeInactiveSessions() {
const cutoff = Date.now() - HLS_INACTIVITY_TIMEOUT_MS;
const inactiveSessions = [...this.sessions.values()].filter((s) => s.lastActivityTime.getTime() < cutoff);
const inactiveSessions = this.sessions.values().filter((s) => s.lastActivityTime.getTime() < cutoff);
return Promise.all(
inactiveSessions.map(async (session) => {
try {
+5 -5
View File
@@ -178,19 +178,19 @@ export class UserService extends BaseService {
const { licensePublicKey } = this.configRepository.getEnv();
const clientLicenseValid = this.cryptoRepository.verifySha256(
const isClientLicenseValid = this.cryptoRepository.verifySha256(
license.licenseKey,
license.activationKey,
licensePublicKey.client,
);
const serverLicenseValid = this.cryptoRepository.verifySha256(
const isServerLicenseValid = this.cryptoRepository.verifySha256(
license.licenseKey,
license.activationKey,
licensePublicKey.server,
);
if (!clientLicenseValid && !serverLicenseValid) {
if (!isClientLicenseValid && !isServerLicenseValid) {
throw new BadRequestException('Invalid license key');
}
@@ -292,12 +292,12 @@ export class UserService extends BaseService {
await this.eventRepository.emit('UserDelete', user);
}
private isReadyForDeletion(user: { id: string; deletedAt?: Date | null }, deleteDelay: number): boolean {
private isReadyForDeletion(user: { id: string; deletedAt?: Date | null }, delayUntilDeletion: number): boolean {
if (!user.deletedAt) {
return false;
}
return DateTime.now().minus({ days: deleteDelay }) > DateTime.fromJSDate(user.deletedAt);
return DateTime.now().minus({ days: delayUntilDeletion }) > DateTime.fromJSDate(user.deletedAt);
}
private async findOrFail(id: string, options: UserFindOptions) {
+4 -4
View File
@@ -17,7 +17,7 @@ const asNotification = (
): ReleaseEventV1 => {
return {
// can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483
isAvailable: semver.intersects(`>${serverVersion}`, releaseVersion.toString(), {
isAvailable: semver.intersects(`>${serverVersion}`, releaseVersion, {
includePrerelease: channel === ReleaseChannel.ReleaseCandidate,
}),
checkedAt,
@@ -60,8 +60,8 @@ export class VersionService extends BaseService {
this.logger.log(`Adding ${current} to upgrade history`);
await this.versionRepository.create({ version: current });
const needsNewMemories = semver.lt(previousVersion, '1.129.0');
if (needsNewMemories) {
const isNeedsNewMemories = semver.lt(previousVersion, '1.129.0');
if (isNeedsNewMemories) {
await this.jobRepository.queue({ name: JobName.MemoryGenerate });
}
}
@@ -115,7 +115,7 @@ export class VersionService extends BaseService {
// can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483
if (
semver.intersects(`>${serverVersion}`, releaseVersion.toString(), {
semver.intersects(`>${serverVersion}`, releaseVersion, {
includePrerelease: newVersionCheck.channel === ReleaseChannel.ReleaseCandidate,
})
) {
@@ -93,6 +93,7 @@ export class WorkflowExecutionService extends BaseService {
for (const pattern of context.allowedHosts) {
const regex = new RegExp(pattern.replaceAll('.', String.raw`\.`).replaceAll('*', '.*'));
if (regex.test(hostname)) {
// eslint-disable-next-line unicorn/no-invalid-argument-count
const res = await fetch(...args);
return {
@@ -124,8 +125,8 @@ export class WorkflowExecutionService extends BaseService {
const plugins = await this.pluginRepository.getForLoad();
for (const { id, name, version, wasmBytes, methods } of plugins) {
const method = methods.some(({ hostFunctions }) => !hostFunctions);
if (method) {
const isMethod = methods.some(({ hostFunctions }) => !hostFunctions);
if (isMethod) {
const label = `${name}@${version}`;
const key = this.getPluginKey({ id, hostFunctions: false });
try {
@@ -136,8 +137,8 @@ export class WorkflowExecutionService extends BaseService {
}
}
const methodWithFunction = methods.some(({ hostFunctions }) => hostFunctions);
if (methodWithFunction) {
const isMethodWithFunction = methods.some(({ hostFunctions }) => hostFunctions);
if (isMethodWithFunction) {
const label = `${name}@${version}/worker`;
const key = this.getPluginKey({ id, hostFunctions: true });
try {
@@ -381,8 +382,8 @@ export class WorkflowExecutionService extends BaseService {
// TODO infer from steps
let type: T | undefined;
for (const targetType of Object.values(WorkflowType)) {
const missing = workflow.steps.some((step) => !step.types.includes(targetType));
if (!missing) {
const isMissing = workflow.steps.some((step) => !step.types.includes(targetType));
if (!isMissing) {
type = targetType as unknown as T;
break;
}

Some files were not shown because too many files have changed in this diff Show More