Quellcode durchsuchen

infra(server)!: fix typeorm asset entity relations (#1782)

* fix: add correct relations to asset typeorm entity

* fix: add missing createdAt column to asset entity

* ci: run check to make sure generated API is up-to-date

* ci: cancel workflows that aren't for the latest commit in a branch

* chore: add fvm config for flutter
Zack Pollard vor 2 Jahren
Ursprung
Commit
5ad4e5b614
65 geänderte Dateien mit 432 neuen und 306 gelöschten Zeilen
  1. 5 1
      .github/workflows/build-mobile.yml
  2. 7 3
      .github/workflows/cache-cleanup.yml
  3. 7 3
      .github/workflows/codeql-analysis.yml
  4. 4 0
      .github/workflows/dispatch_sdk_update.yml
  5. 4 0
      .github/workflows/docker.yml
  6. 4 0
      .github/workflows/github-repo-stats.yml
  7. 8 4
      .github/workflows/prepare-release.yml
  8. 4 0
      .github/workflows/static_analysis.yml
  9. 25 0
      .github/workflows/test.yml
  10. 4 0
      mobile/.fvm/fvm_config.json
  11. 2 1
      mobile/.gitignore
  12. 2 2
      mobile/lib/modules/album/views/album_viewer_page.dart
  13. 1 1
      mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart
  14. 9 9
      mobile/lib/modules/backup/models/current_upload_asset.model.dart
  15. 7 7
      mobile/lib/modules/backup/models/error_upload_asset.model.dart
  16. 1 1
      mobile/lib/modules/backup/providers/backup.provider.dart
  17. 4 4
      mobile/lib/modules/backup/services/backup.service.dart
  18. 1 1
      mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart
  19. 1 1
      mobile/lib/modules/backup/views/failed_backup_status_page.dart
  20. 2 2
      mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart
  21. 18 18
      mobile/lib/shared/models/asset.dart
  22. 2 2
      mobile/lib/shared/providers/asset.provider.dart
  23. 1 1
      mobile/openapi/README.md
  24. 6 6
      mobile/openapi/doc/APIKeyApi.md
  25. 1 1
      mobile/openapi/doc/APIKeyResponseDto.md
  26. 6 6
      mobile/openapi/doc/AssetApi.md
  27. 2 2
      mobile/openapi/doc/AssetResponseDto.md
  28. 15 15
      mobile/openapi/lib/api/api_key_api.dart
  29. 11 11
      mobile/openapi/lib/api/asset_api.dart
  30. 2 2
      mobile/openapi/lib/model/api_key_response_dto.dart
  31. 15 15
      mobile/openapi/lib/model/asset_response_dto.dart
  32. 3 3
      mobile/openapi/test/api_key_api_test.dart
  33. 1 1
      mobile/openapi/test/api_key_response_dto_test.dart
  34. 1 1
      mobile/openapi/test/asset_api_test.dart
  35. 4 4
      mobile/openapi/test/asset_response_dto_test.dart
  36. 1 1
      mobile/pubspec.lock
  37. 6 6
      mobile/test/asset_grid_data_structure_test.dart
  38. 3 3
      server/apps/immich/src/api-v1/album/album-repository.ts
  39. 29 25
      server/apps/immich/src/api-v1/asset/asset-repository.ts
  40. 6 8
      server/apps/immich/src/api-v1/asset/asset.core.ts
  41. 12 10
      server/apps/immich/src/api-v1/asset/asset.service.spec.ts
  42. 1 1
      server/apps/immich/src/api-v1/asset/asset.service.ts
  43. 2 2
      server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts
  44. 11 14
      server/apps/microservices/src/processors/metadata-extraction.processor.ts
  45. 3 3
      server/apps/microservices/src/processors/thumbnail.processor.ts
  46. 1 1
      server/apps/microservices/src/processors/user-deletion.processor.ts
  47. 1 1
      server/apps/microservices/src/processors/video-transcode.processor.ts
  48. 8 8
      server/immich-openapi-specs.json
  49. 8 8
      server/libs/domain/src/asset/response-dto/asset-response.dto.ts
  50. 13 7
      server/libs/domain/test/fixtures.ts
  51. 23 10
      server/libs/infra/src/db/entities/asset.entity.ts
  52. 30 0
      server/libs/infra/src/db/migrations/1676680127415-FixAssetRelations.ts
  53. 14 0
      server/libs/infra/src/db/migrations/1676721296440-AssetCreatedAtField.ts
  54. 2 2
      server/libs/infra/src/db/repository/shared-link.repository.ts
  55. 3 3
      server/libs/storage/src/storage.service.ts
  56. 1 1
      server/package-lock.json
  57. 52 52
      web/src/api/open-api/api.ts
  58. 1 1
      web/src/api/open-api/base.ts
  59. 1 1
      web/src/api/open-api/common.ts
  60. 1 1
      web/src/api/open-api/configuration.ts
  61. 1 1
      web/src/api/open-api/index.ts
  62. 2 2
      web/src/lib/components/album-page/album-viewer.svelte
  63. 2 2
      web/src/lib/components/photos-page/asset-date-group.svelte
  64. 1 1
      web/src/lib/stores/asset-interaction.store.ts
  65. 3 3
      web/src/lib/utils/file-uploader.ts

+ 5 - 1
.github/workflows/build-mobile.yml

@@ -11,6 +11,10 @@ on:
   push:
     branches: [main]
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
 jobs:
   build-sign-android:
     name: Build and sign Android
@@ -24,7 +28,7 @@ jobs:
           github_ref="${{ github.sha }}"
           ref="${input_ref:-$github_ref}"
           echo "ref=$ref" >> $GITHUB_OUTPUT
-          
+
       - uses: actions/checkout@v3
         with:
           ref: ${{ steps.get-ref.outputs.ref }}

+ 7 - 3
.github/workflows/cache-cleanup.yml

@@ -4,24 +4,28 @@ on:
     types:
       - closed
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
 jobs:
   cleanup:
     runs-on: ubuntu-latest
     steps:
       - name: Check out code
         uses: actions/checkout@v3
-        
+
       - name: Cleanup
         run: |
           gh extension install actions/gh-actions-cache
-          
+
           REPO=${{ github.repository }}
           BRANCH=${{ github.ref }}
 
           echo "Fetching list of cache keys"
           cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 )
 
-          ## Setting this to not fail the workflow while deleting cache keys. 
+          ## Setting this to not fail the workflow while deleting cache keys.
           set +e
           echo "Deleting caches..."
           for cacheKey in $cacheKeysForPR

+ 7 - 3
.github/workflows/codeql-analysis.yml

@@ -20,6 +20,10 @@ on:
   schedule:
     - cron: '20 13 * * 1'
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
 jobs:
   analyze:
     name: Analyze
@@ -48,11 +52,11 @@ jobs:
         # If you wish to specify custom queries, you can do so here or in a config file.
         # By default, queries listed here will override any specified in a config file.
         # Prefix the list here with "+" to use these queries and those in the config file.
-        
+
         # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
         # queries: security-extended,security-and-quality
 
-        
+
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
     # If this step fails, then you should remove it and run the build manually (see below)
     - name: Autobuild
@@ -61,7 +65,7 @@ jobs:
     # ℹ️ Command-line programs to run using the OS shell.
     # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
 
-    #   If the Autobuild fails above, remove it and uncomment the following three lines. 
+    #   If the Autobuild fails above, remove it and uncomment the following three lines.
     #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
 
     # - run: |

+ 4 - 0
.github/workflows/dispatch_sdk_update.yml

@@ -5,6 +5,10 @@ on:
   push:
     branches: ["main"]
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
 jobs:
   update-sdk-repos:
     runs-on: ubuntu-latest

+ 4 - 0
.github/workflows/docker.yml

@@ -9,6 +9,10 @@ on:
   release:
     types: [published]
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
 jobs:
   build_and_push:
     runs-on: ubuntu-latest

+ 4 - 0
.github/workflows/github-repo-stats.yml

@@ -7,6 +7,10 @@ on:
     - cron: "0 23 * * *"
   workflow_dispatch: # Allow for running this manually.
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
 jobs:
   j1:
     name: github-repo-stats

+ 8 - 4
.github/workflows/prepare-release.yml

@@ -17,13 +17,17 @@ on:
         required: false
         type: boolean
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
 jobs:
   bump_version:
     runs-on: ubuntu-latest
-    
+
     outputs:
       ref: ${{ steps.push-tag.outputs.commit_long_sha }}
-    
+
     steps:
       - name: Checkout
         uses: actions/checkout@v3
@@ -42,7 +46,7 @@ jobs:
           message: "Version ${{ env.IMMICH_VERSION }}"
           tag: ${{ env.IMMICH_VERSION }}
           push: true
-          
+
   build_mobile:
     uses: ./.github/workflows/build-mobile.yml
     needs: bump_version
@@ -59,7 +63,7 @@ jobs:
         uses: actions/checkout@v3
         with:
           token: ${{ secrets.ORG_RELEASE_TOKEN }}
-          
+
       - name: Download APK
         uses: actions/download-artifact@v3
         with:

+ 4 - 0
.github/workflows/static_analysis.yml

@@ -5,6 +5,10 @@ on:
   push:
     branches: [main]
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
 jobs:
   mobile-dart-analyze:
     name: Run Dart Code Analysis

+ 25 - 0
.github/workflows/test.yml

@@ -5,6 +5,10 @@ on:
   push:
     branches: [main]
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
 jobs:
   e2e-tests:
     name: Run end-to-end test suites
@@ -54,6 +58,27 @@ jobs:
         working-directory: ./mobile
         run: flutter test
 
+  generated-api-up-to-date:
+    name: Check generated files are up-to-date
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Run API generation
+        run: cd server && npm ci && npm run api:generate
+      - name: Find file changes
+        uses: tj-actions/verify-changed-files@v13.1
+        id: verify-changed-files
+        with:
+          files: |
+            mobile/openapi
+            web/src/api/open-api
+      - name: Verify files have not changed
+        if: steps.verify-changed-files.outputs.files_changed == 'true'
+        run: |
+          echo "ERROR: Generated files not up to date!"
+          echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
+          exit 1
+
   mobile-integration-tests:
     name: Run mobile end-to-end integration tests
     runs-on: macos-latest

+ 4 - 0
mobile/.fvm/fvm_config.json

@@ -0,0 +1,4 @@
+{
+  "flutterSdkVersion": "3.7.0",
+  "flavors": {}
+}

+ 2 - 1
mobile/.gitignore

@@ -32,6 +32,7 @@
 .pub-cache/
 .pub/
 /build/
+.fvm/flutter_sdk
 
 # Web related
 lib/generated_plugin_registrant.dart
@@ -48,4 +49,4 @@ app.*.map.json
 /android/app/release
 
 # Fastlane
-ios/fastlane/report.xml
+ios/fastlane/report.xml

+ 2 - 2
mobile/lib/modules/album/views/album_viewer_page.dart

@@ -120,8 +120,8 @@ class AlbumViewerPage extends HookConsumerWidget {
     }
 
     Widget buildAlbumDateRange(Album album) {
-      final DateTime startDate = album.assets.first.createdAt;
-      final DateTime endDate = album.assets.last.createdAt; //Need default.
+      final DateTime startDate = album.assets.first.fileCreatedAt;
+      final DateTime endDate = album.assets.last.fileCreatedAt; //Need default.
       final String startDateText =
           DateFormat(startDate.year == endDate.year ? 'LLL d' : 'LLL d, y')
               .format(startDate);

+ 1 - 1
mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart

@@ -146,7 +146,7 @@ class ExifBottomSheet extends HookConsumerWidget {
     buildDate() {
       return Text(
         DateFormat('date_format'.tr()).format(
-          assetDetail.createdAt.toLocal(),
+          assetDetail.fileCreatedAt.toLocal(),
         ),
         style: const TextStyle(
           fontWeight: FontWeight.bold,

+ 9 - 9
mobile/lib/modules/backup/models/current_upload_asset.model.dart

@@ -2,26 +2,26 @@ import 'dart:convert';
 
 class CurrentUploadAsset {
   final String id;
-  final DateTime createdAt;
+  final DateTime fileCreatedAt;
   final String fileName;
   final String fileType;
 
   CurrentUploadAsset({
     required this.id,
-    required this.createdAt,
+    required this.fileCreatedAt,
     required this.fileName,
     required this.fileType,
   });
 
   CurrentUploadAsset copyWith({
     String? id,
-    DateTime? createdAt,
+    DateTime? fileCreatedAt,
     String? fileName,
     String? fileType,
   }) {
     return CurrentUploadAsset(
       id: id ?? this.id,
-      createdAt: createdAt ?? this.createdAt,
+      fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt,
       fileName: fileName ?? this.fileName,
       fileType: fileType ?? this.fileType,
     );
@@ -31,7 +31,7 @@ class CurrentUploadAsset {
     final result = <String, dynamic>{};
 
     result.addAll({'id': id});
-    result.addAll({'createdAt': createdAt.millisecondsSinceEpoch});
+    result.addAll({'fileCreatedAt': fileCreatedAt.millisecondsSinceEpoch});
     result.addAll({'fileName': fileName});
     result.addAll({'fileType': fileType});
 
@@ -41,7 +41,7 @@ class CurrentUploadAsset {
   factory CurrentUploadAsset.fromMap(Map<String, dynamic> map) {
     return CurrentUploadAsset(
       id: map['id'] ?? '',
-      createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt']),
+      fileCreatedAt: DateTime.fromMillisecondsSinceEpoch(map['fileCreatedAt']),
       fileName: map['fileName'] ?? '',
       fileType: map['fileType'] ?? '',
     );
@@ -54,7 +54,7 @@ class CurrentUploadAsset {
 
   @override
   String toString() {
-    return 'CurrentUploadAsset(id: $id, createdAt: $createdAt, fileName: $fileName, fileType: $fileType)';
+    return 'CurrentUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType)';
   }
 
   @override
@@ -63,7 +63,7 @@ class CurrentUploadAsset {
 
     return other is CurrentUploadAsset &&
         other.id == id &&
-        other.createdAt == createdAt &&
+        other.fileCreatedAt == fileCreatedAt &&
         other.fileName == fileName &&
         other.fileType == fileType;
   }
@@ -71,7 +71,7 @@ class CurrentUploadAsset {
   @override
   int get hashCode {
     return id.hashCode ^
-        createdAt.hashCode ^
+        fileCreatedAt.hashCode ^
         fileName.hashCode ^
         fileType.hashCode;
   }

+ 7 - 7
mobile/lib/modules/backup/models/error_upload_asset.model.dart

@@ -2,7 +2,7 @@ import 'package:photo_manager/photo_manager.dart';
 
 class ErrorUploadAsset {
   final String id;
-  final DateTime createdAt;
+  final DateTime fileCreatedAt;
   final String fileName;
   final String fileType;
   final AssetEntity asset;
@@ -10,7 +10,7 @@ class ErrorUploadAsset {
 
   const ErrorUploadAsset({
     required this.id,
-    required this.createdAt,
+    required this.fileCreatedAt,
     required this.fileName,
     required this.fileType,
     required this.asset,
@@ -19,7 +19,7 @@ class ErrorUploadAsset {
 
   ErrorUploadAsset copyWith({
     String? id,
-    DateTime? createdAt,
+    DateTime? fileCreatedAt,
     String? fileName,
     String? fileType,
     AssetEntity? asset,
@@ -27,7 +27,7 @@ class ErrorUploadAsset {
   }) {
     return ErrorUploadAsset(
       id: id ?? this.id,
-      createdAt: createdAt ?? this.createdAt,
+      fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt,
       fileName: fileName ?? this.fileName,
       fileType: fileType ?? this.fileType,
       asset: asset ?? this.asset,
@@ -37,7 +37,7 @@ class ErrorUploadAsset {
 
   @override
   String toString() {
-    return 'ErrorUploadAsset(id: $id, createdAt: $createdAt, fileName: $fileName, fileType: $fileType, asset: $asset, errorMessage: $errorMessage)';
+    return 'ErrorUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, asset: $asset, errorMessage: $errorMessage)';
   }
 
   @override
@@ -46,7 +46,7 @@ class ErrorUploadAsset {
 
     return other is ErrorUploadAsset &&
         other.id == id &&
-        other.createdAt == createdAt &&
+        other.fileCreatedAt == fileCreatedAt &&
         other.fileName == fileName &&
         other.fileType == fileType &&
         other.asset == asset &&
@@ -56,7 +56,7 @@ class ErrorUploadAsset {
   @override
   int get hashCode {
     return id.hashCode ^
-        createdAt.hashCode ^
+        fileCreatedAt.hashCode ^
         fileName.hashCode ^
         fileType.hashCode ^
         asset.hashCode ^

+ 1 - 1
mobile/lib/modules/backup/providers/backup.provider.dart

@@ -55,7 +55,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
             selectedAlbumsBackupAssetsIds: const {},
             currentUploadAsset: CurrentUploadAsset(
               id: '...',
-              createdAt: DateTime.parse('2020-10-04'),
+              fileCreatedAt: DateTime.parse('2020-10-04'),
               fileName: '...',
               fileType: '...',
             ),

+ 4 - 4
mobile/lib/modules/backup/services/backup.service.dart

@@ -260,8 +260,8 @@ class BackupService {
           req.fields['deviceAssetId'] = entity.id;
           req.fields['deviceId'] = deviceId;
           req.fields['assetType'] = _getAssetType(entity.type);
-          req.fields['createdAt'] = entity.createDateTime.toIso8601String();
-          req.fields['modifiedAt'] = entity.modifiedDateTime.toIso8601String();
+          req.fields['fileCreatedAt'] = entity.createDateTime.toIso8601String();
+          req.fields['fileModifiedAt'] = entity.modifiedDateTime.toIso8601String();
           req.fields['isFavorite'] = entity.isFavorite.toString();
           req.fields['fileExtension'] = fileExtension;
           req.fields['duration'] = entity.videoDuration.toString();
@@ -278,7 +278,7 @@ class BackupService {
           setCurrentUploadAssetCb(
             CurrentUploadAsset(
               id: entity.id,
-              createdAt: entity.createDateTime.year == 1970
+              fileCreatedAt: entity.createDateTime.year == 1970
                   ? entity.modifiedDateTime
                   : entity.createDateTime,
               fileName: originalFileName,
@@ -308,7 +308,7 @@ class BackupService {
               ErrorUploadAsset(
                 asset: entity,
                 id: entity.id,
-                createdAt: entity.createDateTime,
+                fileCreatedAt: entity.createDateTime,
                 fileName: originalFileName,
                 fileType: _getAssetType(entity.type),
                 errorMessage: error['error'],

+ 1 - 1
mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart

@@ -20,7 +20,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
     String getAssetCreationDate() {
       return DateFormat.yMMMMd('en_US').format(
         DateTime.parse(
-          asset.createdAt.toString(),
+          asset.fileCreatedAt.toString(),
         ).toLocal(),
       );
     }

+ 1 - 1
mobile/lib/modules/backup/views/failed_backup_status_page.dart

@@ -89,7 +89,7 @@ class FailedBackupStatusPage extends HookConsumerWidget {
                               Text(
                                 DateFormat.yMMMMd('en_US').format(
                                   DateTime.parse(
-                                    errorAsset.createdAt.toString(),
+                                    errorAsset.fileCreatedAt.toString(),
                                   ).toLocal(),
                                 ),
                                 style: TextStyle(

+ 2 - 2
mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart

@@ -82,14 +82,14 @@ class RenderList {
     if (groupBy == GroupAssetsBy.day) {
       return assets.groupListsBy(
         (element) {
-          final date = element.createdAt.toLocal();
+          final date = element.fileCreatedAt.toLocal();
           return DateTime(date.year, date.month, date.day);
         },
       );
     } else if (groupBy == GroupAssetsBy.month) {
       return assets.groupListsBy(
         (element) {
-          final date = element.createdAt.toLocal();
+          final date = element.fileCreatedAt.toLocal();
           return DateTime(date.year, date.month);
         },
       );

+ 18 - 18
mobile/lib/shared/models/asset.dart

@@ -10,8 +10,8 @@ import 'package:path/path.dart' as p;
 class Asset {
   Asset.remote(AssetResponseDto remote)
       : remoteId = remote.id,
-        createdAt = DateTime.parse(remote.createdAt),
-        modifiedAt = DateTime.parse(remote.modifiedAt),
+        fileCreatedAt = DateTime.parse(remote.fileCreatedAt),
+        fileModifiedAt = DateTime.parse(remote.fileModifiedAt),
         durationInSeconds = remote.duration.toDuration().inSeconds,
         fileName = p.basename(remote.originalPath),
         height = remote.exifInfo?.exifImageHeight?.toInt(),
@@ -37,11 +37,11 @@ class Asset {
         deviceAssetId = local.id,
         deviceId = Hive.box(userInfoBox).get(deviceIdKey),
         ownerId = owner,
-        modifiedAt = local.modifiedDateTime.toUtc(),
+        fileModifiedAt = local.modifiedDateTime.toUtc(),
         isFavorite = local.isFavorite,
-        createdAt = local.createDateTime.toUtc() {
-    if (createdAt.year == 1970) {
-      createdAt = modifiedAt;
+        fileCreatedAt = local.createDateTime.toUtc() {
+    if (fileCreatedAt.year == 1970) {
+      fileCreatedAt = fileModifiedAt;
     }
   }
 
@@ -51,8 +51,8 @@ class Asset {
     required this.deviceAssetId,
     required this.deviceId,
     required this.ownerId,
-    required this.createdAt,
-    required this.modifiedAt,
+    required this.fileCreatedAt,
+    required this.fileModifiedAt,
     this.latitude,
     this.longitude,
     required this.durationInSeconds,
@@ -74,10 +74,10 @@ class Asset {
         width: width!,
         height: height!,
         duration: durationInSeconds,
-        createDateSecond: createdAt.millisecondsSinceEpoch ~/ 1000,
+        createDateSecond: fileCreatedAt.millisecondsSinceEpoch ~/ 1000,
         latitude: latitude,
         longitude: longitude,
-        modifiedDateSecond: modifiedAt.millisecondsSinceEpoch ~/ 1000,
+        modifiedDateSecond: fileModifiedAt.millisecondsSinceEpoch ~/ 1000,
         title: fileName,
       );
     }
@@ -94,9 +94,9 @@ class Asset {
 
   String ownerId;
 
-  DateTime createdAt;
+  DateTime fileCreatedAt;
 
-  DateTime modifiedAt;
+  DateTime fileModifiedAt;
 
   double? latitude;
 
@@ -146,8 +146,8 @@ class Asset {
     json["deviceAssetId"] = deviceAssetId;
     json["deviceId"] = deviceId;
     json["ownerId"] = ownerId;
-    json["createdAt"] = createdAt.millisecondsSinceEpoch;
-    json["modifiedAt"] = modifiedAt.millisecondsSinceEpoch;
+    json["fileCreatedAt"] = fileCreatedAt.millisecondsSinceEpoch;
+    json["fileModifiedAt"] = fileModifiedAt.millisecondsSinceEpoch;
     json["latitude"] = latitude;
     json["longitude"] = longitude;
     json["durationInSeconds"] = durationInSeconds;
@@ -171,10 +171,10 @@ class Asset {
         deviceAssetId: json["deviceAssetId"],
         deviceId: json["deviceId"],
         ownerId: json["ownerId"],
-        createdAt:
-            DateTime.fromMillisecondsSinceEpoch(json["createdAt"], isUtc: true),
-        modifiedAt: DateTime.fromMillisecondsSinceEpoch(
-          json["modifiedAt"],
+        fileCreatedAt:
+            DateTime.fromMillisecondsSinceEpoch(json["fileCreatedAt"], isUtc: true),
+        fileModifiedAt: DateTime.fromMillisecondsSinceEpoch(
+          json["fileModifiedAt"],
           isUtc: true,
         ),
         latitude: json["latitude"],

+ 2 - 2
mobile/lib/shared/providers/asset.provider.dart

@@ -302,11 +302,11 @@ final assetGroupByMonthYearProvider = StateProvider((ref) {
       ref.watch(assetProvider).allAssets.where((e) => e.isRemote).toList();
 
   assets.sortByCompare<DateTime>(
-    (e) => e.createdAt,
+    (e) => e.fileCreatedAt,
     (a, b) => b.compareTo(a),
   );
 
   return assets.groupListsBy(
-    (element) => DateFormat('MMMM, y').format(element.createdAt.toLocal()),
+    (element) => DateFormat('MMMM, y').format(element.fileCreatedAt.toLocal()),
   );
 });

+ 1 - 1
mobile/openapi/README.md

@@ -3,7 +3,7 @@ Immich API
 
 This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
 
-- API version: 1.47.2
+- API version: 1.47.3
 - Build package: org.openapitools.codegen.languages.DartClientCodegen
 
 ## Requirements

+ 6 - 6
mobile/openapi/doc/APIKeyApi.md

@@ -71,7 +71,7 @@ No authorization required
 import 'package:openapi/api.dart';
 
 final api_instance = APIKeyApi();
-final id = 8.14; // num | 
+final id = id_example; // String | 
 
 try {
     api_instance.deleteKey(id);
@@ -84,7 +84,7 @@ try {
 
 Name | Type | Description  | Notes
 ------------- | ------------- | ------------- | -------------
- **id** | **num**|  | 
+ **id** | **String**|  | 
 
 ### Return type
 
@@ -113,7 +113,7 @@ No authorization required
 import 'package:openapi/api.dart';
 
 final api_instance = APIKeyApi();
-final id = 8.14; // num | 
+final id = id_example; // String | 
 
 try {
     final result = api_instance.getKey(id);
@@ -127,7 +127,7 @@ try {
 
 Name | Type | Description  | Notes
 ------------- | ------------- | ------------- | -------------
- **id** | **num**|  | 
+ **id** | **String**|  | 
 
 ### Return type
 
@@ -195,7 +195,7 @@ No authorization required
 import 'package:openapi/api.dart';
 
 final api_instance = APIKeyApi();
-final id = 8.14; // num | 
+final id = id_example; // String | 
 final aPIKeyUpdateDto = APIKeyUpdateDto(); // APIKeyUpdateDto | 
 
 try {
@@ -210,7 +210,7 @@ try {
 
 Name | Type | Description  | Notes
 ------------- | ------------- | ------------- | -------------
- **id** | **num**|  | 
+ **id** | **String**|  | 
  **aPIKeyUpdateDto** | [**APIKeyUpdateDto**](APIKeyUpdateDto.md)|  | 
 
 ### Return type

+ 1 - 1
mobile/openapi/doc/APIKeyResponseDto.md

@@ -8,7 +8,7 @@ import 'package:openapi/api.dart';
 ## Properties
 Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
-**id** | **int** |  | 
+**id** | **String** |  | 
 **name** | **String** |  | 
 **createdAt** | **String** |  | 
 **updatedAt** | **String** |  | 

+ 6 - 6
mobile/openapi/doc/AssetApi.md

@@ -1109,7 +1109,7 @@ Name | Type | Description  | Notes
 [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 
 # **uploadFile**
-> AssetFileUploadResponseDto uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration)
+> AssetFileUploadResponseDto uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration)
 
 
 
@@ -1130,8 +1130,8 @@ final assetType = ; // AssetTypeEnum |
 final assetData = BINARY_DATA_HERE; // MultipartFile | 
 final deviceAssetId = deviceAssetId_example; // String | 
 final deviceId = deviceId_example; // String | 
-final createdAt = createdAt_example; // String | 
-final modifiedAt = modifiedAt_example; // String | 
+final fileCreatedAt = fileCreatedAt_example; // String | 
+final fileModifiedAt = fileModifiedAt_example; // String | 
 final isFavorite = true; // bool | 
 final fileExtension = fileExtension_example; // String | 
 final livePhotoData = BINARY_DATA_HERE; // MultipartFile | 
@@ -1139,7 +1139,7 @@ final isVisible = true; // bool |
 final duration = duration_example; // String | 
 
 try {
-    final result = api_instance.uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration);
+    final result = api_instance.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration);
     print(result);
 } catch (e) {
     print('Exception when calling AssetApi->uploadFile: $e\n');
@@ -1154,8 +1154,8 @@ Name | Type | Description  | Notes
  **assetData** | **MultipartFile**|  | 
  **deviceAssetId** | **String**|  | 
  **deviceId** | **String**|  | 
- **createdAt** | **String**|  | 
- **modifiedAt** | **String**|  | 
+ **fileCreatedAt** | **String**|  | 
+ **fileModifiedAt** | **String**|  | 
  **isFavorite** | **bool**|  | 
  **fileExtension** | **String**|  | 
  **livePhotoData** | **MultipartFile**|  | [optional] 

+ 2 - 2
mobile/openapi/doc/AssetResponseDto.md

@@ -15,8 +15,8 @@ Name | Type | Description | Notes
 **deviceId** | **String** |  | 
 **originalPath** | **String** |  | 
 **resizePath** | **String** |  | 
-**createdAt** | **String** |  | 
-**modifiedAt** | **String** |  | 
+**fileCreatedAt** | **String** |  | 
+**fileModifiedAt** | **String** |  | 
 **updatedAt** | **String** |  | 
 **isFavorite** | **bool** |  | 
 **mimeType** | **String** |  | 

+ 15 - 15
mobile/openapi/lib/api/api_key_api.dart

@@ -74,11 +74,11 @@ class APIKeyApi {
   ///
   /// Parameters:
   ///
-  /// * [num] id (required):
-  Future<Response> deleteKeyWithHttpInfo(num id,) async {
+  /// * [String] id (required):
+  Future<Response> deleteKeyWithHttpInfo(String id,) async {
     // ignore: prefer_const_declarations
     final path = r'/api-key/{id}'
-      .replaceAll('{id}', id.toString());
+      .replaceAll('{id}', id);
 
     // ignore: prefer_final_locals
     Object? postBody;
@@ -105,8 +105,8 @@ class APIKeyApi {
   ///
   /// Parameters:
   ///
-  /// * [num] id (required):
-  Future<void> deleteKey(num id,) async {
+  /// * [String] id (required):
+  Future<void> deleteKey(String id,) async {
     final response = await deleteKeyWithHttpInfo(id,);
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
@@ -119,11 +119,11 @@ class APIKeyApi {
   ///
   /// Parameters:
   ///
-  /// * [num] id (required):
-  Future<Response> getKeyWithHttpInfo(num id,) async {
+  /// * [String] id (required):
+  Future<Response> getKeyWithHttpInfo(String id,) async {
     // ignore: prefer_const_declarations
     final path = r'/api-key/{id}'
-      .replaceAll('{id}', id.toString());
+      .replaceAll('{id}', id);
 
     // ignore: prefer_final_locals
     Object? postBody;
@@ -150,8 +150,8 @@ class APIKeyApi {
   ///
   /// Parameters:
   ///
-  /// * [num] id (required):
-  Future<APIKeyResponseDto?> getKey(num id,) async {
+  /// * [String] id (required):
+  Future<APIKeyResponseDto?> getKey(String id,) async {
     final response = await getKeyWithHttpInfo(id,);
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
@@ -219,13 +219,13 @@ class APIKeyApi {
   ///
   /// Parameters:
   ///
-  /// * [num] id (required):
+  /// * [String] id (required):
   ///
   /// * [APIKeyUpdateDto] aPIKeyUpdateDto (required):
-  Future<Response> updateKeyWithHttpInfo(num id, APIKeyUpdateDto aPIKeyUpdateDto,) async {
+  Future<Response> updateKeyWithHttpInfo(String id, APIKeyUpdateDto aPIKeyUpdateDto,) async {
     // ignore: prefer_const_declarations
     final path = r'/api-key/{id}'
-      .replaceAll('{id}', id.toString());
+      .replaceAll('{id}', id);
 
     // ignore: prefer_final_locals
     Object? postBody = aPIKeyUpdateDto;
@@ -252,10 +252,10 @@ class APIKeyApi {
   ///
   /// Parameters:
   ///
-  /// * [num] id (required):
+  /// * [String] id (required):
   ///
   /// * [APIKeyUpdateDto] aPIKeyUpdateDto (required):
-  Future<APIKeyResponseDto?> updateKey(num id, APIKeyUpdateDto aPIKeyUpdateDto,) async {
+  Future<APIKeyResponseDto?> updateKey(String id, APIKeyUpdateDto aPIKeyUpdateDto,) async {
     final response = await updateKeyWithHttpInfo(id, aPIKeyUpdateDto,);
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));

+ 11 - 11
mobile/openapi/lib/api/asset_api.dart

@@ -1224,9 +1224,9 @@ class AssetApi {
   ///
   /// * [String] deviceId (required):
   ///
-  /// * [String] createdAt (required):
+  /// * [String] fileCreatedAt (required):
   ///
-  /// * [String] modifiedAt (required):
+  /// * [String] fileModifiedAt (required):
   ///
   /// * [bool] isFavorite (required):
   ///
@@ -1237,7 +1237,7 @@ class AssetApi {
   /// * [bool] isVisible:
   ///
   /// * [String] duration:
-  Future<Response> uploadFileWithHttpInfo(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String createdAt, String modifiedAt, bool isFavorite, String fileExtension, { MultipartFile? livePhotoData, bool? isVisible, String? duration, }) async {
+  Future<Response> uploadFileWithHttpInfo(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String fileCreatedAt, String fileModifiedAt, bool isFavorite, String fileExtension, { MultipartFile? livePhotoData, bool? isVisible, String? duration, }) async {
     // ignore: prefer_const_declarations
     final path = r'/asset/upload';
 
@@ -1274,13 +1274,13 @@ class AssetApi {
       hasFields = true;
       mp.fields[r'deviceId'] = parameterToString(deviceId);
     }
-    if (createdAt != null) {
+    if (fileCreatedAt != null) {
       hasFields = true;
-      mp.fields[r'createdAt'] = parameterToString(createdAt);
+      mp.fields[r'fileCreatedAt'] = parameterToString(fileCreatedAt);
     }
-    if (modifiedAt != null) {
+    if (fileModifiedAt != null) {
       hasFields = true;
-      mp.fields[r'modifiedAt'] = parameterToString(modifiedAt);
+      mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt);
     }
     if (isFavorite != null) {
       hasFields = true;
@@ -1325,9 +1325,9 @@ class AssetApi {
   ///
   /// * [String] deviceId (required):
   ///
-  /// * [String] createdAt (required):
+  /// * [String] fileCreatedAt (required):
   ///
-  /// * [String] modifiedAt (required):
+  /// * [String] fileModifiedAt (required):
   ///
   /// * [bool] isFavorite (required):
   ///
@@ -1338,8 +1338,8 @@ class AssetApi {
   /// * [bool] isVisible:
   ///
   /// * [String] duration:
-  Future<AssetFileUploadResponseDto?> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String createdAt, String modifiedAt, bool isFavorite, String fileExtension, { MultipartFile? livePhotoData, bool? isVisible, String? duration, }) async {
-    final response = await uploadFileWithHttpInfo(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension,  livePhotoData: livePhotoData, isVisible: isVisible, duration: duration, );
+  Future<AssetFileUploadResponseDto?> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String fileCreatedAt, String fileModifiedAt, bool isFavorite, String fileExtension, { MultipartFile? livePhotoData, bool? isVisible, String? duration, }) async {
+    final response = await uploadFileWithHttpInfo(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension,  livePhotoData: livePhotoData, isVisible: isVisible, duration: duration, );
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }

+ 2 - 2
mobile/openapi/lib/model/api_key_response_dto.dart

@@ -19,7 +19,7 @@ class APIKeyResponseDto {
     required this.updatedAt,
   });
 
-  int id;
+  String id;
 
   String name;
 
@@ -73,7 +73,7 @@ class APIKeyResponseDto {
       }());
 
       return APIKeyResponseDto(
-        id: mapValueOfType<int>(json, r'id')!,
+        id: mapValueOfType<String>(json, r'id')!,
         name: mapValueOfType<String>(json, r'name')!,
         createdAt: mapValueOfType<String>(json, r'createdAt')!,
         updatedAt: mapValueOfType<String>(json, r'updatedAt')!,

+ 15 - 15
mobile/openapi/lib/model/asset_response_dto.dart

@@ -20,8 +20,8 @@ class AssetResponseDto {
     required this.deviceId,
     required this.originalPath,
     required this.resizePath,
-    required this.createdAt,
-    required this.modifiedAt,
+    required this.fileCreatedAt,
+    required this.fileModifiedAt,
     required this.updatedAt,
     required this.isFavorite,
     required this.mimeType,
@@ -48,9 +48,9 @@ class AssetResponseDto {
 
   String? resizePath;
 
-  String createdAt;
+  String fileCreatedAt;
 
-  String modifiedAt;
+  String fileModifiedAt;
 
   String updatedAt;
 
@@ -93,8 +93,8 @@ class AssetResponseDto {
      other.deviceId == deviceId &&
      other.originalPath == originalPath &&
      other.resizePath == resizePath &&
-     other.createdAt == createdAt &&
-     other.modifiedAt == modifiedAt &&
+     other.fileCreatedAt == fileCreatedAt &&
+     other.fileModifiedAt == fileModifiedAt &&
      other.updatedAt == updatedAt &&
      other.isFavorite == isFavorite &&
      other.mimeType == mimeType &&
@@ -116,8 +116,8 @@ class AssetResponseDto {
     (deviceId.hashCode) +
     (originalPath.hashCode) +
     (resizePath == null ? 0 : resizePath!.hashCode) +
-    (createdAt.hashCode) +
-    (modifiedAt.hashCode) +
+    (fileCreatedAt.hashCode) +
+    (fileModifiedAt.hashCode) +
     (updatedAt.hashCode) +
     (isFavorite.hashCode) +
     (mimeType == null ? 0 : mimeType!.hashCode) +
@@ -130,7 +130,7 @@ class AssetResponseDto {
     (tags.hashCode);
 
   @override
-  String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, updatedAt=$updatedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags]';
+  String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, updatedAt=$updatedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -145,8 +145,8 @@ class AssetResponseDto {
     } else {
       // json[r'resizePath'] = null;
     }
-      json[r'createdAt'] = this.createdAt;
-      json[r'modifiedAt'] = this.modifiedAt;
+      json[r'fileCreatedAt'] = this.fileCreatedAt;
+      json[r'fileModifiedAt'] = this.fileModifiedAt;
       json[r'updatedAt'] = this.updatedAt;
       json[r'isFavorite'] = this.isFavorite;
     if (this.mimeType != null) {
@@ -210,8 +210,8 @@ class AssetResponseDto {
         deviceId: mapValueOfType<String>(json, r'deviceId')!,
         originalPath: mapValueOfType<String>(json, r'originalPath')!,
         resizePath: mapValueOfType<String>(json, r'resizePath'),
-        createdAt: mapValueOfType<String>(json, r'createdAt')!,
-        modifiedAt: mapValueOfType<String>(json, r'modifiedAt')!,
+        fileCreatedAt: mapValueOfType<String>(json, r'fileCreatedAt')!,
+        fileModifiedAt: mapValueOfType<String>(json, r'fileModifiedAt')!,
         updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
         isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
         mimeType: mapValueOfType<String>(json, r'mimeType'),
@@ -278,8 +278,8 @@ class AssetResponseDto {
     'deviceId',
     'originalPath',
     'resizePath',
-    'createdAt',
-    'modifiedAt',
+    'fileCreatedAt',
+    'fileModifiedAt',
     'updatedAt',
     'isFavorite',
     'mimeType',

+ 3 - 3
mobile/openapi/test/api_key_api_test.dart

@@ -26,14 +26,14 @@ void main() {
 
     // 
     //
-    //Future deleteKey(num id) async
+    //Future deleteKey(String id) async
     test('test deleteKey', () async {
       // TODO
     });
 
     // 
     //
-    //Future<APIKeyResponseDto> getKey(num id) async
+    //Future<APIKeyResponseDto> getKey(String id) async
     test('test getKey', () async {
       // TODO
     });
@@ -47,7 +47,7 @@ void main() {
 
     // 
     //
-    //Future<APIKeyResponseDto> updateKey(num id, APIKeyUpdateDto aPIKeyUpdateDto) async
+    //Future<APIKeyResponseDto> updateKey(String id, APIKeyUpdateDto aPIKeyUpdateDto) async
     test('test updateKey', () async {
       // TODO
     });

+ 1 - 1
mobile/openapi/test/api_key_response_dto_test.dart

@@ -16,7 +16,7 @@ void main() {
   // final instance = APIKeyResponseDto();
 
   group('test APIKeyResponseDto', () {
-    // int id
+    // String id
     test('to test the property `id`', () async {
       // TODO
     });

+ 1 - 1
mobile/openapi/test/asset_api_test.dart

@@ -173,7 +173,7 @@ void main() {
 
     // 
     //
-    //Future<AssetFileUploadResponseDto> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String createdAt, String modifiedAt, bool isFavorite, String fileExtension, { MultipartFile livePhotoData, bool isVisible, String duration }) async
+    //Future<AssetFileUploadResponseDto> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String fileCreatedAt, String fileModifiedAt, bool isFavorite, String fileExtension, { MultipartFile livePhotoData, bool isVisible, String duration }) async
     test('test uploadFile', () async {
       // TODO
     });

+ 4 - 4
mobile/openapi/test/asset_response_dto_test.dart

@@ -51,13 +51,13 @@ void main() {
       // TODO
     });
 
-    // String createdAt
-    test('to test the property `createdAt`', () async {
+    // String fileCreatedAt
+    test('to test the property `fileCreatedAt`', () async {
       // TODO
     });
 
-    // String modifiedAt
-    test('to test the property `modifiedAt`', () async {
+    // String fileModifiedAt
+    test('to test the property `fileModifiedAt`', () async {
       // TODO
     });
 

+ 1 - 1
mobile/pubspec.lock

@@ -1513,5 +1513,5 @@ packages:
     source: hosted
     version: "3.1.1"
 sdks:
-  dart: ">=2.19.0 <3.0.0"
+  dart: ">=2.19.0 <4.0.0"
   flutter: ">=3.3.0"

+ 6 - 6
mobile/test/asset_grid_data_structure_test.dart

@@ -16,8 +16,8 @@ void main() {
         deviceAssetId: '$i',
         deviceId: '',
         ownerId: '',
-        createdAt: date,
-        modifiedAt: date,
+        fileCreatedAt: date,
+        fileModifiedAt: date,
         durationInSeconds: 0,
         fileName: '',
         isFavorite: false,
@@ -29,25 +29,25 @@ void main() {
 
   assets.addAll(
     testAssets.sublist(0, 5).map((e) {
-      e.createdAt = DateTime(2022, 1, 5);
+      e.fileCreatedAt = DateTime(2022, 1, 5);
       return e;
     }).toList(),
   );
   assets.addAll(
     testAssets.sublist(5, 10).map((e) {
-      e.createdAt = DateTime(2022, 1, 10);
+      e.fileCreatedAt = DateTime(2022, 1, 10);
       return e;
     }).toList(),
   );
   assets.addAll(
     testAssets.sublist(10, 15).map((e) {
-      e.createdAt = DateTime(2022, 2, 17);
+      e.fileCreatedAt = DateTime(2022, 2, 17);
       return e;
     }).toList(),
   );
   assets.addAll(
     testAssets.sublist(15, 30).map((e) {
-      e.createdAt = DateTime(2022, 10, 15);
+      e.fileCreatedAt = DateTime(2022, 10, 15);
       return e;
     }).toList(),
   );

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

@@ -79,7 +79,7 @@ export class AlbumRepository implements IAlbumRepository {
 
     const queryProperties: FindManyOptions<AlbumEntity> = {
       relations: { sharedUsers: true, assets: true, sharedLinks: true, owner: true },
-      order: { assets: { createdAt: 'ASC' }, createdAt: 'ASC' },
+      order: { assets: { fileCreatedAt: 'ASC' }, createdAt: 'ASC' },
     };
 
     let albumsQuery: Promise<AlbumEntity[]>;
@@ -123,7 +123,7 @@ export class AlbumRepository implements IAlbumRepository {
     const albums = await this.albumRepository.find({
       where: { ownerId: userId, assets: { id: assetId } },
       relations: { owner: true, assets: true, sharedUsers: true },
-      order: { assets: { createdAt: 'ASC' } },
+      order: { assets: { fileCreatedAt: 'ASC' } },
     });
 
     return albums;
@@ -142,7 +142,7 @@ export class AlbumRepository implements IAlbumRepository {
       },
       order: {
         assets: {
-          createdAt: 'ASC',
+          fileCreatedAt: 'ASC',
         },
       },
     });

+ 29 - 25
server/apps/immich/src/api-v1/asset/asset-repository.ts

@@ -19,7 +19,9 @@ import { AssetSearchDto } from './dto/asset-search.dto';
 
 export interface IAssetRepository {
   get(id: string): Promise<AssetEntity | null>;
-  create(asset: Omit<AssetEntity, 'id'>): Promise<AssetEntity>;
+  create(
+    asset: Omit<AssetEntity, 'id' | 'createdAt' | 'updatedAt' | 'ownerId' | 'livePhotoVideoId'>,
+  ): Promise<AssetEntity>;
   remove(asset: AssetEntity): Promise<void>;
 
   update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity>;
@@ -112,13 +114,13 @@ export class AssetRepository implements IAssetRepository {
       .getMany();
   }
 
-  async getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto> {
+  async getAssetCountByUserId(ownerId: string): Promise<AssetCountByUserIdResponseDto> {
     // Get asset count by AssetType
     const items = await this.assetRepository
       .createQueryBuilder('asset')
       .select(`COUNT(asset.id)`, 'count')
       .addSelect(`asset.type`, 'type')
-      .where('"userId" = :userId', { userId: userId })
+      .where('"ownerId" = :ownerId', { ownerId: ownerId })
       .andWhere('asset.isVisible = true')
       .groupBy('asset.type')
       .getRawMany();
@@ -149,7 +151,7 @@ export class AssetRepository implements IAssetRepository {
     // Get asset entity from a list of time buckets
     return await this.assetRepository
       .createQueryBuilder('asset')
-      .where('asset.userId = :userId', { userId: userId })
+      .where('asset.ownerId = :userId', { userId: userId })
       .andWhere(`date_trunc('month', "createdAt") IN (:...buckets)`, {
         buckets: [...getAssetByTimeBucketDto.timeBucket],
       })
@@ -167,7 +169,7 @@ export class AssetRepository implements IAssetRepository {
         .createQueryBuilder('asset')
         .select(`COUNT(asset.id)::int`, 'count')
         .addSelect(`date_trunc('month', "createdAt")`, 'timeBucket')
-        .where('"userId" = :userId', { userId: userId })
+        .where('"ownerId" = :userId', { userId: userId })
         .andWhere('asset.resizePath is not NULL')
         .andWhere('asset.isVisible = true')
         .groupBy(`date_trunc('month', "createdAt")`)
@@ -178,7 +180,7 @@ export class AssetRepository implements IAssetRepository {
         .createQueryBuilder('asset')
         .select(`COUNT(asset.id)::int`, 'count')
         .addSelect(`date_trunc('day', "createdAt")`, 'timeBucket')
-        .where('"userId" = :userId', { userId: userId })
+        .where('"ownerId" = :userId', { userId: userId })
         .andWhere('asset.resizePath is not NULL')
         .andWhere('asset.isVisible = true')
         .groupBy(`date_trunc('day', "createdAt")`)
@@ -192,7 +194,7 @@ export class AssetRepository implements IAssetRepository {
   async getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]> {
     return await this.assetRepository
       .createQueryBuilder('asset')
-      .where('asset.userId = :userId', { userId: userId })
+      .where('asset.ownerId = :userId', { userId: userId })
       .andWhere('asset.isVisible = true')
       .leftJoin('asset.exifInfo', 'ei')
       .leftJoin('asset.smartInfo', 'si')
@@ -216,7 +218,7 @@ export class AssetRepository implements IAssetRepository {
         SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId"
         FROM assets a
         LEFT JOIN smart_info si ON a.id = si."assetId"
-        WHERE a."userId" = $1
+        WHERE a."ownerId" = $1
         AND a."isVisible" = true
         AND si.objects IS NOT NULL
       `,
@@ -230,7 +232,7 @@ export class AssetRepository implements IAssetRepository {
         SELECT DISTINCT ON (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId"
         FROM assets a
         LEFT JOIN exif e ON a.id = e."assetId"
-        WHERE a."userId" = $1
+        WHERE a."ownerId" = $1
         AND a."isVisible" = true
         AND e.city IS NOT NULL
         AND a.type = 'IMAGE';
@@ -255,12 +257,12 @@ export class AssetRepository implements IAssetRepository {
 
   /**
    * Get all assets belong to the user on the database
-   * @param userId
+   * @param ownerId
    */
-  async getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]> {
+  async getAllByUserId(ownerId: string, dto: AssetSearchDto): Promise<AssetEntity[]> {
     return this.assetRepository.find({
       where: {
-        userId,
+        ownerId,
         resizePath: Not(IsNull()),
         isVisible: true,
         isFavorite: dto.isFavorite,
@@ -271,7 +273,7 @@ export class AssetRepository implements IAssetRepository {
       },
       skip: dto.skip || 0,
       order: {
-        createdAt: 'DESC',
+        fileCreatedAt: 'DESC',
       },
     });
   }
@@ -280,7 +282,9 @@ export class AssetRepository implements IAssetRepository {
     return this.assetRepository.findOne({ where: { id } });
   }
 
-  async create(asset: Omit<AssetEntity, 'id'>): Promise<AssetEntity> {
+  async create(
+    asset: Omit<AssetEntity, 'id' | 'createdAt' | 'updatedAt' | 'ownerId' | 'livePhotoVideoId'>,
+  ): Promise<AssetEntity> {
     return this.assetRepository.save(asset);
   }
 
@@ -304,16 +308,16 @@ export class AssetRepository implements IAssetRepository {
 
   /**
    * Get assets by device's Id on the database
-   * @param userId
+   * @param ownerId
    * @param deviceId
    *
    * @returns Promise<string[]> - Array of assetIds belong to the device
    */
-  async getAllByDeviceId(userId: string, deviceId: string): Promise<string[]> {
+  async getAllByDeviceId(ownerId: string, deviceId: string): Promise<string[]> {
     const rows = await this.assetRepository.find({
       where: {
-        userId: userId,
-        deviceId: deviceId,
+        ownerId,
+        deviceId,
         isVisible: true,
       },
       select: ['deviceAssetId'],
@@ -326,14 +330,14 @@ export class AssetRepository implements IAssetRepository {
 
   /**
    * Get asset by checksum on the database
-   * @param userId
+   * @param ownerId
    * @param checksum
    *
    */
-  getAssetByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity> {
+  getAssetByChecksum(ownerId: string, checksum: Buffer): Promise<AssetEntity> {
     return this.assetRepository.findOneOrFail({
       where: {
-        userId,
+        ownerId,
         checksum,
       },
       relations: ['exifInfo'],
@@ -341,7 +345,7 @@ export class AssetRepository implements IAssetRepository {
   }
 
   async getExistingAssets(
-    userId: string,
+    ownerId: string,
     checkDuplicateAssetDto: CheckExistingAssetsDto,
   ): Promise<CheckExistingAssetsResponseDto> {
     const existingAssets = await this.assetRepository.find({
@@ -349,17 +353,17 @@ export class AssetRepository implements IAssetRepository {
       where: {
         deviceAssetId: In(checkDuplicateAssetDto.deviceAssetIds),
         deviceId: checkDuplicateAssetDto.deviceId,
-        userId,
+        ownerId,
       },
     });
     return new CheckExistingAssetsResponseDto(existingAssets.map((a) => a.deviceAssetId));
   }
 
-  async countByIdAndUser(assetId: string, userId: string): Promise<number> {
+  async countByIdAndUser(assetId: string, ownerId: string): Promise<number> {
     return await this.assetRepository.count({
       where: {
         id: assetId,
-        userId,
+        ownerId,
       },
     });
   }

+ 6 - 8
server/apps/immich/src/api-v1/asset/asset.core.ts

@@ -1,6 +1,5 @@
-import { timeUtils } from '@app/common';
 import { AuthUserDto, IJobRepository, JobName } from '@app/domain';
-import { AssetEntity } from '@app/infra/db/entities';
+import { AssetEntity, UserEntity } from '@app/infra/db/entities';
 import { StorageService } from '@app/storage';
 import { IAssetRepository } from './asset-repository';
 import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
@@ -19,24 +18,23 @@ export class AssetCore {
     livePhotoAssetId?: string,
   ): Promise<AssetEntity> {
     let asset = await this.repository.create({
-      userId: authUser.id,
+      owner: { id: authUser.id } as UserEntity,
 
       mimeType: file.mimeType,
       checksum: file.checksum || null,
       originalPath: file.originalPath,
 
-      createdAt: timeUtils.checkValidTimestamp(dto.createdAt) ? dto.createdAt : new Date().toISOString(),
-      modifiedAt: timeUtils.checkValidTimestamp(dto.modifiedAt) ? dto.modifiedAt : new Date().toISOString(),
-      updatedAt: new Date().toISOString(),
-
       deviceAssetId: dto.deviceAssetId,
       deviceId: dto.deviceId,
 
+      fileCreatedAt: dto.fileCreatedAt,
+      fileModifiedAt: dto.fileModifiedAt,
+
       type: dto.assetType,
       isFavorite: dto.isFavorite,
       duration: dto.duration || null,
       isVisible: dto.isVisible ?? true,
-      livePhotoVideoId: livePhotoAssetId || null,
+      livePhotoVideo: livePhotoAssetId != null ? ({ id: livePhotoAssetId } as AssetEntity) : null,
       resizePath: null,
       webpPath: null,
       encodedVideoPath: null,

+ 12 - 10
server/apps/immich/src/api-v1/asset/asset.service.spec.ts

@@ -27,8 +27,8 @@ const _getCreateAssetDto = (): CreateAssetDto => {
   createAssetDto.deviceAssetId = 'deviceAssetId';
   createAssetDto.deviceId = 'deviceId';
   createAssetDto.assetType = AssetType.OTHER;
-  createAssetDto.createdAt = '2022-06-19T23:41:36.910Z';
-  createAssetDto.modifiedAt = '2022-06-19T23:41:36.910Z';
+  createAssetDto.fileCreatedAt = '2022-06-19T23:41:36.910Z';
+  createAssetDto.fileModifiedAt = '2022-06-19T23:41:36.910Z';
   createAssetDto.isFavorite = false;
   createAssetDto.duration = '0:00:00.000000';
 
@@ -39,14 +39,15 @@ const _getAsset_1 = () => {
   const asset_1 = new AssetEntity();
 
   asset_1.id = 'id_1';
-  asset_1.userId = 'user_id_1';
+  asset_1.ownerId = 'user_id_1';
   asset_1.deviceAssetId = 'device_asset_id_1';
   asset_1.deviceId = 'device_id_1';
   asset_1.type = AssetType.VIDEO;
   asset_1.originalPath = 'fake_path/asset_1.jpeg';
   asset_1.resizePath = '';
-  asset_1.createdAt = '2022-06-19T23:41:36.910Z';
-  asset_1.modifiedAt = '2022-06-19T23:41:36.910Z';
+  asset_1.fileModifiedAt = '2022-06-19T23:41:36.910Z';
+  asset_1.fileCreatedAt = '2022-06-19T23:41:36.910Z';
+  asset_1.updatedAt = '2022-06-19T23:41:36.910Z';
   asset_1.isFavorite = false;
   asset_1.mimeType = 'image/jpeg';
   asset_1.webpPath = '';
@@ -59,14 +60,15 @@ const _getAsset_2 = () => {
   const asset_2 = new AssetEntity();
 
   asset_2.id = 'id_2';
-  asset_2.userId = 'user_id_1';
+  asset_2.ownerId = 'user_id_1';
   asset_2.deviceAssetId = 'device_asset_id_2';
   asset_2.deviceId = 'device_id_1';
   asset_2.type = AssetType.VIDEO;
   asset_2.originalPath = 'fake_path/asset_2.jpeg';
   asset_2.resizePath = '';
-  asset_2.createdAt = '2022-06-19T23:41:36.910Z';
-  asset_2.modifiedAt = '2022-06-19T23:41:36.910Z';
+  asset_2.fileModifiedAt = '2022-06-19T23:41:36.910Z';
+  asset_2.fileCreatedAt = '2022-06-19T23:41:36.910Z';
+  asset_2.updatedAt = '2022-06-19T23:41:36.910Z';
   asset_2.isFavorite = false;
   asset_2.mimeType = 'image/jpeg';
   asset_2.webpPath = '';
@@ -292,7 +294,7 @@ describe('AssetService', () => {
       const asset = {
         id: 'live-photo-asset',
         originalPath: file.originalPath,
-        userId: authStub.user1.id,
+        ownerId: authStub.user1.id,
         type: AssetType.IMAGE,
         isVisible: true,
       } as AssetEntity;
@@ -307,7 +309,7 @@ describe('AssetService', () => {
       const livePhotoAsset = {
         id: 'live-photo-motion',
         originalPath: livePhotoFile.originalPath,
-        userId: authStub.user1.id,
+        ownerId: authStub.user1.id,
         type: AssetType.VIDEO,
         isVisible: false,
       } as AssetEntity;

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

@@ -518,7 +518,7 @@ export class AssetService {
       where: {
         deviceAssetId: checkDuplicateAssetDto.deviceAssetId,
         deviceId: checkDuplicateAssetDto.deviceId,
-        userId: authUser.id,
+        ownerId: authUser.id,
       },
     });
 

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

@@ -16,10 +16,10 @@ export class CreateAssetDto {
   assetType!: AssetType;
 
   @IsNotEmpty()
-  createdAt!: string;
+  fileCreatedAt!: string;
 
   @IsNotEmpty()
-  modifiedAt!: string;
+  fileModifiedAt!: string;
 
   @IsNotEmpty()
   isFavorite!: boolean;

+ 11 - 14
server/apps/microservices/src/processors/metadata-extraction.processor.ts

@@ -159,8 +159,8 @@ export class MetadataExtractionProcessor {
         return exifDate.toDate();
       };
 
-      const createdAt = exifToDate(exifData?.DateTimeOriginal ?? exifData?.CreateDate ?? asset.createdAt);
-      const modifyDate = exifToDate(exifData?.ModifyDate ?? asset.modifiedAt);
+      const fileCreatedAt = exifToDate(exifData?.DateTimeOriginal ?? exifData?.CreateDate ?? asset.fileCreatedAt);
+      const fileModifiedAt = exifToDate(exifData?.ModifyDate ?? asset.fileModifiedAt);
       const fileStats = fs.statSync(asset.originalPath);
       const fileSizeInBytes = fileStats.size;
 
@@ -174,8 +174,8 @@ export class MetadataExtractionProcessor {
       newExif.exifImageWidth = exifData?.ExifImageWidth || exifData?.ImageWidth || null;
       newExif.exposureTime = exifData?.ExposureTime || null;
       newExif.orientation = exifData?.Orientation?.toString() || null;
-      newExif.dateTimeOriginal = createdAt;
-      newExif.modifyDate = modifyDate;
+      newExif.dateTimeOriginal = fileCreatedAt;
+      newExif.modifyDate = fileModifiedAt;
       newExif.lensModel = exifData?.LensModel || null;
       newExif.fNumber = exifData?.FNumber || null;
       newExif.focalLength = exifData?.FocalLength ? parseFloat(exifData.FocalLength) : null;
@@ -186,7 +186,7 @@ export class MetadataExtractionProcessor {
 
       await this.assetRepository.save({
         id: asset.id,
-        createdAt: createdAt?.toISOString(),
+        fileCreatedAt: fileCreatedAt?.toISOString(),
       });
 
       if (newExif.livePhotoCID && !asset.livePhotoVideoId) {
@@ -273,7 +273,7 @@ export class MetadataExtractionProcessor {
         }),
       );
       let durationString = asset.duration;
-      let createdAt = asset.createdAt;
+      let fileCreatedAt = asset.fileCreatedAt;
 
       if (data.format.duration) {
         durationString = this.extractDuration(data.format.duration);
@@ -282,14 +282,10 @@ export class MetadataExtractionProcessor {
       const videoTags = data.format.tags;
       if (videoTags) {
         if (videoTags['com.apple.quicktime.creationdate']) {
-          createdAt = String(videoTags['com.apple.quicktime.creationdate']);
+          fileCreatedAt = String(videoTags['com.apple.quicktime.creationdate']);
         } else if (videoTags['creation_time']) {
-          createdAt = String(videoTags['creation_time']);
-        } else {
-          createdAt = asset.createdAt;
+          fileCreatedAt = String(videoTags['creation_time']);
         }
-      } else {
-        createdAt = asset.createdAt;
       }
 
       const exifData = await exiftool.read<ImmichTags>(asset.originalPath).catch((e) => {
@@ -302,7 +298,7 @@ export class MetadataExtractionProcessor {
       newExif.description = '';
       newExif.imageName = path.parse(fileName).name || null;
       newExif.fileSizeInByte = data.format.size || null;
-      newExif.dateTimeOriginal = createdAt ? new Date(createdAt) : null;
+      newExif.dateTimeOriginal = fileCreatedAt ? new Date(fileCreatedAt) : null;
       newExif.modifyDate = null;
       newExif.latitude = null;
       newExif.longitude = null;
@@ -382,8 +378,9 @@ export class MetadataExtractionProcessor {
       }
 
       await this.exifRepository.upsert(newExif, { conflictPaths: ['assetId'] });
-      await this.assetRepository.update({ id: asset.id }, { duration: durationString, createdAt: createdAt });
+      await this.assetRepository.update({ id: asset.id }, { duration: durationString, fileCreatedAt });
     } catch (err) {
+      ``;
       // do nothing
       console.log('Error in video metadata extraction', err);
     }

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

@@ -40,7 +40,7 @@ export class ThumbnailGeneratorProcessor {
     const { asset } = job.data;
     const sanitizedDeviceId = sanitize(String(asset.deviceId));
 
-    const resizePath = join(basePath, asset.userId, 'thumb', sanitizedDeviceId);
+    const resizePath = join(basePath, asset.ownerId, 'thumb', sanitizedDeviceId);
 
     if (!existsSync(resizePath)) {
       mkdirSync(resizePath, { recursive: true });
@@ -75,7 +75,7 @@ export class ThumbnailGeneratorProcessor {
       await this.machineLearningQueue.add(JobName.IMAGE_TAGGING, { asset });
       await this.machineLearningQueue.add(JobName.OBJECT_DETECTION, { asset });
 
-      this.wsCommunicationGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
+      this.wsCommunicationGateway.server.to(asset.ownerId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
     }
 
     if (asset.type == AssetType.VIDEO) {
@@ -106,7 +106,7 @@ export class ThumbnailGeneratorProcessor {
       await this.machineLearningQueue.add(JobName.IMAGE_TAGGING, { asset });
       await this.machineLearningQueue.add(JobName.OBJECT_DETECTION, { asset });
 
-      this.wsCommunicationGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
+      this.wsCommunicationGateway.server.to(asset.ownerId).emit('on_upload_success', JSON.stringify(mapAsset(asset)));
     }
   }
 

+ 1 - 1
server/apps/microservices/src/processors/user-deletion.processor.ts

@@ -61,7 +61,7 @@ export class UserDeletionProcessor {
       await this.albumRepository.remove(albums);
 
       await this.apiKeyRepository.delete({ userId: user.id });
-      await this.assetRepository.delete({ userId: user.id });
+      await this.assetRepository.delete({ ownerId: user.id });
       await this.userRepository.remove(user);
     } catch (error: any) {
       this.logger.error(`Failed to remove user`);

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

@@ -22,7 +22,7 @@ export class VideoTranscodeProcessor {
   async videoConversion(job: Job<IVideoConversionProcessor>) {
     const { asset } = job.data;
     const basePath = APP_UPLOAD_LOCATION;
-    const encodedVideoPath = `${basePath}/${asset.userId}/encoded-video`;
+    const encodedVideoPath = `${basePath}/${asset.ownerId}/encoded-video`;
 
     if (!existsSync(encodedVideoPath)) {
       mkdirSync(encodedVideoPath, { recursive: true });

+ 8 - 8
server/immich-openapi-specs.json

@@ -3324,10 +3324,10 @@
             "type": "string",
             "nullable": true
           },
-          "createdAt": {
+          "fileCreatedAt": {
             "type": "string"
           },
-          "modifiedAt": {
+          "fileModifiedAt": {
             "type": "string"
           },
           "updatedAt": {
@@ -3376,8 +3376,8 @@
           "deviceId",
           "originalPath",
           "resizePath",
-          "createdAt",
-          "modifiedAt",
+          "fileCreatedAt",
+          "fileModifiedAt",
           "updatedAt",
           "isFavorite",
           "mimeType",
@@ -3817,10 +3817,10 @@
           "deviceId": {
             "type": "string"
           },
-          "createdAt": {
+          "fileCreatedAt": {
             "type": "string"
           },
-          "modifiedAt": {
+          "fileModifiedAt": {
             "type": "string"
           },
           "isFavorite": {
@@ -3841,8 +3841,8 @@
           "assetData",
           "deviceAssetId",
           "deviceId",
-          "createdAt",
-          "modifiedAt",
+          "fileCreatedAt",
+          "fileModifiedAt",
           "isFavorite",
           "fileExtension"
         ]

+ 8 - 8
server/libs/domain/src/asset/response-dto/asset-response.dto.ts

@@ -14,8 +14,8 @@ export class AssetResponseDto {
   type!: AssetType;
   originalPath!: string;
   resizePath!: string | null;
-  createdAt!: string;
-  modifiedAt!: string;
+  fileCreatedAt!: string;
+  fileModifiedAt!: string;
   updatedAt!: string;
   isFavorite!: boolean;
   mimeType!: string | null;
@@ -32,13 +32,13 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto {
   return {
     id: entity.id,
     deviceAssetId: entity.deviceAssetId,
-    ownerId: entity.userId,
+    ownerId: entity.ownerId,
     deviceId: entity.deviceId,
     type: entity.type,
     originalPath: entity.originalPath,
     resizePath: entity.resizePath,
-    createdAt: entity.createdAt,
-    modifiedAt: entity.modifiedAt,
+    fileCreatedAt: entity.fileCreatedAt,
+    fileModifiedAt: entity.fileModifiedAt,
     updatedAt: entity.updatedAt,
     isFavorite: entity.isFavorite,
     mimeType: entity.mimeType,
@@ -56,13 +56,13 @@ export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto {
   return {
     id: entity.id,
     deviceAssetId: entity.deviceAssetId,
-    ownerId: entity.userId,
+    ownerId: entity.ownerId,
     deviceId: entity.deviceId,
     type: entity.type,
     originalPath: entity.originalPath,
     resizePath: entity.resizePath,
-    createdAt: entity.createdAt,
-    modifiedAt: entity.modifiedAt,
+    fileCreatedAt: entity.fileCreatedAt,
+    fileModifiedAt: entity.fileModifiedAt,
     updatedAt: entity.updatedAt,
     isFavorite: entity.isFavorite,
     mimeType: entity.mimeType,

+ 13 - 7
server/libs/domain/test/fixtures.ts

@@ -95,20 +95,23 @@ export const assetEntityStub = {
   image: Object.freeze<AssetEntity>({
     id: 'asset-id',
     deviceAssetId: 'device-asset-id',
-    modifiedAt: today.toISOString(),
-    createdAt: today.toISOString(),
-    userId: 'user-id',
+    fileModifiedAt: today.toISOString(),
+    fileCreatedAt: today.toISOString(),
+    owner: userEntityStub.user1,
+    ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/original/path',
     resizePath: null,
     type: AssetType.IMAGE,
     webpPath: null,
     encodedVideoPath: null,
+    createdAt: today.toISOString(),
     updatedAt: today.toISOString(),
     mimeType: null,
     isFavorite: true,
     duration: null,
     isVisible: true,
+    livePhotoVideo: null,
     livePhotoVideoId: null,
     tags: [],
     sharedLinks: [],
@@ -146,8 +149,8 @@ const assetResponse: AssetResponseDto = {
   type: AssetType.VIDEO,
   originalPath: 'fake_path/jpeg',
   resizePath: '',
-  createdAt: today.toISOString(),
-  modifiedAt: today.toISOString(),
+  fileModifiedAt: today.toISOString(),
+  fileCreatedAt: today.toISOString(),
   updatedAt: today.toISOString(),
   isFavorite: false,
   mimeType: 'image/jpeg',
@@ -374,14 +377,16 @@ export const sharedLinkStub = {
       assets: [
         {
           id: 'id_1',
-          userId: 'user_id_1',
+          owner: userEntityStub.user1,
+          ownerId: 'user_id_1',
           deviceAssetId: 'device_asset_id_1',
           deviceId: 'device_id_1',
           type: AssetType.VIDEO,
           originalPath: 'fake_path/jpeg',
           resizePath: '',
+          fileModifiedAt: today.toISOString(),
+          fileCreatedAt: today.toISOString(),
           createdAt: today.toISOString(),
-          modifiedAt: today.toISOString(),
           updatedAt: today.toISOString(),
           isFavorite: false,
           mimeType: 'image/jpeg',
@@ -396,6 +401,7 @@ export const sharedLinkStub = {
           encodedVideoPath: '',
           duration: null,
           isVisible: true,
+          livePhotoVideo: null,
           livePhotoVideoId: null,
           exifInfo: {
             livePhotoCID: null,

+ 23 - 10
server/libs/infra/src/db/entities/asset.entity.ts

@@ -1,9 +1,12 @@
 import {
   Column,
+  CreateDateColumn,
   Entity,
   Index,
+  JoinColumn,
   JoinTable,
   ManyToMany,
+  ManyToOne,
   OneToOne,
   PrimaryGeneratedColumn,
   Unique,
@@ -13,9 +16,10 @@ import { ExifEntity } from './exif.entity';
 import { SharedLinkEntity } from './shared-link.entity';
 import { SmartInfoEntity } from './smart-info.entity';
 import { TagEntity } from './tag.entity';
+import { UserEntity } from './user.entity';
 
 @Entity('assets')
-@Unique('UQ_userid_checksum', ['userId', 'checksum'])
+@Unique('UQ_userid_checksum', ['owner', 'checksum'])
 export class AssetEntity {
   @PrimaryGeneratedColumn('uuid')
   id!: string;
@@ -23,8 +27,11 @@ export class AssetEntity {
   @Column()
   deviceAssetId!: string;
 
+  @ManyToOne(() => UserEntity, { eager: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
+  owner!: UserEntity;
+
   @Column()
-  userId!: string;
+  ownerId!: string;
 
   @Column()
   deviceId!: string;
@@ -44,15 +51,18 @@ export class AssetEntity {
   @Column({ type: 'varchar', nullable: true, default: '' })
   encodedVideoPath!: string | null;
 
-  @Column({ type: 'timestamptz' })
+  @CreateDateColumn({ type: 'timestamptz' })
   createdAt!: string;
 
-  @Column({ type: 'timestamptz' })
-  modifiedAt!: string;
-
   @UpdateDateColumn({ type: 'timestamptz' })
   updatedAt!: string;
 
+  @Column({ type: 'timestamptz' })
+  fileCreatedAt!: string;
+
+  @Column({ type: 'timestamptz' })
+  fileModifiedAt!: string;
+
   @Column({ type: 'boolean', default: false })
   isFavorite!: boolean;
 
@@ -69,7 +79,11 @@ export class AssetEntity {
   @Column({ type: 'boolean', default: true })
   isVisible!: boolean;
 
-  @Column({ type: 'uuid', nullable: true })
+  @OneToOne(() => AssetEntity, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' })
+  @JoinColumn()
+  livePhotoVideo!: AssetEntity | null;
+
+  @Column({ nullable: true })
   livePhotoVideoId!: string | null;
 
   @OneToOne(() => ExifEntity, (exifEntity) => exifEntity.asset)
@@ -78,12 +92,11 @@ export class AssetEntity {
   @OneToOne(() => SmartInfoEntity, (smartInfoEntity) => smartInfoEntity.asset)
   smartInfo?: SmartInfoEntity;
 
-  // https://github.com/typeorm/typeorm/blob/master/docs/many-to-many-relations.md
-  @ManyToMany(() => TagEntity, (tag) => tag.assets, { cascade: true })
+  @ManyToMany(() => TagEntity, (tag) => tag.assets, { cascade: true, eager: true })
   @JoinTable({ name: 'tag_asset' })
   tags!: TagEntity[];
 
-  @ManyToMany(() => SharedLinkEntity, (link) => link.assets, { cascade: true })
+  @ManyToMany(() => SharedLinkEntity, (link) => link.assets, { cascade: true, eager: true })
   @JoinTable({ name: 'shared_link__asset' })
   sharedLinks!: SharedLinkEntity[];
 }

+ 30 - 0
server/libs/infra/src/db/migrations/1676680127415-FixAssetRelations.ts

@@ -0,0 +1,30 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class FixAssetRelations1676680127415 implements MigrationInterface {
+    name = 'FixAssetRelations1676680127415'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "modifiedAt" TO "fileModifiedAt"`);
+        await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "createdAt" TO "fileCreatedAt"`);
+
+        await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "userId" TO "ownerId"`);
+        await queryRunner.query(`ALTER TABLE assets ALTER COLUMN "ownerId" TYPE uuid USING "ownerId"::uuid;`);
+
+        await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "UQ_16294b83fa8c0149719a1f631ef" UNIQUE ("livePhotoVideoId")`);
+        await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "FK_2c5ac0d6fb58b238fd2068de67d" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
+        await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "FK_16294b83fa8c0149719a1f631ef" FOREIGN KEY ("livePhotoVideoId") REFERENCES "assets"("id") ON DELETE SET NULL ON UPDATE CASCADE`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_16294b83fa8c0149719a1f631ef"`);
+        await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_2c5ac0d6fb58b238fd2068de67d"`);
+        await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "UQ_16294b83fa8c0149719a1f631ef"`);
+
+        await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "fileCreatedAt" TO "createdAt"`);
+        await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "fileModifiedAt" TO "modifiedAt"`);
+
+        await queryRunner.query(`ALTER TABLE "assets" RENAME COLUMN "ownerId" TO "userId"`);
+        await queryRunner.query(`ALTER TABLE assets ALTER COLUMN "userId" TYPE varchar`);
+    }
+
+}

+ 14 - 0
server/libs/infra/src/db/migrations/1676721296440-AssetCreatedAtField.ts

@@ -0,0 +1,14 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class AssetCreatedAtField1676721296440 implements MigrationInterface {
+    name = 'AssetCreatedAtField1676721296440'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "assets" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "createdAt"`);
+    }
+
+}

+ 2 - 2
server/libs/infra/src/db/repository/shared-link.repository.ts

@@ -31,11 +31,11 @@ export class SharedLinkRepository implements ISharedLinkRepository {
       order: {
         createdAt: 'DESC',
         assets: {
-          createdAt: 'ASC',
+          fileCreatedAt: 'ASC',
         },
         album: {
           assets: {
-            createdAt: 'ASC',
+            fileCreatedAt: 'ASC',
           },
         },
       },

+ 3 - 3
server/libs/storage/src/storage.service.ts

@@ -50,7 +50,7 @@ export class StorageService {
       const source = asset.originalPath;
       const ext = path.extname(source).split('.').pop() as string;
       const sanitized = sanitize(path.basename(filename, `.${ext}`));
-      const rootPath = path.join(APP_UPLOAD_LOCATION, asset.userId);
+      const rootPath = path.join(APP_UPLOAD_LOCATION, asset.ownerId);
       const storagePath = this.render(this.storageTemplate, asset, sanitized, ext);
       const fullPath = path.normalize(path.join(rootPath, storagePath));
       let destination = `${fullPath}.${ext}`;
@@ -132,7 +132,7 @@ export class StorageService {
       this.render(
         template,
         {
-          createdAt: new Date().toISOString(),
+          fileCreatedAt: new Date().toISOString(),
           originalPath: '/upload/test/IMG_123.jpg',
           type: AssetType.IMAGE,
         } as AssetEntity,
@@ -161,7 +161,7 @@ export class StorageService {
     const fileType = asset.type == AssetType.IMAGE ? 'IMG' : 'VID';
     const fileTypeFull = asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO';
 
-    const dt = luxon.DateTime.fromISO(new Date(asset.createdAt).toISOString());
+    const dt = luxon.DateTime.fromISO(new Date(asset.fileCreatedAt).toISOString());
 
     const dateTokens = [
       ...supportedYearTokens,

+ 1 - 1
server/package-lock.json

@@ -6,7 +6,7 @@
   "packages": {
     "": {
       "name": "immich",
-      "version": "1.46.1",
+      "version": "1.47.3",
       "license": "UNLICENSED",
       "dependencies": {
         "@nestjs/bull": "^0.6.2",

+ 52 - 52
web/src/api/open-api/api.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.47.2
+ * The version of the OpenAPI document: 1.47.3
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -61,10 +61,10 @@ export interface APIKeyCreateResponseDto {
 export interface APIKeyResponseDto {
     /**
      * 
-     * @type {number}
+     * @type {string}
      * @memberof APIKeyResponseDto
      */
-    'id': number;
+    'id': string;
     /**
      * 
      * @type {string}
@@ -467,13 +467,13 @@ export interface AssetResponseDto {
      * @type {string}
      * @memberof AssetResponseDto
      */
-    'createdAt': string;
+    'fileCreatedAt': string;
     /**
      * 
      * @type {string}
      * @memberof AssetResponseDto
      */
-    'modifiedAt': string;
+    'fileModifiedAt': string;
     /**
      * 
      * @type {string}
@@ -2356,11 +2356,11 @@ export const APIKeyApiAxiosParamCreator = function (configuration?: Configuratio
         },
         /**
          * 
-         * @param {number} id 
+         * @param {string} id 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        deleteKey: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        deleteKey: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             // verify required parameter 'id' is not null or undefined
             assertParamExists('deleteKey', 'id', id)
             const localVarPath = `/api-key/{id}`
@@ -2389,11 +2389,11 @@ export const APIKeyApiAxiosParamCreator = function (configuration?: Configuratio
         },
         /**
          * 
-         * @param {number} id 
+         * @param {string} id 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getKey: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        getKey: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             // verify required parameter 'id' is not null or undefined
             assertParamExists('getKey', 'id', id)
             const localVarPath = `/api-key/{id}`
@@ -2451,12 +2451,12 @@ export const APIKeyApiAxiosParamCreator = function (configuration?: Configuratio
         },
         /**
          * 
-         * @param {number} id 
+         * @param {string} id 
          * @param {APIKeyUpdateDto} aPIKeyUpdateDto 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        updateKey: async (id: number, aPIKeyUpdateDto: APIKeyUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        updateKey: async (id: string, aPIKeyUpdateDto: APIKeyUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             // verify required parameter 'id' is not null or undefined
             assertParamExists('updateKey', 'id', id)
             // verify required parameter 'aPIKeyUpdateDto' is not null or undefined
@@ -2510,21 +2510,21 @@ export const APIKeyApiFp = function(configuration?: Configuration) {
         },
         /**
          * 
-         * @param {number} id 
+         * @param {string} id 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async deleteKey(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
+        async deleteKey(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
             const localVarAxiosArgs = await localVarAxiosParamCreator.deleteKey(id, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
         /**
          * 
-         * @param {number} id 
+         * @param {string} id 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async getKey(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<APIKeyResponseDto>> {
+        async getKey(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<APIKeyResponseDto>> {
             const localVarAxiosArgs = await localVarAxiosParamCreator.getKey(id, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
@@ -2539,12 +2539,12 @@ export const APIKeyApiFp = function(configuration?: Configuration) {
         },
         /**
          * 
-         * @param {number} id 
+         * @param {string} id 
          * @param {APIKeyUpdateDto} aPIKeyUpdateDto 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async updateKey(id: number, aPIKeyUpdateDto: APIKeyUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<APIKeyResponseDto>> {
+        async updateKey(id: string, aPIKeyUpdateDto: APIKeyUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<APIKeyResponseDto>> {
             const localVarAxiosArgs = await localVarAxiosParamCreator.updateKey(id, aPIKeyUpdateDto, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
@@ -2569,20 +2569,20 @@ export const APIKeyApiFactory = function (configuration?: Configuration, basePat
         },
         /**
          * 
-         * @param {number} id 
+         * @param {string} id 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        deleteKey(id: number, options?: any): AxiosPromise<void> {
+        deleteKey(id: string, options?: any): AxiosPromise<void> {
             return localVarFp.deleteKey(id, options).then((request) => request(axios, basePath));
         },
         /**
          * 
-         * @param {number} id 
+         * @param {string} id 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getKey(id: number, options?: any): AxiosPromise<APIKeyResponseDto> {
+        getKey(id: string, options?: any): AxiosPromise<APIKeyResponseDto> {
             return localVarFp.getKey(id, options).then((request) => request(axios, basePath));
         },
         /**
@@ -2595,12 +2595,12 @@ export const APIKeyApiFactory = function (configuration?: Configuration, basePat
         },
         /**
          * 
-         * @param {number} id 
+         * @param {string} id 
          * @param {APIKeyUpdateDto} aPIKeyUpdateDto 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        updateKey(id: number, aPIKeyUpdateDto: APIKeyUpdateDto, options?: any): AxiosPromise<APIKeyResponseDto> {
+        updateKey(id: string, aPIKeyUpdateDto: APIKeyUpdateDto, options?: any): AxiosPromise<APIKeyResponseDto> {
             return localVarFp.updateKey(id, aPIKeyUpdateDto, options).then((request) => request(axios, basePath));
         },
     };
@@ -2626,23 +2626,23 @@ export class APIKeyApi extends BaseAPI {
 
     /**
      * 
-     * @param {number} id 
+     * @param {string} id 
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @memberof APIKeyApi
      */
-    public deleteKey(id: number, options?: AxiosRequestConfig) {
+    public deleteKey(id: string, options?: AxiosRequestConfig) {
         return APIKeyApiFp(this.configuration).deleteKey(id, options).then((request) => request(this.axios, this.basePath));
     }
 
     /**
      * 
-     * @param {number} id 
+     * @param {string} id 
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @memberof APIKeyApi
      */
-    public getKey(id: number, options?: AxiosRequestConfig) {
+    public getKey(id: string, options?: AxiosRequestConfig) {
         return APIKeyApiFp(this.configuration).getKey(id, options).then((request) => request(this.axios, this.basePath));
     }
 
@@ -2658,13 +2658,13 @@ export class APIKeyApi extends BaseAPI {
 
     /**
      * 
-     * @param {number} id 
+     * @param {string} id 
      * @param {APIKeyUpdateDto} aPIKeyUpdateDto 
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @memberof APIKeyApi
      */
-    public updateKey(id: number, aPIKeyUpdateDto: APIKeyUpdateDto, options?: AxiosRequestConfig) {
+    public updateKey(id: string, aPIKeyUpdateDto: APIKeyUpdateDto, options?: AxiosRequestConfig) {
         return APIKeyApiFp(this.configuration).updateKey(id, aPIKeyUpdateDto, options).then((request) => request(this.axios, this.basePath));
     }
 }
@@ -4432,8 +4432,8 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
          * @param {any} assetData 
          * @param {string} deviceAssetId 
          * @param {string} deviceId 
-         * @param {string} createdAt 
-         * @param {string} modifiedAt 
+         * @param {string} fileCreatedAt 
+         * @param {string} fileModifiedAt 
          * @param {boolean} isFavorite 
          * @param {string} fileExtension 
          * @param {any} [livePhotoData] 
@@ -4442,7 +4442,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        uploadFile: async (assetType: AssetTypeEnum, assetData: any, deviceAssetId: string, deviceId: string, createdAt: string, modifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        uploadFile: async (assetType: AssetTypeEnum, assetData: any, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             // verify required parameter 'assetType' is not null or undefined
             assertParamExists('uploadFile', 'assetType', assetType)
             // verify required parameter 'assetData' is not null or undefined
@@ -4451,10 +4451,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
             assertParamExists('uploadFile', 'deviceAssetId', deviceAssetId)
             // verify required parameter 'deviceId' is not null or undefined
             assertParamExists('uploadFile', 'deviceId', deviceId)
-            // verify required parameter 'createdAt' is not null or undefined
-            assertParamExists('uploadFile', 'createdAt', createdAt)
-            // verify required parameter 'modifiedAt' is not null or undefined
-            assertParamExists('uploadFile', 'modifiedAt', modifiedAt)
+            // verify required parameter 'fileCreatedAt' is not null or undefined
+            assertParamExists('uploadFile', 'fileCreatedAt', fileCreatedAt)
+            // verify required parameter 'fileModifiedAt' is not null or undefined
+            assertParamExists('uploadFile', 'fileModifiedAt', fileModifiedAt)
             // verify required parameter 'isFavorite' is not null or undefined
             assertParamExists('uploadFile', 'isFavorite', isFavorite)
             // verify required parameter 'fileExtension' is not null or undefined
@@ -4497,12 +4497,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
                 localVarFormParams.append('deviceId', deviceId as any);
             }
     
-            if (createdAt !== undefined) { 
-                localVarFormParams.append('createdAt', createdAt as any);
+            if (fileCreatedAt !== undefined) { 
+                localVarFormParams.append('fileCreatedAt', fileCreatedAt as any);
             }
     
-            if (modifiedAt !== undefined) { 
-                localVarFormParams.append('modifiedAt', modifiedAt as any);
+            if (fileModifiedAt !== undefined) { 
+                localVarFormParams.append('fileModifiedAt', fileModifiedAt as any);
             }
     
             if (isFavorite !== undefined) { 
@@ -4772,8 +4772,8 @@ export const AssetApiFp = function(configuration?: Configuration) {
          * @param {any} assetData 
          * @param {string} deviceAssetId 
          * @param {string} deviceId 
-         * @param {string} createdAt 
-         * @param {string} modifiedAt 
+         * @param {string} fileCreatedAt 
+         * @param {string} fileModifiedAt 
          * @param {boolean} isFavorite 
          * @param {string} fileExtension 
          * @param {any} [livePhotoData] 
@@ -4782,8 +4782,8 @@ export const AssetApiFp = function(configuration?: Configuration) {
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async uploadFile(assetType: AssetTypeEnum, assetData: any, deviceAssetId: string, deviceId: string, createdAt: string, modifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> {
-            const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration, options);
+        async uploadFile(assetType: AssetTypeEnum, assetData: any, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
     }
@@ -5002,8 +5002,8 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
          * @param {any} assetData 
          * @param {string} deviceAssetId 
          * @param {string} deviceId 
-         * @param {string} createdAt 
-         * @param {string} modifiedAt 
+         * @param {string} fileCreatedAt 
+         * @param {string} fileModifiedAt 
          * @param {boolean} isFavorite 
          * @param {string} fileExtension 
          * @param {any} [livePhotoData] 
@@ -5012,8 +5012,8 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        uploadFile(assetType: AssetTypeEnum, assetData: any, deviceAssetId: string, deviceId: string, createdAt: string, modifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options?: any): AxiosPromise<AssetFileUploadResponseDto> {
-            return localVarFp.uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration, options).then((request) => request(axios, basePath));
+        uploadFile(assetType: AssetTypeEnum, assetData: any, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options?: any): AxiosPromise<AssetFileUploadResponseDto> {
+            return localVarFp.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration, options).then((request) => request(axios, basePath));
         },
     };
 };
@@ -5275,8 +5275,8 @@ export class AssetApi extends BaseAPI {
      * @param {any} assetData 
      * @param {string} deviceAssetId 
      * @param {string} deviceId 
-     * @param {string} createdAt 
-     * @param {string} modifiedAt 
+     * @param {string} fileCreatedAt 
+     * @param {string} fileModifiedAt 
      * @param {boolean} isFavorite 
      * @param {string} fileExtension 
      * @param {any} [livePhotoData] 
@@ -5286,8 +5286,8 @@ export class AssetApi extends BaseAPI {
      * @throws {RequiredError}
      * @memberof AssetApi
      */
-    public uploadFile(assetType: AssetTypeEnum, assetData: any, deviceAssetId: string, deviceId: string, createdAt: string, modifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig) {
-        return AssetApiFp(this.configuration).uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration, options).then((request) => request(this.axios, this.basePath));
+    public uploadFile(assetType: AssetTypeEnum, assetData: any, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig) {
+        return AssetApiFp(this.configuration).uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration, options).then((request) => request(this.axios, this.basePath));
     }
 }
 

+ 1 - 1
web/src/api/open-api/base.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.47.2
+ * The version of the OpenAPI document: 1.47.3
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/common.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.47.2
+ * The version of the OpenAPI document: 1.47.3
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/configuration.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.47.2
+ * The version of the OpenAPI document: 1.47.3
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/index.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.47.2
+ * The version of the OpenAPI document: 1.47.3
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 2 - 2
web/src/lib/components/album-page/album-viewer.svelte

@@ -96,8 +96,8 @@
 	};
 
 	const getDateRange = () => {
-		const startDate = new Date(album.assets[0].createdAt);
-		const endDate = new Date(album.assets[album.assetCount - 1].createdAt);
+		const startDate = new Date(album.assets[0].fileCreatedAt);
+		const endDate = new Date(album.assets[album.assetCount - 1].fileCreatedAt);
 
 		const startDateString = startDate.toLocaleDateString(locale, albumDateFormat);
 		const endDateString = endDate.toLocaleDateString(locale, albumDateFormat);

+ 2 - 2
web/src/lib/components/photos-page/asset-date-group.svelte

@@ -31,7 +31,7 @@
 	let hoveredDateGroup = '';
 	$: assetsGroupByDate = lodash
 		.chain(assets)
-		.groupBy((a) => new Date(a.createdAt).toLocaleDateString(locale, groupDateFormat))
+		.groupBy((a) => new Date(a.fileCreatedAt).toLocaleDateString(locale, groupDateFormat))
 		.sortBy((group) => assets.indexOf(group[0]))
 		.value();
 
@@ -114,7 +114,7 @@
 	bind:clientHeight={actualBucketHeight}
 >
 	{#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)}
-		{@const dateGroupTitle = new Date(assetsInDateGroup[0].createdAt).toLocaleDateString(
+		{@const dateGroupTitle = new Date(assetsInDateGroup[0].fileCreatedAt).toLocaleDateString(
 			locale,
 			groupDateFormat
 		)}

+ 1 - 1
web/src/lib/stores/asset-interaction.store.ts

@@ -65,7 +65,7 @@ function createAssetInteractionStore() {
 	const navigateAsset = async (direction: 'next' | 'previous') => {
 		// Flatten and sort the asset by date if there are new assets
 		if (assetSortedByDate.length === 0 || savedAssetLength !== _assetGridState.assets.length) {
-			assetSortedByDate = sortBy(_assetGridState.assets, (a) => a.createdAt);
+			assetSortedByDate = sortBy(_assetGridState.assets, (a) => a.fileCreatedAt);
 			savedAssetLength = _assetGridState.assets.length;
 		}
 

+ 3 - 3
web/src/lib/utils/file-uploader.ts

@@ -69,7 +69,7 @@ async function fileUploader(
 	const assetType = mimeType.split('/')[0].toUpperCase();
 	const fileExtension = getFilenameExtension(asset.name);
 	const formData = new FormData();
-	const createdAt = new Date(asset.lastModified).toISOString();
+	const fileCreatedAt = new Date(asset.lastModified).toISOString();
 	const deviceAssetId = 'web' + '-' + asset.name + '-' + asset.lastModified;
 
 	try {
@@ -83,10 +83,10 @@ async function fileUploader(
 		formData.append('assetType', assetType);
 
 		// Get Asset Created Date
-		formData.append('createdAt', createdAt);
+		formData.append('fileCreatedAt', fileCreatedAt);
 
 		// Get Asset Modified At
-		formData.append('modifiedAt', new Date(asset.lastModified).toISOString());
+		formData.append('fileModifiedAt', new Date(asset.lastModified).toISOString());
 
 		// Set Asset is Favorite to false
 		formData.append('isFavorite', 'false');