diff --git a/docs/docs/developer/pr-checklist.md b/docs/docs/developer/pr-checklist.md index f855e854c4..e68567bc8f 100644 --- a/docs/docs/developer/pr-checklist.md +++ b/docs/docs/developer/pr-checklist.md @@ -14,15 +14,15 @@ When contributing code through a pull request, please check the following: - [ ] `pnpm run check:typescript` (check typescript) - [ ] `pnpm test` (unit tests) +:::tip AIO +Run all web checks with `pnpm run check:all` +::: + ## Documentation - [ ] `pnpm run format` (formatting via Prettier) - [ ] Update the `_redirects` file if you have renamed a page or removed it from the documentation. -:::tip AIO -Run all web checks with `pnpm run check:all` -::: - ## Server Checks - [ ] `pnpm run lint` (linting via ESLint) diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 55c226d507..8863a13ee7 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -93,7 +93,7 @@ Information on the current workers can be found [here](/administration/jobs-work All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`. `DB_URL` must be in the format `postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename`. -You can require SSL by adding `?sslmode=require` to the end of the `DB_URL` string, or require SSL and skip certificate verification by adding `?sslmode=require&sslmode=no-verify`. +You can require SSL by adding `?sslmode=require` to the end of the `DB_URL` string, or require SSL and skip certificate verification by adding `?sslmode=require&uselibpqcompat=true`. This allows both immich and `pg_dumpall` (the utility used for database backups) to [properly connect](https://github.com/brianc/node-postgres/tree/master/packages/pg-connection-string#tcp-connections) to your database. When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSWORD` and `DB_DATABASE_NAME` database variables are ignored. diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index 33661105e4..3d8fddc9b7 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -75,6 +75,20 @@ class AssetService { isFlipped = false; } + if (width == null || height == null) { + if (asset.hasRemote) { + final id = asset is LocalAsset ? asset.remoteId! : (asset as RemoteAsset).id; + final remoteAsset = await _remoteAssetRepository.get(id); + width = remoteAsset?.width?.toDouble(); + height = remoteAsset?.height?.toDouble(); + } else { + final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; + final localAsset = await _localAssetRepository.get(id); + width = localAsset?.width?.toDouble(); + height = localAsset?.height?.toDouble(); + } + } + final orientedWidth = isFlipped ? height : width; final orientedHeight = isFlipped ? width : height; if (orientedWidth != null && orientedHeight != null && orientedHeight > 0) { diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index 5cbae9c5a1..04eaf04694 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -363,14 +363,14 @@ extension on Iterable { } } -extension on PlatformAsset { +extension PlatformToLocalAsset on PlatformAsset { LocalAsset toLocalAsset() => LocalAsset( id: id, name: name, checksum: null, type: AssetType.values.elementAtOrNull(type) ?? AssetType.other, createdAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(), - updatedAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(), + updatedAt: tryFromSecondsSinceEpoch(updatedAt, isUtc: true) ?? DateTime.timestamp(), width: width, height: height, durationInSeconds: durationInSeconds, diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index b0d7ea6013..552c9e356a 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -22,14 +22,16 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; +import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/datetime_helpers.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; // ignore: import_rule_photo_manager import 'package:photo_manager/photo_manager.dart'; -const int targetVersion = 18; +const int targetVersion = 19; Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { final hasVersion = Store.tryGet(StoreKey.version) != null; @@ -78,6 +80,12 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { await Store.put(StoreKey.shouldResetSync, true); } + if (version < 19 && Store.isBetaTimelineEnabled) { + if (!await _populateUpdatedAtTime(drift)) { + return; + } + } + if (targetVersion >= 12) { await Store.put(StoreKey.version, targetVersion); return; @@ -221,6 +229,32 @@ Future _migrateDeviceAsset(Isar db) async { }); } +Future _populateUpdatedAtTime(Drift db) async { + try { + final nativeApi = NativeSyncApi(); + final albums = await nativeApi.getAlbums(); + for (final album in albums) { + final assets = await nativeApi.getAssetsForAlbum(album.id); + await db.batch((batch) async { + for (final asset in assets) { + batch.update( + db.localAssetEntity, + LocalAssetEntityCompanion( + updatedAt: Value(tryFromSecondsSinceEpoch(asset.updatedAt, isUtc: true) ?? DateTime.timestamp()), + ), + where: (t) => t.id.equals(asset.id), + ); + } + }); + } + + return true; + } catch (error) { + dPrint(() => "[MIGRATION] Error while populating updatedAt time: $error"); + return false; + } +} + Future migrateDeviceAssetToSqlite(Isar db, Drift drift) async { try { final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll(); diff --git a/mobile/test/domain/services/asset.service_test.dart b/mobile/test/domain/services/asset.service_test.dart new file mode 100644 index 0000000000..5e7179ffa6 --- /dev/null +++ b/mobile/test/domain/services/asset.service_test.dart @@ -0,0 +1,165 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/domain/services/asset.service.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../infrastructure/repository.mock.dart'; +import '../../test_utils.dart'; + +void main() { + late AssetService sut; + late MockRemoteAssetRepository mockRemoteAssetRepository; + late MockDriftLocalAssetRepository mockLocalAssetRepository; + + setUp(() { + mockRemoteAssetRepository = MockRemoteAssetRepository(); + mockLocalAssetRepository = MockDriftLocalAssetRepository(); + sut = AssetService( + remoteAssetRepository: mockRemoteAssetRepository, + localAssetRepository: mockLocalAssetRepository, + ); + }); + + group('getAspectRatio', () { + test('flips dimensions on Android for 90° and 270° orientations', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + addTearDown(() => debugDefaultTargetPlatformOverride = null); + + for (final orientation in [90, 270]) { + final localAsset = TestUtils.createLocalAsset( + id: 'local-$orientation', + width: 1920, + height: 1080, + orientation: orientation, + ); + + final result = await sut.getAspectRatio(localAsset); + + expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip on Android'); + } + }); + + test('does not flip dimensions on iOS regardless of orientation', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + addTearDown(() => debugDefaultTargetPlatformOverride = null); + + for (final orientation in [0, 90, 270]) { + final localAsset = TestUtils.createLocalAsset( + id: 'local-$orientation', + width: 1920, + height: 1080, + orientation: orientation, + ); + + final result = await sut.getAspectRatio(localAsset); + + expect(result, 1920 / 1080, reason: 'iOS should never flip dimensions'); + } + }); + + test('fetches dimensions from remote repository when missing from asset', () async { + final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null); + + final exif = const ExifInfo(orientation: '1'); + + final fetchedAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 1080); + + when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); + when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => fetchedAsset); + + final result = await sut.getAspectRatio(remoteAsset); + + expect(result, 1920 / 1080); + verify(() => mockRemoteAssetRepository.get('remote-1')).called(1); + }); + + test('fetches dimensions from local repository when missing from local asset', () async { + final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0); + + final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 0); + + when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset); + + final result = await sut.getAspectRatio(localAsset); + + expect(result, 1920 / 1080); + verify(() => mockLocalAssetRepository.get('local-1')).called(1); + }); + + test('returns 1.0 when dimensions are still unavailable after fetching', () async { + final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null); + + final exif = const ExifInfo(orientation: '1'); + + when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); + when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => null); + + final result = await sut.getAspectRatio(remoteAsset); + + expect(result, 1.0); + }); + + test('returns 1.0 when height is zero', () async { + final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 0); + + final exif = const ExifInfo(orientation: '1'); + + when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); + + final result = await sut.getAspectRatio(remoteAsset); + + expect(result, 1.0); + }); + + test('handles local asset with remoteId and uses exif from remote', () async { + final localAsset = TestUtils.createLocalAsset( + id: 'local-1', + remoteId: 'remote-1', + width: 1920, + height: 1080, + orientation: 0, + ); + + final exif = const ExifInfo(orientation: '6'); + + when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif); + + final result = await sut.getAspectRatio(localAsset); + + expect(result, 1080 / 1920); + }); + + test('handles various flipped EXIF orientations correctly', () async { + final flippedOrientations = ['5', '6', '7', '8', '90', '-90']; + + for (final orientation in flippedOrientations) { + final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080); + + final exif = ExifInfo(orientation: orientation); + + when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif); + + final result = await sut.getAspectRatio(remoteAsset); + + expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip dimensions'); + } + }); + + test('handles various non-flipped EXIF orientations correctly', () async { + final nonFlippedOrientations = ['1', '2', '3', '4']; + + for (final orientation in nonFlippedOrientations) { + final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080); + + final exif = ExifInfo(orientation: orientation); + + when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif); + + final result = await sut.getAspectRatio(remoteAsset); + + expect(result, 1920 / 1080, reason: 'Orientation $orientation should NOT flip dimensions'); + } + }); + }); +} diff --git a/mobile/test/domain/services/local_sync_service_test.dart b/mobile/test/domain/services/local_sync_service_test.dart index 2f236971e0..92ab01c7e0 100644 --- a/mobile/test/domain/services/local_sync_service_test.dart +++ b/mobile/test/domain/services/local_sync_service_test.dart @@ -54,12 +54,7 @@ void main() { when(() => mockNativeSyncApi.shouldFullSync()).thenAnswer((_) async => false); when(() => mockNativeSyncApi.getMediaChanges()).thenAnswer( - (_) async => SyncDelta( - hasChanges: false, - updates: const [], - deletes: const [], - assetAlbums: const {}, - ), + (_) async => SyncDelta(hasChanges: false, updates: const [], deletes: const [], assetAlbums: const {}), ); when(() => mockNativeSyncApi.getTrashedAssets()).thenAnswer((_) async => {}); when(() => mockTrashedLocalAssetRepository.processTrashSnapshot(any())).thenAnswer((_) async {}); @@ -144,13 +139,19 @@ void main() { }); final localAssetToTrash = LocalAssetStub.image2.copyWith(id: 'local-trash', checksum: 'checksum-trash'); - when(() => mockTrashedLocalAssetRepository.getToTrash()).thenAnswer((_) async => {'album-a': [localAssetToTrash]}); + when(() => mockTrashedLocalAssetRepository.getToTrash()).thenAnswer( + (_) async => { + 'album-a': [localAssetToTrash], + }, + ); final assetEntity = MockAssetEntity(); when(() => assetEntity.getMediaUrl()).thenAnswer((_) async => 'content://local-trash'); when(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).thenAnswer((_) async => assetEntity); - await sut.processTrashedAssets({'album-a': [platformAsset]}); + await sut.processTrashedAssets({ + 'album-a': [platformAsset], + }); verify(() => mockTrashedLocalAssetRepository.processTrashSnapshot(any())).called(1); verify(() => mockTrashedLocalAssetRepository.getToTrash()).called(1); @@ -159,8 +160,7 @@ void main() { verify(() => mockTrashedLocalAssetRepository.applyRestoredAssets(restoredIds)).called(1); verify(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).called(1); - final moveArgs = - verify(() => mockLocalFilesManager.moveToTrash(captureAny())).captured.single as List; + final moveArgs = verify(() => mockLocalFilesManager.moveToTrash(captureAny())).captured.single as List; expect(moveArgs, ['content://local-trash']); final trashArgs = verify(() => mockTrashedLocalAssetRepository.trashLocalAsset(captureAny())).captured.single @@ -187,4 +187,25 @@ void main() { verifyNever(() => mockTrashedLocalAssetRepository.trashLocalAsset(any())); }); }); + + group('LocalSyncService - PlatformAsset conversion', () { + test('toLocalAsset uses correct updatedAt timestamp', () { + final platformAsset = PlatformAsset( + id: 'test-id', + name: 'test.jpg', + type: AssetType.image.index, + durationInSeconds: 0, + orientation: 0, + isFavorite: false, + createdAt: 1700000000, + updatedAt: 1732000000, + ); + + final localAsset = platformAsset.toLocalAsset(); + + expect(localAsset.createdAt.millisecondsSinceEpoch ~/ 1000, 1700000000); + expect(localAsset.updatedAt.millisecondsSinceEpoch ~/ 1000, 1732000000); + expect(localAsset.updatedAt, isNot(localAsset.createdAt)); + }); + }); } diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index becfafe33d..aac384c29e 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -4,6 +4,7 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; @@ -35,6 +36,8 @@ class MockLocalAssetRepository extends Mock implements DriftLocalAssetRepository class MockDriftLocalAssetRepository extends Mock implements DriftLocalAssetRepository {} +class MockRemoteAssetRepository extends Mock implements RemoteAssetRepository {} + class MockTrashedLocalAssetRepository extends Mock implements DriftTrashedLocalAssetRepository {} class MockStorageRepository extends Mock implements StorageRepository {} diff --git a/mobile/test/test_utils.dart b/mobile/test/test_utils.dart index 9b59773d3b..498607e3d2 100644 --- a/mobile/test/test_utils.dart +++ b/mobile/test/test_utils.dart @@ -5,6 +5,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:fake_async/fake_async.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as domain; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/android_device_asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; @@ -116,4 +117,43 @@ abstract final class TestUtils { } return result; } + + static domain.RemoteAsset createRemoteAsset({required String id, int? width, int? height, String? ownerId}) { + return domain.RemoteAsset( + id: id, + checksum: 'checksum1', + ownerId: ownerId ?? 'owner1', + name: 'test.jpg', + type: domain.AssetType.image, + createdAt: DateTime(2024, 1, 1), + updatedAt: DateTime(2024, 1, 1), + durationInSeconds: 0, + isFavorite: false, + width: width, + height: height, + ); + } + + static domain.LocalAsset createLocalAsset({ + required String id, + String? remoteId, + int? width, + int? height, + int orientation = 0, + }) { + return domain.LocalAsset( + id: id, + remoteId: remoteId, + checksum: 'checksum1', + name: 'test.jpg', + type: domain.AssetType.image, + createdAt: DateTime(2024, 1, 1), + updatedAt: DateTime(2024, 1, 1), + durationInSeconds: 0, + isFavorite: false, + width: width, + height: height, + orientation: orientation, + ); + } } diff --git a/plugins/package-lock.json b/plugins/package-lock.json index 3b0f0b34cb..231e298970 100644 --- a/plugins/package-lock.json +++ b/plugins/package-lock.json @@ -1,385 +1,459 @@ { - "name": "js-pdk-template", + "name": "plugins", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "js-pdk-template", + "name": "plugins", "version": "1.0.0", - "license": "BSD-3-Clause", + "license": "AGPL-3.0", "devDependencies": { "@extism/js-pdk": "^1.0.1", - "esbuild": "^0.19.6", + "esbuild": "^0.25.0", "typescript": "^5.3.2" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ - "x64" + "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@extism/js-pdk": { @@ -389,41 +463,45 @@ "dev": true }, "node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/typescript": { diff --git a/plugins/package.json b/plugins/package.json index ab6b2f8435..024b7bc8a9 100644 --- a/plugins/package.json +++ b/plugins/package.json @@ -13,7 +13,7 @@ "license": "AGPL-3.0", "devDependencies": { "@extism/js-pdk": "^1.0.1", - "esbuild": "^0.19.6", + "esbuild": "^0.25.0", "typescript": "^5.3.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3cf42646ea..8cb0d95b38 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -311,8 +311,8 @@ importers: specifier: ^1.0.1 version: 1.1.1 esbuild: - specifier: ^0.19.6 - version: 0.19.12 + specifier: ^0.25.0 + version: 0.25.12 typescript: specifier: ^5.3.2 version: 5.9.3 @@ -456,7 +456,7 @@ importers: version: 5.10.0 js-yaml: specifier: ^4.1.0 - version: 4.1.0 + version: 4.1.1 jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -717,8 +717,8 @@ importers: specifier: file:../open-api/typescript-sdk version: link:../open-api/typescript-sdk '@immich/ui': - specifier: ^0.45.1 - version: 0.45.1(svelte@5.43.0) + specifier: ^0.46.0 + version: 0.46.0(svelte@5.43.0) '@mapbox/mapbox-gl-rtl-text': specifier: 0.2.3 version: 0.2.3(mapbox-gl@1.13.3) @@ -2862,8 +2862,8 @@ packages: peerDependencies: svelte: ^5.0.0 - '@immich/ui@0.45.1': - resolution: {integrity: sha512-LanpfRI7cJLXExRxaYd4xMRvq/SWZlHmBhSsw56l0aAI6Mltm+IcFMD6LF+jvSzGOSLGLcNxIAYsqqWAPmn8+g==} + '@immich/ui@0.46.0': + resolution: {integrity: sha512-d/X41oYmglAiMg129qZDaVYUZxC6HB5v31viM7ZDmxTdFDXaM61AKv4RbbbLs69Nd4f94LM68xGKsCOPcol18g==} peerDependencies: svelte: ^5.0.0 @@ -7896,14 +7896,18 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + jsdom@20.0.3: resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} engines: {node: '>=14'} @@ -13850,7 +13854,7 @@ snapshots: '@types/react-router-config': 5.0.11 combine-promises: 1.2.0 fs-extra: 11.3.2 - js-yaml: 4.1.0 + js-yaml: 4.1.1 lodash: 4.17.21 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -14306,7 +14310,7 @@ snapshots: '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fs-extra: 11.3.2 joi: 17.13.3 - js-yaml: 4.1.0 + js-yaml: 4.1.1 lodash: 4.17.21 tslib: 2.8.1 transitivePeerDependencies: @@ -14331,7 +14335,7 @@ snapshots: globby: 11.1.0 gray-matter: 4.0.3 jiti: 1.21.7 - js-yaml: 4.1.0 + js-yaml: 4.1.1 lodash: 4.17.21 micromatch: 4.0.8 p-queue: 6.6.2 @@ -14537,7 +14541,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -14753,7 +14757,7 @@ snapshots: dependencies: svelte: 5.43.0 - '@immich/ui@0.45.1(svelte@5.43.0)': + '@immich/ui@0.46.0(svelte@5.43.0)': dependencies: '@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.43.0) '@internationalized/date': 3.10.0 @@ -18374,7 +18378,7 @@ snapshots: cosmiconfig@8.3.6(typescript@5.8.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: @@ -18383,7 +18387,7 @@ snapshots: cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: @@ -19841,7 +19845,7 @@ snapshots: gray-matter@4.0.3: dependencies: - js-yaml: 3.14.1 + js-yaml: 3.14.2 kind-of: 6.0.3 section-matter: 1.0.0 strip-bom-string: 1.0.0 @@ -20555,7 +20559,7 @@ snapshots: js-tokens@9.0.1: {} - js-yaml@3.14.1: + js-yaml@3.14.2: dependencies: argparse: 1.0.10 esprima: 4.0.1 @@ -20564,6 +20568,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + jsdom@20.0.3(canvas@2.11.2): dependencies: abab: 2.0.6 diff --git a/server/src/services/backup.service.spec.ts b/server/src/services/backup.service.spec.ts index 8aa20aa868..9e25fbaf2e 100644 --- a/server/src/services/backup.service.spec.ts +++ b/server/src/services/backup.service.spec.ts @@ -153,6 +153,37 @@ describe(BackupService.name, () => { mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); }); + it('should sanitize DB_URL (remove uselibpqcompat) before calling pg_dumpall', async () => { + // create a service instance with a URL connection that includes libpqcompat + const dbUrl = 'postgresql://postgres:pwd@host:5432/immich?sslmode=require&uselibpqcompat=true'; + const configMock = { + getEnv: () => ({ database: { config: { connectionType: 'url', url: dbUrl }, skipMigrations: false } }), + getWorker: () => ImmichWorker.Api, + isDev: () => false, + } as unknown as any; + + ({ sut, mocks } = newTestService(BackupService, { config: configMock })); + + mocks.storage.readdir.mockResolvedValue([]); + mocks.process.spawn.mockReturnValue(mockSpawn(0, 'data', '')); + mocks.storage.rename.mockResolvedValue(); + mocks.storage.unlink.mockResolvedValue(); + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); + mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); + mocks.database.getPostgresVersion.mockResolvedValue('14.10'); + + await sut.handleBackupDatabase(); + + expect(mocks.process.spawn).toHaveBeenCalled(); + const call = mocks.process.spawn.mock.calls[0]; + const args = call[1] as string[]; + // ['--dbname', '', '--clean', '--if-exists'] + expect(args[0]).toBe('--dbname'); + const passedUrl = args[1]; + expect(passedUrl).not.toContain('uselibpqcompat'); + expect(passedUrl).toContain('sslmode=require'); + }); + it('should run a database backup successfully', async () => { const result = await sut.handleBackupDatabase(); expect(result).toBe(JobStatus.Success); diff --git a/server/src/services/backup.service.ts b/server/src/services/backup.service.ts index 6f8cc0e34a..3731b1810f 100644 --- a/server/src/services/backup.service.ts +++ b/server/src/services/backup.service.ts @@ -81,8 +81,16 @@ export class BackupService extends BaseService { const isUrlConnection = config.connectionType === 'url'; + let connectionUrl: string = isUrlConnection ? config.url : ''; + if (URL.canParse(connectionUrl)) { + // remove known bad url parameters for pg_dumpall + const url = new URL(connectionUrl); + url.searchParams.delete('uselibpqcompat'); + connectionUrl = url.toString(); + } + const databaseParams = isUrlConnection - ? ['--dbname', config.url] + ? ['--dbname', connectionUrl] : [ '--username', config.username, @@ -118,7 +126,7 @@ export class BackupService extends BaseService { { env: { PATH: process.env.PATH, - PGPASSWORD: isUrlConnection ? new URL(config.url).password : config.password, + PGPASSWORD: isUrlConnection ? new URL(connectionUrl).password : config.password, }, }, ); diff --git a/web/package.json b/web/package.json index 1fb0f50653..8d28eeb9c2 100644 --- a/web/package.json +++ b/web/package.json @@ -11,13 +11,13 @@ "preview": "vite preview", "check:svelte": "svelte-check --no-tsconfig --fail-on-warnings", "check:typescript": "tsc --noEmit", - "check:watch": "npm run check:svelte -- --watch", - "check:code": "npm run format && npm run lint:p && npm run check:svelte && npm run check:typescript", - "check:all": "npm run check:code && npm run test:cov", + "check:watch": "pnpm run check:svelte --watch", + "check:code": "pnpm run format && pnpm run lint && pnpm run check:svelte && pnpm run check:typescript", + "check:all": "pnpm run check:code && pnpm run test:cov", "lint": "eslint . --max-warnings 0 --concurrency 4", - "lint:fix": "npm run lint -- --fix", + "lint:fix": "pnpm run lint --fix", "format": "prettier --check .", - "format:fix": "prettier --write . && npm run format:i18n", + "format:fix": "prettier --write . && pnpm run format:i18n", "format:i18n": "pnpm dlx sort-json ../i18n/*.json", "test": "vitest --run", "test:cov": "vitest --coverage", @@ -28,7 +28,7 @@ "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/justified-layout-wasm": "^0.4.3", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.45.1", + "@immich/ui": "^0.46.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.14.0", diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte index bb826110b7..e4b588af8f 100644 --- a/web/src/lib/components/album-page/albums-list.svelte +++ b/web/src/lib/components/album-page/albums-list.svelte @@ -33,7 +33,6 @@ import { groupBy } from 'lodash-es'; import { onMount, type Snippet } from 'svelte'; import { t } from 'svelte-i18n'; - import { run } from 'svelte/legacy'; interface Props { ownedAlbums?: AlbumResponseDto[]; @@ -128,65 +127,45 @@ }, }; - let albums: AlbumResponseDto[] = $state([]); - let filteredAlbums: AlbumResponseDto[] = $state([]); - let groupedAlbums: AlbumGroup[] = $state([]); + let albums = $derived.by(() => { + switch (userSettings.filter) { + case AlbumFilter.Owned: { + return ownedAlbums; + } + case AlbumFilter.Shared: { + return sharedAlbums; + } + default: { + const nonOwnedAlbums = sharedAlbums.filter((album) => album.ownerId !== $user.id); + return nonOwnedAlbums.length > 0 ? ownedAlbums.concat(nonOwnedAlbums) : ownedAlbums; + } + } + }); + const normalizedSearchQuery = $derived(normalizeSearchString(searchQuery)); + let filteredAlbums = $derived( + normalizedSearchQuery + ? albums.filter(({ albumName }) => normalizeSearchString(albumName).includes(normalizedSearchQuery)) + : albums, + ); - let albumGroupOption: string = $state(AlbumGroupBy.None); + let albumGroupOption = $derived(getSelectedAlbumGroupOption(userSettings)); + let groupedAlbums = $derived.by(() => { + const groupFunc = groupOptions[albumGroupOption] ?? groupOptions[AlbumGroupBy.None]; + const groupedAlbums = groupFunc(stringToSortOrder(userSettings.groupOrder), filteredAlbums); - let albumToShare: AlbumResponseDto | null = $state(null); + return groupedAlbums.map((group) => ({ + id: group.id, + name: group.name, + albums: sortAlbums(group.albums, { sortBy: userSettings.sortBy, orderBy: userSettings.sortOrder }), + })); + }); let contextMenuPosition: ContextMenuPosition = $state({ x: 0, y: 0 }); let selectedAlbum: AlbumResponseDto | undefined = $state(); let isOpen = $state(false); - // Step 1: Filter between Owned and Shared albums, or both. - run(() => { - switch (userSettings.filter) { - case AlbumFilter.Owned: { - albums = ownedAlbums; - break; - } - case AlbumFilter.Shared: { - albums = sharedAlbums; - break; - } - default: { - const userId = $user.id; - const nonOwnedAlbums = sharedAlbums.filter((album) => album.ownerId !== userId); - albums = nonOwnedAlbums.length > 0 ? ownedAlbums.concat(nonOwnedAlbums) : ownedAlbums; - } - } - }); - - // Step 2: Filter using the given search query. - run(() => { - if (searchQuery) { - const searchAlbumNormalized = normalizeSearchString(searchQuery); - - filteredAlbums = albums.filter((album) => { - return normalizeSearchString(album.albumName).includes(searchAlbumNormalized); - }); - } else { - filteredAlbums = albums; - } - }); - - // Step 3: Group albums. - run(() => { - albumGroupOption = getSelectedAlbumGroupOption(userSettings); - const groupFunc = groupOptions[albumGroupOption] ?? groupOptions[AlbumGroupBy.None]; - groupedAlbums = groupFunc(stringToSortOrder(userSettings.groupOrder), filteredAlbums); - }); - - // Step 4: Sort albums amongst each group. - run(() => { - groupedAlbums = groupedAlbums.map((group) => ({ - id: group.id, - name: group.name, - albums: sortAlbums(group.albums, { sortBy: userSettings.sortBy, orderBy: userSettings.sortOrder }), - })); - + // TODO get rid of this + $effect(() => { albumGroupIds = groupedAlbums.map(({ id }) => id); }); @@ -231,7 +210,7 @@ const result = await modalManager.show(AlbumShareModal, { album: selectedAlbum }); switch (result?.action) { case 'sharedUsers': { - await handleAddUsers(result.data); + await handleAddUsers(selectedAlbum, result.data); break; } @@ -300,22 +279,17 @@ updateRecentAlbumInfo(album); }; - const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => { - if (!albumToShare) { - return; - } + const handleAddUsers = async (album: AlbumResponseDto, albumUsers: AlbumUserAddDto[]) => { try { - const album = await addUsersToAlbum({ - id: albumToShare.id, + const updatedAlbum = await addUsersToAlbum({ + id: album.id, addUsersDto: { albumUsers, }, }); - updateAlbumInfo(album); + updateAlbumInfo(updatedAlbum); } catch (error) { handleError(error, $t('errors.unable_to_add_album_users')); - } finally { - albumToShare = null; } }; diff --git a/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts b/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts index aa4bae8919..566b11b8b7 100644 --- a/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts +++ b/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts @@ -1,6 +1,5 @@ import { setDifference, type TimelineDate } from '$lib/utils/timeline-util'; import { AssetOrder } from '@immich/sdk'; -import { SvelteSet } from 'svelte/reactivity'; import type { DayGroup } from './day-group.svelte'; import type { MonthGroup } from './month-group.svelte'; import type { TimelineAsset } from './types'; @@ -10,8 +9,10 @@ export class GroupInsertionCache { [year: number]: { [month: number]: { [day: number]: DayGroup } }; } = {}; unprocessedAssets: TimelineAsset[] = []; - changedDayGroups = new SvelteSet(); - newDayGroups = new SvelteSet(); + // eslint-disable-next-line svelte/prefer-svelte-reactivity + changedDayGroups = new Set(); + // eslint-disable-next-line svelte/prefer-svelte-reactivity + newDayGroups = new Set(); getDayGroup({ year, month, day }: TimelineDate): DayGroup | undefined { return this.#lookupCache[year]?.[month]?.[day]; @@ -32,7 +33,8 @@ export class GroupInsertionCache { } get updatedBuckets() { - const updated = new SvelteSet(); + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const updated = new Set(); for (const group of this.changedDayGroups) { updated.add(group.monthGroup); } @@ -40,7 +42,8 @@ export class GroupInsertionCache { } get bucketsWithNewDayGroups() { - const updated = new SvelteSet(); + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const updated = new Set(); for (const group of this.newDayGroups) { updated.add(group.monthGroup); } diff --git a/web/src/lib/managers/timeline-manager/internal/load-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/load-support.svelte.ts index ec50e3d75e..859e818583 100644 --- a/web/src/lib/managers/timeline-manager/internal/load-support.svelte.ts +++ b/web/src/lib/managers/timeline-manager/internal/load-support.svelte.ts @@ -46,7 +46,7 @@ export async function loadFromTimeBuckets( } } - const unprocessedAssets = monthGroup.addAssets(bucketResponse); + const unprocessedAssets = monthGroup.addAssets(bucketResponse, true); if (unprocessedAssets.length > 0) { console.error( `Warning: getTimeBucket API returning assets not in requested month: ${monthGroup.yearMonth.month}, ${JSON.stringify( diff --git a/web/src/lib/managers/timeline-manager/month-group.svelte.ts b/web/src/lib/managers/timeline-manager/month-group.svelte.ts index bef512c226..1d9e1bbaa7 100644 --- a/web/src/lib/managers/timeline-manager/month-group.svelte.ts +++ b/web/src/lib/managers/timeline-manager/month-group.svelte.ts @@ -153,7 +153,7 @@ export class MonthGroup { }; } - addAssets(bucketAssets: TimeBucketAssetResponseDto) { + addAssets(bucketAssets: TimeBucketAssetResponseDto, preSorted: boolean) { const addContext = new GroupInsertionCache(); for (let i = 0; i < bucketAssets.id.length; i++) { const { localDateTime, fileCreatedAt } = getTimes( @@ -194,6 +194,9 @@ export class MonthGroup { } this.addTimelineAsset(timelineAsset, addContext); } + if (preSorted) { + return addContext.unprocessedAssets; + } for (const group of addContext.existingDayGroups) { group.sortAssets(this.#sortOrder); diff --git a/web/src/lib/modals/HelpAndFeedbackModal.svelte b/web/src/lib/modals/HelpAndFeedbackModal.svelte index f25f7d1704..8b73978672 100644 --- a/web/src/lib/modals/HelpAndFeedbackModal.svelte +++ b/web/src/lib/modals/HelpAndFeedbackModal.svelte @@ -2,7 +2,7 @@ import { type ServerAboutResponseDto } from '@immich/sdk'; import { Icon, Modal, ModalBody } from '@immich/ui'; import { mdiBugOutline, mdiFaceAgent, mdiGit, mdiGithub, mdiInformationOutline } from '@mdi/js'; - import { siDiscord } from 'simple-icons'; + import { type SimpleIcon, siDiscord } from 'simple-icons'; import { t } from 'svelte-i18n'; interface Props { @@ -13,94 +13,57 @@ let { onClose, info }: Props = $props(); +{#snippet link(url: string, icon: string | SimpleIcon, text: string)} + +{/snippet} +

{$t('official_immich_resources')}

-
- +
+ {@render link( + `https://docs.${info.version}.archive.immich.app/overview/introduction`, + mdiInformationOutline, + $t('documentation'), + )} - + {@render link('https://github.com/immich-app/immich/', mdiGithub, $t('source'))} - + {@render link('https://discord.immich.app', siDiscord, $t('discord'))} - + {@render link( + 'https://github.com/immich-app/immich/issues/new/choose', + mdiBugOutline, + $t('bugs_and_feature_requests'), + )}
{#if info.thirdPartyBugFeatureUrl || info.thirdPartySourceUrl || info.thirdPartyDocumentationUrl || info.thirdPartySupportUrl}

{$t('third_party_resources')}

{$t('support_third_party_description')}

-
+
{#if info.thirdPartyDocumentationUrl} - + {@render link(info.thirdPartyDocumentationUrl, mdiInformationOutline, $t('documentation'))} {/if} {#if info.thirdPartySourceUrl} - + {@render link(info.thirdPartySourceUrl, mdiGit, $t('source'))} {/if} {#if info.thirdPartySupportUrl} - + {@render link(info.thirdPartySupportUrl, mdiFaceAgent, $t('support'))} {/if} {#if info.thirdPartyBugFeatureUrl} - + {@render link(info.thirdPartyBugFeatureUrl, mdiBugOutline, $t('bugs_and_feature_requests'))} {/if}
{/if}