diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml index eee2c9f488..2aaf73ef22 100644 --- a/.github/workflows/check-openapi.yml +++ b/.github/workflows/check-openapi.yml @@ -19,7 +19,7 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/cli/package.json b/cli/package.json index d553ffb299..849957ae36 100644 --- a/cli/package.json +++ b/cli/package.json @@ -13,7 +13,7 @@ "cli" ], "devDependencies": { - "@eslint/js": "^9.8.0", + "@eslint/js": "^10.0.0", "@immich/sdk": "workspace:*", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", @@ -25,11 +25,11 @@ "byte-size": "^9.0.0", "cli-progress": "^3.12.0", "commander": "^12.0.0", - "eslint": "^9.14.0", + "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^62.0.0", - "globals": "^16.0.0", + "eslint-plugin-unicorn": "^63.0.0", + "globals": "^17.0.0", "mock-fs": "^5.2.0", "prettier": "^3.7.4", "prettier-plugin-organize-imports": "^4.0.0", diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md index 2fb5a1c56a..6e8246b06c 100644 --- a/docs/docs/features/libraries.md +++ b/docs/docs/features/libraries.md @@ -80,6 +80,10 @@ There is an automatic scan job that is scheduled to run once a day. Its schedule This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library management page. +### Deleting a Library + +When deleting an external library, all assets inside are immediately deleted along with the library. Note that while a library can take a long time to fully delete in the background, it is immediately removed from the library list. If the deletion process is interrupted (for example, due to server restart), it will be cleaned up in the next nightly cron job. The cleanup process can also be manually initiated by clicking the "Scan All Libraries" button in the library list. + ## Usage Let's show a concrete example where we add an existing gallery to Immich. Here, we have the following folders we want to add: diff --git a/e2e/package.json b/e2e/package.json index 02facc450d..ac1ae081b3 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -24,7 +24,7 @@ "author": "", "license": "GNU Affero General Public License version 3", "devDependencies": { - "@eslint/js": "^9.8.0", + "@eslint/js": "^10.0.0", "@faker-js/faker": "^10.1.0", "@immich/cli": "workspace:*", "@immich/e2e-auth-server": "workspace:*", @@ -37,12 +37,12 @@ "@types/pngjs": "^6.0.4", "@types/supertest": "^6.0.2", "dotenv": "^17.2.3", - "eslint": "^9.14.0", + "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^62.0.0", - "exiftool-vendored": "^34.3.0", - "globals": "^16.0.0", + "eslint-plugin-unicorn": "^63.0.0", + "exiftool-vendored": "^35.0.0", + "globals": "^17.0.0", "luxon": "^3.4.4", "pg": "^8.11.3", "pngjs": "^7.0.0", diff --git a/e2e/src/specs/web/shared-link.e2e-spec.ts b/e2e/src/specs/web/shared-link.e2e-spec.ts index 017bc0fcb2..f6d1ec98d4 100644 --- a/e2e/src/specs/web/shared-link.e2e-spec.ts +++ b/e2e/src/specs/web/shared-link.e2e-spec.ts @@ -45,8 +45,7 @@ test.describe('Shared Links', () => { await page.goto(`/share/${sharedLink.key}`); await page.getByRole('heading', { name: 'Test Album' }).waitFor(); await page.locator(`[data-asset-id="${asset.id}"]`).hover(); - await page.waitForSelector('[data-group] svg'); - await page.getByRole('checkbox').click(); + await page.waitForSelector(`[data-asset-id="${asset.id}"] [role="checkbox"]`); await Promise.all([page.waitForEvent('download'), page.getByRole('button', { name: 'Download' }).click()]); }); diff --git a/e2e/src/ui/specs/timeline/timeline.e2e-spec.ts b/e2e/src/ui/specs/timeline/timeline.e2e-spec.ts index 9408f6079a..6a7ce82672 100644 --- a/e2e/src/ui/specs/timeline/timeline.e2e-spec.ts +++ b/e2e/src/ui/specs/timeline/timeline.e2e-spec.ts @@ -438,7 +438,7 @@ test.describe('Timeline', () => { const asset = getAsset(timelineRestData, album.assetIds[0])!; await pageUtils.goToAsset(page, asset.fileCreatedAt); await thumbnailUtils.expectInViewport(page, asset.id); - await thumbnailUtils.expectSelectedReadonly(page, asset.id); + await thumbnailUtils.expectSelectedDisabled(page, asset.id); }); test('Add photos to album', async ({ page }) => { const album = timelineRestData.album; @@ -447,7 +447,7 @@ test.describe('Timeline', () => { const asset = getAsset(timelineRestData, album.assetIds[0])!; await pageUtils.goToAsset(page, asset.fileCreatedAt); await thumbnailUtils.expectInViewport(page, asset.id); - await thumbnailUtils.expectSelectedReadonly(page, asset.id); + await thumbnailUtils.expectSelectedDisabled(page, asset.id); await pageUtils.selectDay(page, 'Tue, Feb 27, 2024'); const put = pageRoutePromise(page, `**/api/albums/${album.id}/assets`, async (route, request) => { const requestJson = request.postDataJSON(); diff --git a/e2e/src/ui/specs/timeline/utils.ts b/e2e/src/ui/specs/timeline/utils.ts index e3799a7c3b..d3e4e5f7ec 100644 --- a/e2e/src/ui/specs/timeline/utils.ts +++ b/e2e/src/ui/specs/timeline/utils.ts @@ -102,9 +102,9 @@ export const thumbnailUtils = { async expectThumbnailIsNotArchive(page: Page, assetId: string) { await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(0); }, - async expectSelectedReadonly(page: Page, assetId: string) { + async expectSelectedDisabled(page: Page, assetId: string) { await expect( - page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"][data-selected]`), + page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"][data-selected][data-disabled]`), ).toBeVisible(); }, async expectTimelineHasOnScreenAssets(page: Page) { diff --git a/i18n/en.json b/i18n/en.json index a8e34c9209..8bfd1cfbaf 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2322,6 +2322,7 @@ "unstack_action_prompt": "{count} unstacked", "unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}", "unsupported_field_type": "Unsupported field type", + "unsupported_file_type": "File {file} can't be uploaded because its file type {type} is not supported.", "untagged": "Untagged", "untitled_workflow": "Untitled workflow", "up_next": "Up next", diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index 25f59a8fe5..1540d391e4 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -654,18 +654,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/25/fab23259a52ece5670dcb8452e1af34b89e6135ecc17cd4b54b4b479eac6/fsspec-2023.12.2-py3-none-any.whl", hash = "sha256:d800d87f72189a745fa3d6b033b9dc4a34ad069f60ca60b943a63599f5501960", size = 168979, upload-time = "2023-12-11T21:19:52.446Z" }, ] -[[package]] -name = "ftfy" -version = "6.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a5/d3/8650919bc3c7c6e90ee3fa7fd618bf373cbbe55dff043bd67353dbb20cd8/ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec", size = 308927, upload-time = "2024-10-26T00:50:35.149Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821, upload-time = "2024-10-26T00:50:33.425Z" }, -] - [[package]] name = "gevent" version = "24.10.3" @@ -788,14 +776,14 @@ wheels = [ [[package]] name = "gunicorn" -version = "23.0.0" +version = "25.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/13/ef67f59f6a7896fdc2c1d62b5665c5219d6b0a9a1784938eb9a28e55e128/gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616", size = 594377, upload-time = "2026-02-13T11:09:58.989Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, + { url = "https://files.pythonhosted.org/packages/da/73/4ad5b1f6a2e21cf1e85afdaad2b7b1a933985e2f5d679147a1953aaa192c/gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b", size = 197067, upload-time = "2026-02-13T11:09:57.146Z" }, ] [[package]] @@ -939,7 +927,6 @@ source = { editable = "." } dependencies = [ { name = "aiocache" }, { name = "fastapi" }, - { name = "ftfy" }, { name = "gunicorn" }, { name = "huggingface-hub" }, { name = "insightface" }, @@ -1018,7 +1005,6 @@ types = [ requires-dist = [ { name = "aiocache", specifier = ">=0.12.1,<1.0" }, { name = "fastapi", specifier = ">=0.95.2,<1.0" }, - { name = "ftfy", specifier = ">=6.1.1" }, { name = "gunicorn", specifier = ">=21.1.0" }, { name = "huggingface-hub", specifier = ">=0.20.1,<1.0" }, { name = "insightface", specifier = ">=0.7.3,<1.0" }, diff --git a/mise.toml b/mise.toml index 5e3088974c..a87b1c3a29 100644 --- a/mise.toml +++ b/mise.toml @@ -16,7 +16,7 @@ config_roots = [ [tools] node = "24.13.1" flutter = "3.35.7" -pnpm = "10.29.3" +pnpm = "10.30.0" terragrunt = "0.98.0" opentofu = "1.11.4" java = "21.0.2" diff --git a/mobile/drift_schemas/main/drift_schema_v20.json b/mobile/drift_schemas/main/drift_schema_v20.json new file mode 100644 index 0000000000..f85af83439 --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v20.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"library_id","getter_name":"libraryId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_edited","getter_name":"isEdited","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_edited\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_edited\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"stack_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"i_cloud_id","getter_name":"iCloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"linked_remote_album_id","getter_name":"linkedRemoteAlbumId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":6,"references":[3,5],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":7,"references":[6],"type":"index","data":{"on":6,"name":"idx_local_album_asset_album_asset","sql":"CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)","unique":false,"columns":[]}},{"id":8,"references":[4],"type":"index","data":{"on":4,"name":"idx_remote_album_owner_id","sql":"CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)","unique":false,"columns":[]}},{"id":9,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":10,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_cloud_id","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)","unique":false,"columns":[]}},{"id":11,"references":[2],"type":"index","data":{"on":2,"name":"idx_stack_primary_asset_id","sql":"CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)","unique":false,"columns":[]}},{"id":12,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_owner_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)","unique":false,"columns":[]}},{"id":13,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n","unique":true,"columns":[]}},{"id":14,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_library_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n","unique":true,"columns":[]}},{"id":15,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)","unique":false,"columns":[]}},{"id":16,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_stack_id","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)","unique":false,"columns":[]}},{"id":17,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_local_date_time_day","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))","unique":false,"columns":[]}},{"id":18,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_local_date_time_month","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))","unique":false,"columns":[]}},{"id":19,"references":[],"type":"table","data":{"name":"auth_user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"pin_code","getter_name":"pinCode","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":20,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"key","getter_name":"key","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(UserMetadataKey.values)","dart_type_name":"UserMetadataKey"}},{"name":"value","getter_name":"value","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userMetadataConverter","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id","key"]}},{"id":21,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":22,"references":[1],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"lens","getter_name":"lens","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":23,"references":[1,4],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":24,"references":[4,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":25,"references":[1],"type":"table","data":{"name":"remote_asset_cloud_id_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"cloud_id","getter_name":"cloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":26,"references":[0],"type":"table","data":{"name":"memory_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(MemoryTypeEnum.values)","dart_type_name":"MemoryTypeEnum"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_saved","getter_name":"isSaved","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_saved\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_saved\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"memory_at","getter_name":"memoryAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"seen_at","getter_name":"seenAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"show_at","getter_name":"showAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hide_at","getter_name":"hideAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":27,"references":[1,26],"type":"table","data":{"name":"memory_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"memory_id","getter_name":"memoryId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES memory_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES memory_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","memory_id"]}},{"id":28,"references":[0],"type":"table","data":{"name":"person_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"face_asset_id","getter_name":"faceAssetId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_hidden","getter_name":"isHidden","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_hidden\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_hidden\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"birth_date","getter_name":"birthDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":29,"references":[1,28],"type":"table","data":{"name":"asset_face_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"person_id","getter_name":"personId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES person_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES person_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"image_width","getter_name":"imageWidth","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"image_height","getter_name":"imageHeight","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x1","getter_name":"boundingBoxX1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y1","getter_name":"boundingBoxY1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x2","getter_name":"boundingBoxX2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y2","getter_name":"boundingBoxY2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_visible","getter_name":"isVisible","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_visible\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_visible\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":30,"references":[],"type":"table","data":{"name":"store_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"string_value","getter_name":"stringValue","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"int_value","getter_name":"intValue","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":31,"references":[],"type":"table","data":{"name":"trashed_local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"source","getter_name":"source","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TrashOrigin.values)","dart_type_name":"TrashOrigin"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id","album_id"]}},{"id":32,"references":[21],"type":"index","data":{"on":21,"name":"idx_partner_shared_with_id","sql":"CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)","unique":false,"columns":[]}},{"id":33,"references":[22],"type":"index","data":{"on":22,"name":"idx_lat_lng","sql":"CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)","unique":false,"columns":[]}},{"id":34,"references":[23],"type":"index","data":{"on":23,"name":"idx_remote_album_asset_album_asset","sql":"CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)","unique":false,"columns":[]}},{"id":35,"references":[25],"type":"index","data":{"on":25,"name":"idx_remote_asset_cloud_id","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)","unique":false,"columns":[]}},{"id":36,"references":[28],"type":"index","data":{"on":28,"name":"idx_person_owner_id","sql":"CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)","unique":false,"columns":[]}},{"id":37,"references":[29],"type":"index","data":{"on":29,"name":"idx_asset_face_person_id","sql":"CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)","unique":false,"columns":[]}},{"id":38,"references":[29],"type":"index","data":{"on":29,"name":"idx_asset_face_asset_id","sql":"CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)","unique":false,"columns":[]}},{"id":39,"references":[31],"type":"index","data":{"on":31,"name":"idx_trashed_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":40,"references":[31],"type":"index","data":{"on":31,"name":"idx_trashed_local_asset_album","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)","unique":false,"columns":[]}}]} \ No newline at end of file diff --git a/mobile/lib/constants/enums.dart b/mobile/lib/constants/enums.dart index 350f6b80fa..32ef9bbbed 100644 --- a/mobile/lib/constants/enums.dart +++ b/mobile/lib/constants/enums.dart @@ -18,3 +18,5 @@ enum ActionSource { timeline, viewer } enum CleanupStep { selectDate, scan, delete } enum AssetKeepType { none, photosOnly, videosOnly } + +enum AssetDateAggregation { start, end } diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index 0cf3f3e1c1..945ba8eb3f 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -43,8 +43,8 @@ class RemoteAlbumService { AlbumSortMode.title => albums.sortedBy((album) => album.name), AlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt), AlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount), - AlbumSortMode.mostRecent => await _sortByNewestAsset(albums), - AlbumSortMode.mostOldest => await _sortByOldestAsset(albums), + AlbumSortMode.mostRecent => await _sortByAssetDate(albums, aggregation: AssetDateAggregation.end), + AlbumSortMode.mostOldest => await _sortByAssetDate(albums, aggregation: AssetDateAggregation.start), }; final effectiveOrder = isReverse ? sortMode.defaultOrder.reverse() : sortMode.defaultOrder; @@ -172,46 +172,25 @@ class RemoteAlbumService { return _repository.getAlbumsContainingAsset(assetId); } - Future> _sortByNewestAsset(List albums) async { - // map album IDs to their newest asset dates - final Map> assetTimestampFutures = {}; - for (final album in albums) { - assetTimestampFutures[album.id] = _repository.getNewestAssetTimestamp(album.id); + Future> _sortByAssetDate( + List albums, { + required AssetDateAggregation aggregation, + }) async { + if (albums.isEmpty) return []; + + final albumIds = albums.map((e) => e.id).toList(); + final sortedIds = await _repository.getSortedAlbumIds(albumIds, aggregation: aggregation); + + final albumMap = Map.fromEntries(albums.map((a) => MapEntry(a.id, a))); + + final sortedAlbums = sortedIds.map((id) => albumMap[id]).whereType().toList(); + + if (sortedAlbums.length < albums.length) { + final returnedIdSet = sortedIds.toSet(); + final emptyAlbums = albums.where((a) => !returnedIdSet.contains(a.id)); + sortedAlbums.addAll(emptyAlbums); } - // await all database queries - final entries = await Future.wait( - assetTimestampFutures.entries.map((entry) async => MapEntry(entry.key, await entry.value)), - ); - final assetTimestamps = Map.fromEntries(entries); - - final sorted = albums.sorted((a, b) { - final aDate = assetTimestamps[a.id] ?? DateTime.fromMillisecondsSinceEpoch(0); - final bDate = assetTimestamps[b.id] ?? DateTime.fromMillisecondsSinceEpoch(0); - return aDate.compareTo(bDate); - }); - - return sorted; - } - - Future> _sortByOldestAsset(List albums) async { - // map album IDs to their oldest asset dates - final Map> assetTimestampFutures = { - for (final album in albums) album.id: _repository.getOldestAssetTimestamp(album.id), - }; - - // await all database queries - final entries = await Future.wait( - assetTimestampFutures.entries.map((entry) async => MapEntry(entry.key, await entry.value)), - ); - final assetTimestamps = Map.fromEntries(entries); - - final sorted = albums.sorted((a, b) { - final aDate = assetTimestamps[a.id] ?? DateTime.fromMillisecondsSinceEpoch(0); - final bDate = assetTimestamps[b.id] ?? DateTime.fromMillisecondsSinceEpoch(0); - return aDate.compareTo(bDate); - }); - - return sorted; + return sortedAlbums; } } diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index af1c94ca71..2bda6cd683 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -68,12 +68,12 @@ class SyncStreamService { return false; } - final semVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_); + final serverSemVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_); final value = Store.get(StoreKey.syncMigrationStatus, "[]"); final migrations = (jsonDecode(value) as List).cast(); int previousLength = migrations.length; - await _runPreSyncTasks(migrations, semVer); + await _runPreSyncTasks(migrations, serverSemVer); if (migrations.length != previousLength) { _logger.info("Updated pre-sync migration status: $migrations"); @@ -82,10 +82,14 @@ class SyncStreamService { // Start the sync stream and handle events bool shouldReset = false; - await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true); + await _syncApiRepository.streamChanges( + _handleEvents, + serverVersion: serverSemVer, + onReset: () => shouldReset = true, + ); if (shouldReset) { _logger.info("Resetting sync state as requested by server"); - await _syncApiRepository.streamChanges(_handleEvents); + await _syncApiRepository.streamChanges(_handleEvents, serverVersion: serverSemVer); } previousLength = migrations.length; @@ -282,6 +286,8 @@ class SyncStreamService { return _syncStreamRepository.deletePeopleV1(data.cast()); case SyncEntityType.assetFaceV1: return _syncStreamRepository.updateAssetFacesV1(data.cast()); + case SyncEntityType.assetFaceV2: + return _syncStreamRepository.updateAssetFacesV2(data.cast()); case SyncEntityType.assetFaceDeleteV1: return _syncStreamRepository.deleteAssetFacesV1(data.cast()); default: diff --git a/mobile/lib/infrastructure/entities/asset_face.entity.dart b/mobile/lib/infrastructure/entities/asset_face.entity.dart index 45a0b436bd..40fe9ab1c1 100644 --- a/mobile/lib/infrastructure/entities/asset_face.entity.dart +++ b/mobile/lib/infrastructure/entities/asset_face.entity.dart @@ -28,6 +28,10 @@ class AssetFaceEntity extends Table with DriftDefaultsMixin { TextColumn get sourceType => text()(); + BoolColumn get isVisible => boolean().withDefault(const Constant(true))(); + + DateTimeColumn get deletedAt => dateTime().nullable()(); + @override Set get primaryKey => {id}; } diff --git a/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart b/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart index 7f2f3825e3..c97dd545a8 100644 --- a/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart @@ -5,11 +5,12 @@ import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.da as i1; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart' as i2; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' - as i3; -import 'package:drift/internal/modular.dart' as i4; + as i4; +import 'package:drift/internal/modular.dart' as i5; import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart' - as i5; + as i6; typedef $$AssetFaceEntityTableCreateCompanionBuilder = i1.AssetFaceEntityCompanion Function({ @@ -23,6 +24,8 @@ typedef $$AssetFaceEntityTableCreateCompanionBuilder = required int boundingBoxX2, required int boundingBoxY2, required String sourceType, + i0.Value isVisible, + i0.Value deletedAt, }); typedef $$AssetFaceEntityTableUpdateCompanionBuilder = i1.AssetFaceEntityCompanion Function({ @@ -36,6 +39,8 @@ typedef $$AssetFaceEntityTableUpdateCompanionBuilder = i0.Value boundingBoxX2, i0.Value boundingBoxY2, i0.Value sourceType, + i0.Value isVisible, + i0.Value deletedAt, }); final class $$AssetFaceEntityTableReferences @@ -51,29 +56,29 @@ final class $$AssetFaceEntityTableReferences super.$_typedResult, ); - static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => - i4.ReadDatabaseContainer(db) - .resultSet('remote_asset_entity') + static i4.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') .createAlias( i0.$_aliasNameGenerator( - i4.ReadDatabaseContainer(db) + i5.ReadDatabaseContainer(db) .resultSet('asset_face_entity') .assetId, - i4.ReadDatabaseContainer( + i5.ReadDatabaseContainer( db, - ).resultSet('remote_asset_entity').id, + ).resultSet('remote_asset_entity').id, ), ); - i3.$$RemoteAssetEntityTableProcessedTableManager get assetId { + i4.$$RemoteAssetEntityTableProcessedTableManager get assetId { final $_column = $_itemColumn('asset_id')!; - final manager = i3 + final manager = i4 .$$RemoteAssetEntityTableTableManager( $_db, - i4.ReadDatabaseContainer( + i5.ReadDatabaseContainer( $_db, - ).resultSet('remote_asset_entity'), + ).resultSet('remote_asset_entity'), ) .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); @@ -83,29 +88,29 @@ final class $$AssetFaceEntityTableReferences ); } - static i5.$PersonEntityTable _personIdTable(i0.GeneratedDatabase db) => - i4.ReadDatabaseContainer(db) - .resultSet('person_entity') + static i6.$PersonEntityTable _personIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('person_entity') .createAlias( i0.$_aliasNameGenerator( - i4.ReadDatabaseContainer(db) + i5.ReadDatabaseContainer(db) .resultSet('asset_face_entity') .personId, - i4.ReadDatabaseContainer( + i5.ReadDatabaseContainer( db, - ).resultSet('person_entity').id, + ).resultSet('person_entity').id, ), ); - i5.$$PersonEntityTableProcessedTableManager? get personId { + i6.$$PersonEntityTableProcessedTableManager? get personId { final $_column = $_itemColumn('person_id'); if ($_column == null) return null; - final manager = i5 + final manager = i6 .$$PersonEntityTableTableManager( $_db, - i4.ReadDatabaseContainer( + i5.ReadDatabaseContainer( $_db, - ).resultSet('person_entity'), + ).resultSet('person_entity'), ) .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_personIdTable($_db)); @@ -165,24 +170,34 @@ class $$AssetFaceEntityTableFilterComposer builder: (column) => i0.ColumnFilters(column), ); - i3.$$RemoteAssetEntityTableFilterComposer get assetId { - final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( + i0.ColumnFilters get isVisible => $composableBuilder( + column: $table.isVisible, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => i0.ColumnFilters(column), + ); + + i4.$$RemoteAssetEntityTableFilterComposer get assetId { + final i4.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.assetId, - referencedTable: i4.ReadDatabaseContainer( + referencedTable: i5.ReadDatabaseContainer( $db, - ).resultSet('remote_asset_entity'), + ).resultSet('remote_asset_entity'), getReferencedColumn: (t) => t.id, builder: ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => i3.$$RemoteAssetEntityTableFilterComposer( + }) => i4.$$RemoteAssetEntityTableFilterComposer( $db: $db, - $table: i4.ReadDatabaseContainer( + $table: i5.ReadDatabaseContainer( $db, - ).resultSet('remote_asset_entity'), + ).resultSet('remote_asset_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -192,24 +207,24 @@ class $$AssetFaceEntityTableFilterComposer return composer; } - i5.$$PersonEntityTableFilterComposer get personId { - final i5.$$PersonEntityTableFilterComposer composer = $composerBuilder( + i6.$$PersonEntityTableFilterComposer get personId { + final i6.$$PersonEntityTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.personId, - referencedTable: i4.ReadDatabaseContainer( + referencedTable: i5.ReadDatabaseContainer( $db, - ).resultSet('person_entity'), + ).resultSet('person_entity'), getReferencedColumn: (t) => t.id, builder: ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => i5.$$PersonEntityTableFilterComposer( + }) => i6.$$PersonEntityTableFilterComposer( $db: $db, - $table: i4.ReadDatabaseContainer( + $table: i5.ReadDatabaseContainer( $db, - ).resultSet('person_entity'), + ).resultSet('person_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -269,25 +284,35 @@ class $$AssetFaceEntityTableOrderingComposer builder: (column) => i0.ColumnOrderings(column), ); - i3.$$RemoteAssetEntityTableOrderingComposer get assetId { - final i3.$$RemoteAssetEntityTableOrderingComposer composer = + i0.ColumnOrderings get isVisible => $composableBuilder( + column: $table.isVisible, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => i0.ColumnOrderings(column), + ); + + i4.$$RemoteAssetEntityTableOrderingComposer get assetId { + final i4.$$RemoteAssetEntityTableOrderingComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.assetId, - referencedTable: i4.ReadDatabaseContainer( + referencedTable: i5.ReadDatabaseContainer( $db, - ).resultSet('remote_asset_entity'), + ).resultSet('remote_asset_entity'), getReferencedColumn: (t) => t.id, builder: ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => i3.$$RemoteAssetEntityTableOrderingComposer( + }) => i4.$$RemoteAssetEntityTableOrderingComposer( $db: $db, - $table: i4.ReadDatabaseContainer( + $table: i5.ReadDatabaseContainer( $db, - ).resultSet('remote_asset_entity'), + ).resultSet('remote_asset_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -297,24 +322,24 @@ class $$AssetFaceEntityTableOrderingComposer return composer; } - i5.$$PersonEntityTableOrderingComposer get personId { - final i5.$$PersonEntityTableOrderingComposer composer = $composerBuilder( + i6.$$PersonEntityTableOrderingComposer get personId { + final i6.$$PersonEntityTableOrderingComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.personId, - referencedTable: i4.ReadDatabaseContainer( + referencedTable: i5.ReadDatabaseContainer( $db, - ).resultSet('person_entity'), + ).resultSet('person_entity'), getReferencedColumn: (t) => t.id, builder: ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => i5.$$PersonEntityTableOrderingComposer( + }) => i6.$$PersonEntityTableOrderingComposer( $db: $db, - $table: i4.ReadDatabaseContainer( + $table: i5.ReadDatabaseContainer( $db, - ).resultSet('person_entity'), + ).resultSet('person_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -372,25 +397,31 @@ class $$AssetFaceEntityTableAnnotationComposer builder: (column) => column, ); - i3.$$RemoteAssetEntityTableAnnotationComposer get assetId { - final i3.$$RemoteAssetEntityTableAnnotationComposer composer = + i0.GeneratedColumn get isVisible => + $composableBuilder(column: $table.isVisible, builder: (column) => column); + + i0.GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + i4.$$RemoteAssetEntityTableAnnotationComposer get assetId { + final i4.$$RemoteAssetEntityTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.assetId, - referencedTable: i4.ReadDatabaseContainer( + referencedTable: i5.ReadDatabaseContainer( $db, - ).resultSet('remote_asset_entity'), + ).resultSet('remote_asset_entity'), getReferencedColumn: (t) => t.id, builder: ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => i3.$$RemoteAssetEntityTableAnnotationComposer( + }) => i4.$$RemoteAssetEntityTableAnnotationComposer( $db: $db, - $table: i4.ReadDatabaseContainer( + $table: i5.ReadDatabaseContainer( $db, - ).resultSet('remote_asset_entity'), + ).resultSet('remote_asset_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -400,24 +431,24 @@ class $$AssetFaceEntityTableAnnotationComposer return composer; } - i5.$$PersonEntityTableAnnotationComposer get personId { - final i5.$$PersonEntityTableAnnotationComposer composer = $composerBuilder( + i6.$$PersonEntityTableAnnotationComposer get personId { + final i6.$$PersonEntityTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.personId, - referencedTable: i4.ReadDatabaseContainer( + referencedTable: i5.ReadDatabaseContainer( $db, - ).resultSet('person_entity'), + ).resultSet('person_entity'), getReferencedColumn: (t) => t.id, builder: ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => i5.$$PersonEntityTableAnnotationComposer( + }) => i6.$$PersonEntityTableAnnotationComposer( $db: $db, - $table: i4.ReadDatabaseContainer( + $table: i5.ReadDatabaseContainer( $db, - ).resultSet('person_entity'), + ).resultSet('person_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -468,6 +499,8 @@ class $$AssetFaceEntityTableTableManager i0.Value boundingBoxX2 = const i0.Value.absent(), i0.Value boundingBoxY2 = const i0.Value.absent(), i0.Value sourceType = const i0.Value.absent(), + i0.Value isVisible = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), }) => i1.AssetFaceEntityCompanion( id: id, assetId: assetId, @@ -479,6 +512,8 @@ class $$AssetFaceEntityTableTableManager boundingBoxX2: boundingBoxX2, boundingBoxY2: boundingBoxY2, sourceType: sourceType, + isVisible: isVisible, + deletedAt: deletedAt, ), createCompanionCallback: ({ @@ -492,6 +527,8 @@ class $$AssetFaceEntityTableTableManager required int boundingBoxX2, required int boundingBoxY2, required String sourceType, + i0.Value isVisible = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), }) => i1.AssetFaceEntityCompanion.insert( id: id, assetId: assetId, @@ -503,6 +540,8 @@ class $$AssetFaceEntityTableTableManager boundingBoxX2: boundingBoxX2, boundingBoxY2: boundingBoxY2, sourceType: sourceType, + isVisible: isVisible, + deletedAt: deletedAt, ), withReferenceMapper: (p0) => p0 .map( @@ -709,6 +748,33 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity type: i0.DriftSqlType.string, requiredDuringInsert: true, ); + static const i0.VerificationMeta _isVisibleMeta = const i0.VerificationMeta( + 'isVisible', + ); + @override + late final i0.GeneratedColumn isVisible = i0.GeneratedColumn( + 'is_visible', + aliasedName, + false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_visible" IN (0, 1))', + ), + defaultValue: const i3.Constant(true), + ); + static const i0.VerificationMeta _deletedAtMeta = const i0.VerificationMeta( + 'deletedAt', + ); + @override + late final i0.GeneratedColumn deletedAt = + i0.GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + ); @override List get $columns => [ id, @@ -721,6 +787,8 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity boundingBoxX2, boundingBoxY2, sourceType, + isVisible, + deletedAt, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -824,6 +892,18 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity } else if (isInserting) { context.missing(_sourceTypeMeta); } + if (data.containsKey('is_visible')) { + context.handle( + _isVisibleMeta, + isVisible.isAcceptableOrUnknown(data['is_visible']!, _isVisibleMeta), + ); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } return context; } @@ -873,6 +953,14 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity i0.DriftSqlType.string, data['${effectivePrefix}source_type'], )!, + isVisible: attachedDatabase.typeMapping.read( + i0.DriftSqlType.bool, + data['${effectivePrefix}is_visible'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), ); } @@ -899,6 +987,8 @@ class AssetFaceEntityData extends i0.DataClass final int boundingBoxX2; final int boundingBoxY2; final String sourceType; + final bool isVisible; + final DateTime? deletedAt; const AssetFaceEntityData({ required this.id, required this.assetId, @@ -910,6 +1000,8 @@ class AssetFaceEntityData extends i0.DataClass required this.boundingBoxX2, required this.boundingBoxY2, required this.sourceType, + required this.isVisible, + this.deletedAt, }); @override Map toColumns(bool nullToAbsent) { @@ -926,6 +1018,10 @@ class AssetFaceEntityData extends i0.DataClass map['bounding_box_x2'] = i0.Variable(boundingBoxX2); map['bounding_box_y2'] = i0.Variable(boundingBoxY2); map['source_type'] = i0.Variable(sourceType); + map['is_visible'] = i0.Variable(isVisible); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = i0.Variable(deletedAt); + } return map; } @@ -945,6 +1041,8 @@ class AssetFaceEntityData extends i0.DataClass boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), sourceType: serializer.fromJson(json['sourceType']), + isVisible: serializer.fromJson(json['isVisible']), + deletedAt: serializer.fromJson(json['deletedAt']), ); } @override @@ -961,6 +1059,8 @@ class AssetFaceEntityData extends i0.DataClass 'boundingBoxX2': serializer.toJson(boundingBoxX2), 'boundingBoxY2': serializer.toJson(boundingBoxY2), 'sourceType': serializer.toJson(sourceType), + 'isVisible': serializer.toJson(isVisible), + 'deletedAt': serializer.toJson(deletedAt), }; } @@ -975,6 +1075,8 @@ class AssetFaceEntityData extends i0.DataClass int? boundingBoxX2, int? boundingBoxY2, String? sourceType, + bool? isVisible, + i0.Value deletedAt = const i0.Value.absent(), }) => i1.AssetFaceEntityData( id: id ?? this.id, assetId: assetId ?? this.assetId, @@ -986,6 +1088,8 @@ class AssetFaceEntityData extends i0.DataClass boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, sourceType: sourceType ?? this.sourceType, + isVisible: isVisible ?? this.isVisible, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, ); AssetFaceEntityData copyWithCompanion(i1.AssetFaceEntityCompanion data) { return AssetFaceEntityData( @@ -1013,6 +1117,8 @@ class AssetFaceEntityData extends i0.DataClass sourceType: data.sourceType.present ? data.sourceType.value : this.sourceType, + isVisible: data.isVisible.present ? data.isVisible.value : this.isVisible, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, ); } @@ -1028,7 +1134,9 @@ class AssetFaceEntityData extends i0.DataClass ..write('boundingBoxY1: $boundingBoxY1, ') ..write('boundingBoxX2: $boundingBoxX2, ') ..write('boundingBoxY2: $boundingBoxY2, ') - ..write('sourceType: $sourceType') + ..write('sourceType: $sourceType, ') + ..write('isVisible: $isVisible, ') + ..write('deletedAt: $deletedAt') ..write(')')) .toString(); } @@ -1045,6 +1153,8 @@ class AssetFaceEntityData extends i0.DataClass boundingBoxX2, boundingBoxY2, sourceType, + isVisible, + deletedAt, ); @override bool operator ==(Object other) => @@ -1059,7 +1169,9 @@ class AssetFaceEntityData extends i0.DataClass other.boundingBoxY1 == this.boundingBoxY1 && other.boundingBoxX2 == this.boundingBoxX2 && other.boundingBoxY2 == this.boundingBoxY2 && - other.sourceType == this.sourceType); + other.sourceType == this.sourceType && + other.isVisible == this.isVisible && + other.deletedAt == this.deletedAt); } class AssetFaceEntityCompanion @@ -1074,6 +1186,8 @@ class AssetFaceEntityCompanion final i0.Value boundingBoxX2; final i0.Value boundingBoxY2; final i0.Value sourceType; + final i0.Value isVisible; + final i0.Value deletedAt; const AssetFaceEntityCompanion({ this.id = const i0.Value.absent(), this.assetId = const i0.Value.absent(), @@ -1085,6 +1199,8 @@ class AssetFaceEntityCompanion this.boundingBoxX2 = const i0.Value.absent(), this.boundingBoxY2 = const i0.Value.absent(), this.sourceType = const i0.Value.absent(), + this.isVisible = const i0.Value.absent(), + this.deletedAt = const i0.Value.absent(), }); AssetFaceEntityCompanion.insert({ required String id, @@ -1097,6 +1213,8 @@ class AssetFaceEntityCompanion required int boundingBoxX2, required int boundingBoxY2, required String sourceType, + this.isVisible = const i0.Value.absent(), + this.deletedAt = const i0.Value.absent(), }) : id = i0.Value(id), assetId = i0.Value(assetId), imageWidth = i0.Value(imageWidth), @@ -1117,6 +1235,8 @@ class AssetFaceEntityCompanion i0.Expression? boundingBoxX2, i0.Expression? boundingBoxY2, i0.Expression? sourceType, + i0.Expression? isVisible, + i0.Expression? deletedAt, }) { return i0.RawValuesInsertable({ if (id != null) 'id': id, @@ -1129,6 +1249,8 @@ class AssetFaceEntityCompanion if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, if (sourceType != null) 'source_type': sourceType, + if (isVisible != null) 'is_visible': isVisible, + if (deletedAt != null) 'deleted_at': deletedAt, }); } @@ -1143,6 +1265,8 @@ class AssetFaceEntityCompanion i0.Value? boundingBoxX2, i0.Value? boundingBoxY2, i0.Value? sourceType, + i0.Value? isVisible, + i0.Value? deletedAt, }) { return i1.AssetFaceEntityCompanion( id: id ?? this.id, @@ -1155,6 +1279,8 @@ class AssetFaceEntityCompanion boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, sourceType: sourceType ?? this.sourceType, + isVisible: isVisible ?? this.isVisible, + deletedAt: deletedAt ?? this.deletedAt, ); } @@ -1191,6 +1317,12 @@ class AssetFaceEntityCompanion if (sourceType.present) { map['source_type'] = i0.Variable(sourceType.value); } + if (isVisible.present) { + map['is_visible'] = i0.Variable(isVisible.value); + } + if (deletedAt.present) { + map['deleted_at'] = i0.Variable(deletedAt.value); + } return map; } @@ -1206,7 +1338,9 @@ class AssetFaceEntityCompanion ..write('boundingBoxY1: $boundingBoxY1, ') ..write('boundingBoxX2: $boundingBoxX2, ') ..write('boundingBoxY2: $boundingBoxY2, ') - ..write('sourceType: $sourceType') + ..write('sourceType: $sourceType, ') + ..write('isVisible: $isVisible, ') + ..write('deletedAt: $deletedAt') ..write(')')) .toString(); } diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 5495d21bd3..2d90044aea 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -97,7 +97,7 @@ class Drift extends $Drift implements IDatabaseRepository { } @override - int get schemaVersion => 19; + int get schemaVersion => 20; @override MigrationStrategy get migration => MigrationStrategy( @@ -226,6 +226,10 @@ class Drift extends $Drift implements IDatabaseRepository { await m.createIndex(v19.idxRemoteAssetLocalDateTimeMonth); await m.createIndex(v19.idxStackPrimaryAssetId); }, + from19To20: (m, v20) async { + await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.isVisible); + await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.deletedAt); + }, ), ); diff --git a/mobile/lib/infrastructure/repositories/db.repository.steps.dart b/mobile/lib/infrastructure/repositories/db.repository.steps.dart index e56eb97c75..527b0693c7 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.steps.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.steps.dart @@ -8360,6 +8360,550 @@ final class Schema19 extends i0.VersionedSchema { ); } +final class Schema20 extends i0.VersionedSchema { + Schema20({required super.database}) : super(version: 20); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAlbumAssetAlbumAsset, + idxRemoteAlbumOwnerId, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxStackPrimaryAssetId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + idxRemoteAssetStackId, + idxRemoteAssetLocalDateTimeDay, + idxRemoteAssetLocalDateTimeMonth, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxPartnerSharedWithId, + idxLatLng, + idxRemoteAlbumAssetAlbumAsset, + idxRemoteAssetCloudId, + idxPersonOwnerId, + idxAssetFacePersonId, + idxAssetFaceAssetId, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + late final Shape20 userEntity = Shape20( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_84, + _column_85, + _column_91, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape28 remoteAssetEntity = Shape28( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_86, + _column_101, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 stackEntity = Shape3( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_9, _column_5, _column_15, _column_75], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape26 localAssetEntity = Shape26( + source: i0.VersionedTable( + entityName: 'local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_22, + _column_14, + _column_23, + _column_98, + _column_96, + _column_46, + _column_47, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape19 localAlbumEntity = Shape19( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_90, + _column_33, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape22 localAlbumAssetEntity = Shape22( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_34, _column_35, _column_33], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', + ); + final i1.Index idxRemoteAlbumOwnerId = i1.Index( + 'idx_remote_album_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)', + ); + final i1.Index idxLocalAssetChecksum = i1.Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + final i1.Index idxLocalAssetCloudId = i1.Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + final i1.Index idxStackPrimaryAssetId = i1.Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', + ); + final i1.Index idxRemoteAssetOwnerChecksum = i1.Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + final i1.Index idxRemoteAssetChecksum = i1.Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + final i1.Index idxRemoteAssetStackId = i1.Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', + ); + final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index( + 'idx_remote_asset_local_date_time_day', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))', + ); + final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index( + 'idx_remote_asset_local_date_time_month', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))', + ); + late final Shape21 authUserEntity = Shape21( + source: i0.VersionedTable( + entityName: 'auth_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_2, + _column_84, + _column_85, + _column_92, + _column_93, + _column_7, + _column_94, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(user_id, "key")'], + columns: [_column_25, _column_26, _column_27], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_28, _column_29, _column_30], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_36, _column_60], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(album_id, user_id)'], + columns: [_column_60, _column_25, _column_61], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape27 remoteAssetCloudIdEntity = Shape27( + source: i0.VersionedTable( + entityName: 'remote_asset_cloud_id_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_99, + _column_100, + _column_96, + _column_46, + _column_47, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'], + columns: [_column_36, _column_68], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape14 personEntity = Shape14( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape29 assetFaceEntity = Shape29( + source: i0.VersionedTable( + entityName: 'asset_face_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_36, + _column_76, + _column_77, + _column_78, + _column_79, + _column_80, + _column_81, + _column_82, + _column_83, + _column_102, + _column_18, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape18 storeEntity = Shape18( + source: i0.VersionedTable( + entityName: 'store_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_87, _column_88, _column_89], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape25 trashedLocalAssetEntity = Shape25( + source: i0.VersionedTable( + entityName: 'trashed_local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id, album_id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_95, + _column_22, + _column_14, + _column_23, + _column_97, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxPartnerSharedWithId = i1.Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', + ); + final i1.Index idxLatLng = i1.Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', + ); + final i1.Index idxRemoteAssetCloudId = i1.Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + final i1.Index idxPersonOwnerId = i1.Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', + ); + final i1.Index idxAssetFacePersonId = i1.Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', + ); + final i1.Index idxAssetFaceAssetId = i1.Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', + ); + final i1.Index idxTrashedLocalAssetChecksum = i1.Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + final i1.Index idxTrashedLocalAssetAlbum = i1.Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); +} + +class Shape29 extends i0.VersionedTable { + Shape29({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get assetId => + columnsByName['asset_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get personId => + columnsByName['person_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get imageWidth => + columnsByName['image_width']! as i1.GeneratedColumn; + i1.GeneratedColumn get imageHeight => + columnsByName['image_height']! as i1.GeneratedColumn; + i1.GeneratedColumn get boundingBoxX1 => + columnsByName['bounding_box_x1']! as i1.GeneratedColumn; + i1.GeneratedColumn get boundingBoxY1 => + columnsByName['bounding_box_y1']! as i1.GeneratedColumn; + i1.GeneratedColumn get boundingBoxX2 => + columnsByName['bounding_box_x2']! as i1.GeneratedColumn; + i1.GeneratedColumn get boundingBoxY2 => + columnsByName['bounding_box_y2']! as i1.GeneratedColumn; + i1.GeneratedColumn get sourceType => + columnsByName['source_type']! as i1.GeneratedColumn; + i1.GeneratedColumn get isVisible => + columnsByName['is_visible']! as i1.GeneratedColumn; + i1.GeneratedColumn get deletedAt => + columnsByName['deleted_at']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_102(String aliasedName) => + i1.GeneratedColumn( + 'is_visible', + aliasedName, + false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_visible" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -8379,6 +8923,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema17 schema) from16To17, required Future Function(i1.Migrator m, Schema18 schema) from17To18, required Future Function(i1.Migrator m, Schema19 schema) from18To19, + required Future Function(i1.Migrator m, Schema20 schema) from19To20, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -8472,6 +9017,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from18To19(migrator, schema); return 19; + case 19: + final schema = Schema20(database: database); + final migrator = i1.Migrator(database, schema); + await from19To20(migrator, schema); + return 20; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -8497,6 +9047,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema17 schema) from16To17, required Future Function(i1.Migrator m, Schema18 schema) from17To18, required Future Function(i1.Migrator m, Schema19 schema) from18To19, + required Future Function(i1.Migrator m, Schema20 schema) from19To20, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, @@ -8517,5 +9068,6 @@ i1.OnUpgrade stepByStep({ from16To17: from16To17, from17To18: from17To18, from18To19: from18To19, + from19To20: from19To20, ), ); diff --git a/mobile/lib/infrastructure/repositories/local_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_asset.repository.dart index 9d7cbd831b..6f6ef20aeb 100644 --- a/mobile/lib/infrastructure/repositories/local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart @@ -184,7 +184,8 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { } if (keepFavorites) { - whereClause = whereClause & _db.localAssetEntity.isFavorite.equals(false); + whereClause = + whereClause & _db.localAssetEntity.isFavorite.equals(false) & _db.remoteAssetEntity.isFavorite.equals(false); } query.where(whereClause); diff --git a/mobile/lib/infrastructure/repositories/people.repository.dart b/mobile/lib/infrastructure/repositories/people.repository.dart index 40402b6f72..9e55d44867 100644 --- a/mobile/lib/infrastructure/repositories/people.repository.dart +++ b/mobile/lib/infrastructure/repositories/people.repository.dart @@ -16,9 +16,15 @@ class DriftPeopleRepository extends DriftDatabaseRepository { } Future> getAssetPeople(String assetId) async { - final query = _db.select(_db.assetFaceEntity).join([ - innerJoin(_db.personEntity, _db.personEntity.id.equalsExp(_db.assetFaceEntity.personId)), - ])..where(_db.assetFaceEntity.assetId.equals(assetId) & _db.personEntity.isHidden.equals(false)); + final query = + _db.select(_db.assetFaceEntity).join([ + innerJoin(_db.personEntity, _db.personEntity.id.equalsExp(_db.assetFaceEntity.personId)), + ])..where( + _db.assetFaceEntity.assetId.equals(assetId) & + _db.assetFaceEntity.isVisible.equals(true) & + _db.assetFaceEntity.deletedAt.isNull() & + _db.personEntity.isHidden.equals(false), + ); return query.map((row) { final person = row.readTable(_db.personEntity); @@ -39,7 +45,9 @@ class DriftPeopleRepository extends DriftDatabaseRepository { ..where( people.isHidden.equals(false) & assets.deletedAt.isNull() & - assets.visibility.equalsValue(AssetVisibility.timeline), + assets.visibility.equalsValue(AssetVisibility.timeline) & + faces.isVisible.equals(true) & + faces.deletedAt.isNull(), ) ..groupBy([people.id], having: faces.id.count().isBiggerOrEqualValue(3) | people.name.equals('').not()) ..orderBy([ diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index d7d4a250ad..a594647f19 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -1,6 +1,8 @@ import 'dart:async'; +import 'dart:convert'; import 'package:drift/drift.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; @@ -321,26 +323,32 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { }).watchSingleOrNull(); } - Future getNewestAssetTimestamp(String albumId) { - final query = _db.remoteAlbumAssetEntity.selectOnly() - ..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)) - ..addColumns([_db.remoteAssetEntity.localDateTime.max()]) - ..join([ - innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)), - ]); + Future> getSortedAlbumIds(List albumIds, {required AssetDateAggregation aggregation}) async { + if (albumIds.isEmpty) return []; - return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.max())).getSingleOrNull(); - } + final jsonIds = jsonEncode(albumIds); + final sqlAgg = aggregation == AssetDateAggregation.start ? 'MIN' : 'MAX'; - Future getOldestAssetTimestamp(String albumId) { - final query = _db.remoteAlbumAssetEntity.selectOnly() - ..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)) - ..addColumns([_db.remoteAssetEntity.localDateTime.min()]) - ..join([ - innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)), - ]); + final rows = await _db + .customSelect( + ''' + SELECT + raae.album_id, + $sqlAgg(rae.local_date_time) AS asset_date + FROM json_each(?) ids + INNER JOIN remote_album_asset_entity raae + ON raae.album_id = ids.value + INNER JOIN remote_asset_entity rae + ON rae.id = raae.asset_id + GROUP BY raae.album_id + ORDER BY asset_date ASC + ''', + variables: [Variable(jsonIds)], + readsFrom: {_db.remoteAlbumAssetEntity, _db.remoteAssetEntity}, + ) + .get(); - return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.min())).getSingleOrNull(); + return rows.map((row) => row.read('album_id')).toList(); } Future getCount() { diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index d13083d706..0e5c99edd7 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/semver.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; @@ -25,6 +26,7 @@ class SyncApiRepository { Future streamChanges( Future Function(List, Function() abort, Function() reset) onData, { + required SemVer serverVersion, Function()? onReset, int batchSize = kSyncEventBatchSize, http.Client? httpClient, @@ -64,7 +66,8 @@ class SyncApiRepository { SyncRequestType.partnerStacksV1, SyncRequestType.userMetadataV1, SyncRequestType.peopleV1, - SyncRequestType.assetFacesV1, + if (serverVersion < const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV1, + if (serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV2, ], reset: shouldReset, ).toJson(), @@ -190,6 +193,7 @@ const _kResponseMap = { SyncEntityType.personV1: SyncPersonV1.fromJson, SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson, SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson, + SyncEntityType.assetFaceV2: SyncAssetFaceV2.fromJson, SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson, SyncEntityType.syncCompleteV1: _SyncEmptyDto.fromJson, }; diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 26f89432a5..8ff1c2d59c 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -652,6 +652,37 @@ class SyncStreamRepository extends DriftDatabaseRepository { } } + Future updateAssetFacesV2(Iterable data) async { + try { + await _db.batch((batch) { + for (final assetFace in data) { + final companion = AssetFaceEntityCompanion( + assetId: Value(assetFace.assetId), + personId: Value(assetFace.personId), + imageWidth: Value(assetFace.imageWidth), + imageHeight: Value(assetFace.imageHeight), + boundingBoxX1: Value(assetFace.boundingBoxX1), + boundingBoxY1: Value(assetFace.boundingBoxY1), + boundingBoxX2: Value(assetFace.boundingBoxX2), + boundingBoxY2: Value(assetFace.boundingBoxY2), + sourceType: Value(assetFace.sourceType), + deletedAt: Value(assetFace.deletedAt), + isVisible: Value(assetFace.isVisible), + ); + + batch.insert( + _db.assetFaceEntity, + companion.copyWith(id: Value(assetFace.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: updateAssetFacesV2', error, stack); + rethrow; + } + } + Future deleteAssetFacesV1(Iterable data) async { try { await _db.batch((batch) { diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 7544b4b2ac..e39dc10a8a 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -323,6 +323,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { row.deletedAt.isNull() & row.ownerId.equals(userId) & row.visibility.equalsValue(AssetVisibility.archive), groupBy: groupBy, origin: TimelineOrigin.archive, + joinLocal: true, ); TimelineQuery locked(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder( @@ -421,7 +422,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository { _db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & - _db.assetFaceEntity.personId.equals(personId), + _db.assetFaceEntity.personId.equals(personId) & + _db.assetFaceEntity.isVisible.equals(true) & + _db.assetFaceEntity.deletedAt.isNull(), ); return query.map((row) { @@ -446,7 +449,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository { _db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & - _db.assetFaceEntity.personId.equals(personId), + _db.assetFaceEntity.personId.equals(personId) & + _db.assetFaceEntity.isVisible.equals(true) & + _db.assetFaceEntity.deletedAt.isNull(), ) ..groupBy([dateExp]) ..orderBy([OrderingTerm.desc(dateExp)]); @@ -476,7 +481,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository { _db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & - _db.assetFaceEntity.personId.equals(personId), + _db.assetFaceEntity.personId.equals(personId) & + _db.assetFaceEntity.isVisible.equals(true) & + _db.assetFaceEntity.deletedAt.isNull(), ) ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) ..limit(count, offset: offset); diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index c7d786626c..0d728422d1 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -109,9 +109,43 @@ class SplashScreenPageState extends ConsumerState { if (context.router.current.name == SplashScreenRoute.name) { final needBetaMigration = Store.get(StoreKey.needBetaMigration, false); if (needBetaMigration) { + bool migrate = + (await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text("New Timeline Experience"), + content: const Text( + "The old timeline has been deprecated and will be removed in an upcoming release. Would you like to switch to the new timeline now?", + ), + actions: [ + TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")), + ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")), + ], + ), + )) ?? + false; + if (migrate != true) { + migrate = + (await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text("Are you sure?"), + content: const Text( + "If you choose to remain on the old timeline, you will be automatically migrated to the new timeline in an upcoming release. Would you like to switch now?", + ), + actions: [ + TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")), + ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")), + ], + ), + )) ?? + false; + } await Store.put(StoreKey.needBetaMigration, false); - unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)])); - return; + if (migrate) { + unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)])); + return; + } } unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute())); diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart index cb0e7091c8..0d9bc41734 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/asset_grid/trash_delete_dialog.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; /// This delete action has the following behavior: @@ -22,6 +23,18 @@ class DeleteTrashActionButton extends ConsumerWidget { return; } + final selectCount = ref.watch(multiSelectProvider.select((s) => s.selectedAssets.length)); + + final confirmDelete = + await showDialog( + context: context, + builder: (context) => TrashDeleteDialog(count: selectCount), + ) ?? + false; + if (!confirmDelete) { + return; + } + final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source); ref.read(multiSelectProvider.notifier).reset(); diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart index 125ad36f9a..686b3fcf10 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart @@ -56,7 +56,6 @@ class _AssetPageState extends ConsumerState { final ValueNotifier _videoScaleStateNotifier = ValueNotifier(PhotoViewScaleState.initial); double _snapOffset = 0.0; - double _lastScrollOffset = 0.0; DragStartDetails? _dragStart; _DragIntent _dragIntent = _DragIntent.none; @@ -95,7 +94,6 @@ class _AssetPageState extends ConsumerState { void _showDetails() { if (!_proxyScrollController.hasClients || _snapOffset <= 0) return; - _lastScrollOffset = _proxyScrollController.offset; _proxyScrollController.animateTo(_snapOffset, duration: Durations.medium2, curve: Curves.easeOutCubic); } @@ -109,17 +107,15 @@ class _AssetPageState extends ConsumerState { void _onScroll() { final offset = _proxyScrollController.offset; - if (offset > SnapScrollPhysics.minSnapDistance && offset > _lastScrollOffset) { + if (offset > SnapScrollPhysics.minSnapDistance) { _viewer.setShowingDetails(true); } else if (offset < SnapScrollPhysics.minSnapDistance - kTouchSlop) { _viewer.setShowingDetails(false); } - _lastScrollOffset = offset; } void _beginDrag(DragStartDetails details) { _dragStart = details; - _lastScrollOffset = _proxyScrollController.hasClients ? _proxyScrollController.offset : 0.0; if (_viewController != null) { _initialPhotoViewState = _viewController!.value; diff --git a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart index dd6390406b..7ed02af26b 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart @@ -25,7 +25,7 @@ class _DriftPersonNameEditFormState extends ConsumerState _restoreAssetIndex; - bool get shouldRestoreAssetPosition => _shouldRestoreAssetPosition; - - void setRestoreAssetIndex(int? index) { - _restoreAssetIndex = index; - notifyListeners(); - } - - void setShouldRestoreAssetPosition(bool should) { - _shouldRestoreAssetPosition = should; - notifyListeners(); - } - - void clearRestoreAssetIndex() { - _restoreAssetIndex = null; - notifyListeners(); - } -} - -class _TimelineRestorationProvider extends InheritedNotifier<_TimelineRestorationState> { - const _TimelineRestorationProvider({required super.notifier, required super.child}); - - static _TimelineRestorationState of(BuildContext context) { - return context.dependOnInheritedWidgetOfExactType<_TimelineRestorationProvider>()!.notifier!; - } -} - -class Timeline extends StatefulWidget { +class Timeline extends StatelessWidget { const Timeline({ super.key, this.topSliverWidget, @@ -90,68 +59,38 @@ class Timeline extends StatefulWidget { final bool readOnly; final bool persistentBottomBar; - @override - State createState() => _TimelineState(); -} - -class _TimelineState extends State { - double? _lastWidth; - late final _TimelineRestorationState _restorationState; - - @override - void initState() { - super.initState(); - _restorationState = _TimelineRestorationState(); - } - - @override - void dispose() { - _restorationState.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, floatingActionButton: const DownloadStatusFloatingButton(), body: LayoutBuilder( - builder: (_, constraints) { - if (_lastWidth != null && _lastWidth != constraints.maxWidth) { - _restorationState.setShouldRestoreAssetPosition(true); - } - _lastWidth = constraints.maxWidth; - return _TimelineRestorationProvider( - notifier: _restorationState, - child: ProviderScope( - key: ValueKey(_lastWidth), - overrides: [ - timelineArgsProvider.overrideWith( - (ref) => TimelineArgs( - maxWidth: constraints.maxWidth, - maxHeight: constraints.maxHeight, - columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))), - showStorageIndicator: widget.showStorageIndicator, - withStack: widget.withStack, - groupBy: widget.groupBy, - ), - ), - if (widget.readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()), - ], - child: _SliverTimeline( - key: const ValueKey('_sliver_timeline'), - topSliverWidget: widget.topSliverWidget, - topSliverWidgetHeight: widget.topSliverWidgetHeight, - appBar: widget.appBar, - bottomSheet: widget.bottomSheet, - withScrubber: widget.withScrubber, - persistentBottomBar: widget.persistentBottomBar, - snapToMonth: widget.snapToMonth, - initialScrollOffset: widget.initialScrollOffset, + builder: (_, constraints) => ProviderScope( + overrides: [ + timelineArgsProvider.overrideWith( + (ref) => TimelineArgs( + maxWidth: constraints.maxWidth, + maxHeight: constraints.maxHeight, + columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))), + showStorageIndicator: showStorageIndicator, + withStack: withStack, + groupBy: groupBy, ), ), - ); - }, + if (readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()), + ], + child: _SliverTimeline( + topSliverWidget: topSliverWidget, + topSliverWidgetHeight: topSliverWidgetHeight, + appBar: appBar, + bottomSheet: bottomSheet, + withScrubber: withScrubber, + persistentBottomBar: persistentBottomBar, + snapToMonth: snapToMonth, + initialScrollOffset: initialScrollOffset, + maxWidth: constraints.maxWidth, + ), + ), ), ); } @@ -170,7 +109,6 @@ class _AlwaysReadOnlyNotifier extends ReadOnlyModeNotifier { class _SliverTimeline extends ConsumerStatefulWidget { const _SliverTimeline({ - super.key, this.topSliverWidget, this.topSliverWidgetHeight, this.appBar, @@ -179,6 +117,7 @@ class _SliverTimeline extends ConsumerStatefulWidget { this.persistentBottomBar = false, this.snapToMonth = true, this.initialScrollOffset, + this.maxWidth, }); final Widget? topSliverWidget; @@ -189,6 +128,7 @@ class _SliverTimeline extends ConsumerStatefulWidget { final bool persistentBottomBar; final bool snapToMonth; final double? initialScrollOffset; + final double? maxWidth; @override ConsumerState createState() => _SliverTimelineState(); @@ -207,6 +147,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { int _perRow = 4; double _scaleFactor = 3.0; double _baseScaleFactor = 3.0; + int? _restoreAssetIndex; @override void initState() { @@ -225,6 +166,20 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { ref.listenManual(multiSelectProvider.select((s) => s.isEnabled), _onMultiSelectionToggled); } + @override + void didUpdateWidget(covariant _SliverTimeline oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.maxWidth != oldWidget.maxWidth) { + final asyncSegments = ref.read(timelineSegmentProvider); + asyncSegments.whenData((segments) { + final index = _getCurrentAssetIndex(segments); + // Refresh to wait for new segments to be generated with the updated width before restoring the scroll position + final _ = ref.refresh(timelineArgsProvider); + _restoreAssetIndex = index; + }); + } + } + void _onEvent(Event event) { switch (event) { case ScrollToTopEvent(): @@ -242,21 +197,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { } } - void _onMultiSelectionToggled(_, bool isEnabled) { - EventStream.shared.emit(MultiSelectToggleEvent(isEnabled)); - } - void _restoreAssetPosition(_) { - final restorationState = _TimelineRestorationProvider.of(context); - if (!restorationState.shouldRestoreAssetPosition || restorationState.restoreAssetIndex == null) return; + if (_restoreAssetIndex == null) return; final asyncSegments = ref.read(timelineSegmentProvider); asyncSegments.whenData((segments) { - final targetSegment = segments.lastWhereOrNull( - (segment) => segment.firstAssetIndex <= restorationState.restoreAssetIndex!, - ); + final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _restoreAssetIndex!); if (targetSegment != null) { - final assetIndexInSegment = restorationState.restoreAssetIndex! - targetSegment.firstAssetIndex; + final assetIndexInSegment = _restoreAssetIndex! - targetSegment.firstAssetIndex; final newColumnCount = ref.read(timelineArgsProvider).columnCount; final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor(); final targetRowIndex = targetSegment.firstIndex + 1 + rowIndexInSegment; @@ -268,7 +216,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { }); } }); - restorationState.clearRestoreAssetIndex(); + _restoreAssetIndex = null; + } + + void _onMultiSelectionToggled(_, bool isEnabled) { + EventStream.shared.emit(MultiSelectToggleEvent(isEnabled)); } int? _getCurrentAssetIndex(List segments) { @@ -478,67 +430,56 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { return PrimaryScrollController( controller: _scrollController, - child: NotificationListener( - onNotification: (notification) { - final currentIndex = _getCurrentAssetIndex(segments); - if (currentIndex != null && mounted) { - _TimelineRestorationProvider.of(context).setRestoreAssetIndex(currentIndex); - } - return false; - }, - child: RawGestureDetector( - gestures: { - CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => CustomScaleGestureRecognizer(), - (CustomScaleGestureRecognizer scale) { - scale.onStart = (details) { - _baseScaleFactor = _scaleFactor; - }; + child: RawGestureDetector( + gestures: { + CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers( + () => CustomScaleGestureRecognizer(), + (CustomScaleGestureRecognizer scale) { + scale.onStart = (details) { + _baseScaleFactor = _scaleFactor; + }; - scale.onUpdate = (details) { - final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0); - final newPerRow = 7 - newScaleFactor.toInt(); + scale.onUpdate = (details) { + final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0); + final newPerRow = 7 - newScaleFactor.toInt(); + + if (newPerRow != _perRow) { final targetAssetIndex = _getCurrentAssetIndex(segments); + setState(() { + _scaleFactor = newScaleFactor; + _perRow = newPerRow; + _restoreAssetIndex = targetAssetIndex; + }); - if (newPerRow != _perRow) { - final restorationState = _TimelineRestorationProvider.of(context); - setState(() { - _scaleFactor = newScaleFactor; - _perRow = newPerRow; - }); - - restorationState.setRestoreAssetIndex(targetAssetIndex); - restorationState.setShouldRestoreAssetPosition(true); - ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow); - } - }; - }, - ), - }, - child: TimelineDragRegion( - onStart: !isReadonlyModeEnabled ? _setDragStartIndex : null, - onAssetEnter: _handleDragAssetEnter, - onEnd: !isReadonlyModeEnabled ? _stopDrag : null, - onScroll: _dragScroll, - onScrollStart: () { - // Minimize the bottom sheet when drag selection starts - ref.read(timelineStateProvider.notifier).setScrolling(true); + ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow); + } + }; }, - child: Stack( - children: [ - timeline, - if (isMultiSelectStatusVisible) - Positioned( - top: MediaQuery.paddingOf(context).top, - left: 25, - child: const SizedBox( - height: kToolbarHeight, - child: Center(child: _MultiSelectStatusButton()), - ), + ), + }, + child: TimelineDragRegion( + onStart: !isReadonlyModeEnabled ? _setDragStartIndex : null, + onAssetEnter: _handleDragAssetEnter, + onEnd: !isReadonlyModeEnabled ? _stopDrag : null, + onScroll: _dragScroll, + onScrollStart: () { + // Minimize the bottom sheet when drag selection starts + ref.read(timelineStateProvider.notifier).setScrolling(true); + }, + child: Stack( + children: [ + timeline, + if (isBottomWidgetVisible) + Positioned( + top: MediaQuery.paddingOf(context).top, + left: 25, + child: const SizedBox( + height: kToolbarHeight, + child: Center(child: _MultiSelectStatusButton()), ), - if (isBottomWidgetVisible) widget.bottomSheet!, - ], - ), + ), + if (isBottomWidgetVisible) widget.bottomSheet!, + ], ), ), ), diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 70f9ba88c7..30a46daa56 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -30,11 +30,10 @@ import 'package:immich_mobile/utils/datetime_helpers.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; - // ignore: import_rule_photo_manager import 'package:photo_manager/photo_manager.dart'; -const int targetVersion = 21; +const int targetVersion = 22; Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { final hasVersion = Store.tryGet(StoreKey.version) != null; @@ -100,6 +99,10 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { } } + if (version < 22 && !Store.isBetaTimelineEnabled) { + await Store.put(StoreKey.needBetaMigration, true); + } + if (targetVersion >= 12) { await Store.put(StoreKey.version, targetVersion); return; diff --git a/mobile/lib/utils/option.dart b/mobile/lib/utils/option.dart new file mode 100644 index 0000000000..3470e8489e --- /dev/null +++ b/mobile/lib/utils/option.dart @@ -0,0 +1,58 @@ +sealed class Option { + const Option(); + + const factory Option.some(T value) = Some; + + const factory Option.none() = None; + + factory Option.fromNullable(T? value) => value != null ? Some(value) : None(); + + @pragma('vm:prefer-inline') + bool get isSome => this is Some; + + @pragma('vm:prefer-inline') + bool get isNone => this is None; + + @pragma('vm:prefer-inline') + T? get unwrapOrNull => switch (this) { + Some(:final value) => value, + None() => null, + }; + + U fold(U Function(T value) onSome, U Function() onNone) => switch (this) { + Some(:final value) => onSome(value), + None() => onNone(), + }; + + @override + String toString() => switch (this) { + Some(:final value) => 'Some($value)', + None() => 'None', + }; +} + +final class Some extends Option { + final T value; + + const Some(this.value); + + @override + bool operator ==(Object other) => other is Some && other.value == value; + + @override + int get hashCode => value.hashCode; +} + +final class None extends Option { + const None(); + + @override + bool operator ==(Object other) => other is None; + + @override + int get hashCode => 0; +} + +extension ObjectOptionExtension on T? { + Option toOption() => Option.fromNullable(this); +} diff --git a/mobile/lib/widgets/asset_grid/trash_delete_dialog.dart b/mobile/lib/widgets/asset_grid/trash_delete_dialog.dart new file mode 100644 index 0000000000..eaca04b481 --- /dev/null +++ b/mobile/lib/widgets/asset_grid/trash_delete_dialog.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_ui/immich_ui.dart'; + +class TrashDeleteDialog extends StatelessWidget { + const TrashDeleteDialog({super.key, required this.count}); + + final int count; + + @override + Widget build(BuildContext context) { + return AlertDialog( + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), + title: Text(context.t.permanently_delete), + content: ImmichFormattedText(context.t.permanently_delete_assets_prompt(count: count)), + actions: [ + SizedBox( + width: double.infinity, + height: 48, + child: FilledButton( + onPressed: () => context.pop(false), + style: FilledButton.styleFrom( + backgroundColor: context.colorScheme.surfaceDim, + foregroundColor: context.primaryColor, + ), + child: Text(context.t.cancel, style: const TextStyle(fontWeight: FontWeight.bold)), + ), + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + height: 48, + + child: FilledButton( + onPressed: () => context.pop(true), + style: FilledButton.styleFrom( + backgroundColor: context.colorScheme.errorContainer, + foregroundColor: context.colorScheme.onErrorContainer, + ), + child: Text(context.t.delete, style: const TextStyle(fontWeight: FontWeight.bold)), + ), + ), + ], + ); + } +} diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index d6b516a078..e86d313294 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -135,7 +135,7 @@ class AdvancedSettings extends HookConsumerWidget { title: "advanced_settings_enable_alternate_media_filter_title".tr(), subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(), ), - const BetaTimelineListTile(), + if (!Store.isBetaTimelineEnabled) const BetaTimelineListTile(), if (Store.isBetaTimelineEnabled) SettingsSwitchListTile( valueNotifier: readonlyModeEnabled, diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index 5fda01a594..a026b99028 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -421,14 +421,14 @@ class AssetsApi { /// /// * [String] id (required): /// - /// * [AssetEditActionListDto] assetEditActionListDto (required): - Future editAssetWithHttpInfo(String id, AssetEditActionListDto assetEditActionListDto,) async { + /// * [AssetEditsCreateDto] assetEditsCreateDto (required): + Future editAssetWithHttpInfo(String id, AssetEditsCreateDto assetEditsCreateDto,) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/edits' .replaceAll('{id}', id); // ignore: prefer_final_locals - Object? postBody = assetEditActionListDto; + Object? postBody = assetEditsCreateDto; final queryParams = []; final headerParams = {}; @@ -456,9 +456,9 @@ class AssetsApi { /// /// * [String] id (required): /// - /// * [AssetEditActionListDto] assetEditActionListDto (required): - Future editAsset(String id, AssetEditActionListDto assetEditActionListDto,) async { - final response = await editAssetWithHttpInfo(id, assetEditActionListDto,); + /// * [AssetEditsCreateDto] assetEditsCreateDto (required): + Future editAsset(String id, AssetEditsCreateDto assetEditsCreateDto,) async { + final response = await editAssetWithHttpInfo(id, assetEditsCreateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -466,7 +466,7 @@ class AssetsApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetEditsDto',) as AssetEditsDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetEditsResponseDto',) as AssetEditsResponseDto; } return null; @@ -576,7 +576,7 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future getAssetEdits(String id,) async { + Future getAssetEdits(String id,) async { final response = await getAssetEditsWithHttpInfo(id,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -585,7 +585,7 @@ class AssetsApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetEditsDto',) as AssetEditsDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetEditsResponseDto',) as AssetEditsResponseDto; } return null; diff --git a/mobile/openapi/lib/model/asset_edit_action_crop.dart b/mobile/openapi/lib/model/asset_edit_action_item_dto.dart similarity index 59% rename from mobile/openapi/lib/model/asset_edit_action_crop.dart rename to mobile/openapi/lib/model/asset_edit_action_item_dto.dart index 7672ed825b..7829de4bd5 100644 --- a/mobile/openapi/lib/model/asset_edit_action_crop.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class AssetEditActionCrop { - /// Returns a new [AssetEditActionCrop] instance. - AssetEditActionCrop({ +class AssetEditActionItemDto { + /// Returns a new [AssetEditActionItemDto] instance. + AssetEditActionItemDto({ required this.action, required this.parameters, }); @@ -20,10 +20,10 @@ class AssetEditActionCrop { /// Type of edit action to perform AssetEditAction action; - CropParameters parameters; + AssetEditActionItemDtoParameters parameters; @override - bool operator ==(Object other) => identical(this, other) || other is AssetEditActionCrop && + bool operator ==(Object other) => identical(this, other) || other is AssetEditActionItemDto && other.action == action && other.parameters == parameters; @@ -34,7 +34,7 @@ class AssetEditActionCrop { (parameters.hashCode); @override - String toString() => 'AssetEditActionCrop[action=$action, parameters=$parameters]'; + String toString() => 'AssetEditActionItemDto[action=$action, parameters=$parameters]'; Map toJson() { final json = {}; @@ -43,27 +43,27 @@ class AssetEditActionCrop { return json; } - /// Returns a new [AssetEditActionCrop] instance and imports its values from + /// Returns a new [AssetEditActionItemDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static AssetEditActionCrop? fromJson(dynamic value) { - upgradeDto(value, "AssetEditActionCrop"); + static AssetEditActionItemDto? fromJson(dynamic value) { + upgradeDto(value, "AssetEditActionItemDto"); if (value is Map) { final json = value.cast(); - return AssetEditActionCrop( + return AssetEditActionItemDto( action: AssetEditAction.fromJson(json[r'action'])!, - parameters: CropParameters.fromJson(json[r'parameters'])!, + parameters: AssetEditActionItemDtoParameters.fromJson(json[r'parameters'])!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = AssetEditActionCrop.fromJson(row); + final value = AssetEditActionItemDto.fromJson(row); if (value != null) { result.add(value); } @@ -72,12 +72,12 @@ class AssetEditActionCrop { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = AssetEditActionCrop.fromJson(entry.value); + final value = AssetEditActionItemDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -86,14 +86,14 @@ class AssetEditActionCrop { return map; } - // maps a json object with a list of AssetEditActionCrop-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of AssetEditActionItemDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = AssetEditActionCrop.listFromJson(entry.value, growable: growable,); + map[entry.key] = AssetEditActionItemDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart b/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart new file mode 100644 index 0000000000..fc67aa022f --- /dev/null +++ b/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart @@ -0,0 +1,153 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AssetEditActionItemDtoParameters { + /// Returns a new [AssetEditActionItemDtoParameters] instance. + AssetEditActionItemDtoParameters({ + required this.height, + required this.width, + required this.x, + required this.y, + required this.angle, + required this.axis, + }); + + /// Height of the crop + /// + /// Minimum value: 1 + num height; + + /// Width of the crop + /// + /// Minimum value: 1 + num width; + + /// Top-Left X coordinate of crop + /// + /// Minimum value: 0 + num x; + + /// Top-Left Y coordinate of crop + /// + /// Minimum value: 0 + num y; + + /// Rotation angle in degrees + num angle; + + /// Axis to mirror along + MirrorAxis axis; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetEditActionItemDtoParameters && + other.height == height && + other.width == width && + other.x == x && + other.y == y && + other.angle == angle && + other.axis == axis; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (height.hashCode) + + (width.hashCode) + + (x.hashCode) + + (y.hashCode) + + (angle.hashCode) + + (axis.hashCode); + + @override + String toString() => 'AssetEditActionItemDtoParameters[height=$height, width=$width, x=$x, y=$y, angle=$angle, axis=$axis]'; + + Map toJson() { + final json = {}; + json[r'height'] = this.height; + json[r'width'] = this.width; + json[r'x'] = this.x; + json[r'y'] = this.y; + json[r'angle'] = this.angle; + json[r'axis'] = this.axis; + return json; + } + + /// Returns a new [AssetEditActionItemDtoParameters] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetEditActionItemDtoParameters? fromJson(dynamic value) { + upgradeDto(value, "AssetEditActionItemDtoParameters"); + if (value is Map) { + final json = value.cast(); + + return AssetEditActionItemDtoParameters( + height: num.parse('${json[r'height']}'), + width: num.parse('${json[r'width']}'), + x: num.parse('${json[r'x']}'), + y: num.parse('${json[r'y']}'), + angle: num.parse('${json[r'angle']}'), + axis: MirrorAxis.fromJson(json[r'axis'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetEditActionItemDtoParameters.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AssetEditActionItemDtoParameters.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetEditActionItemDtoParameters-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AssetEditActionItemDtoParameters.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'height', + 'width', + 'x', + 'y', + 'angle', + 'axis', + }; +} + diff --git a/mobile/openapi/lib/model/asset_edit_action_list_dto_edits_inner.dart b/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart similarity index 54% rename from mobile/openapi/lib/model/asset_edit_action_list_dto_edits_inner.dart rename to mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart index 00c9be2381..a23a1ef5f3 100644 --- a/mobile/openapi/lib/model/asset_edit_action_list_dto_edits_inner.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart @@ -10,60 +10,67 @@ part of openapi.api; -class AssetEditActionListDtoEditsInner { - /// Returns a new [AssetEditActionListDtoEditsInner] instance. - AssetEditActionListDtoEditsInner({ +class AssetEditActionItemResponseDto { + /// Returns a new [AssetEditActionItemResponseDto] instance. + AssetEditActionItemResponseDto({ required this.action, + required this.id, required this.parameters, }); /// Type of edit action to perform AssetEditAction action; - MirrorParameters parameters; + String id; + + AssetEditActionItemDtoParameters parameters; @override - bool operator ==(Object other) => identical(this, other) || other is AssetEditActionListDtoEditsInner && + bool operator ==(Object other) => identical(this, other) || other is AssetEditActionItemResponseDto && other.action == action && + other.id == id && other.parameters == parameters; @override int get hashCode => // ignore: unnecessary_parenthesis (action.hashCode) + + (id.hashCode) + (parameters.hashCode); @override - String toString() => 'AssetEditActionListDtoEditsInner[action=$action, parameters=$parameters]'; + String toString() => 'AssetEditActionItemResponseDto[action=$action, id=$id, parameters=$parameters]'; Map toJson() { final json = {}; json[r'action'] = this.action; + json[r'id'] = this.id; json[r'parameters'] = this.parameters; return json; } - /// Returns a new [AssetEditActionListDtoEditsInner] instance and imports its values from + /// Returns a new [AssetEditActionItemResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static AssetEditActionListDtoEditsInner? fromJson(dynamic value) { - upgradeDto(value, "AssetEditActionListDtoEditsInner"); + static AssetEditActionItemResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetEditActionItemResponseDto"); if (value is Map) { final json = value.cast(); - return AssetEditActionListDtoEditsInner( + return AssetEditActionItemResponseDto( action: AssetEditAction.fromJson(json[r'action'])!, - parameters: MirrorParameters.fromJson(json[r'parameters'])!, + id: mapValueOfType(json, r'id')!, + parameters: AssetEditActionItemDtoParameters.fromJson(json[r'parameters'])!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = AssetEditActionListDtoEditsInner.fromJson(row); + final value = AssetEditActionItemResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -72,12 +79,12 @@ class AssetEditActionListDtoEditsInner { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = AssetEditActionListDtoEditsInner.fromJson(entry.value); + final value = AssetEditActionItemResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -86,14 +93,14 @@ class AssetEditActionListDtoEditsInner { return map; } - // maps a json object with a list of AssetEditActionListDtoEditsInner-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of AssetEditActionItemResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = AssetEditActionListDtoEditsInner.listFromJson(entry.value, growable: growable,); + map[entry.key] = AssetEditActionItemResponseDto.listFromJson(entry.value, growable: growable,); } } return map; @@ -102,6 +109,7 @@ class AssetEditActionListDtoEditsInner { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'action', + 'id', 'parameters', }; } diff --git a/mobile/openapi/lib/model/asset_edit_action_mirror.dart b/mobile/openapi/lib/model/asset_edit_action_mirror.dart deleted file mode 100644 index aef98fc1a8..0000000000 --- a/mobile/openapi/lib/model/asset_edit_action_mirror.dart +++ /dev/null @@ -1,108 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class AssetEditActionMirror { - /// Returns a new [AssetEditActionMirror] instance. - AssetEditActionMirror({ - required this.action, - required this.parameters, - }); - - /// Type of edit action to perform - AssetEditAction action; - - MirrorParameters parameters; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetEditActionMirror && - other.action == action && - other.parameters == parameters; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (action.hashCode) + - (parameters.hashCode); - - @override - String toString() => 'AssetEditActionMirror[action=$action, parameters=$parameters]'; - - Map toJson() { - final json = {}; - json[r'action'] = this.action; - json[r'parameters'] = this.parameters; - return json; - } - - /// Returns a new [AssetEditActionMirror] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetEditActionMirror? fromJson(dynamic value) { - upgradeDto(value, "AssetEditActionMirror"); - if (value is Map) { - final json = value.cast(); - - return AssetEditActionMirror( - action: AssetEditAction.fromJson(json[r'action'])!, - parameters: MirrorParameters.fromJson(json[r'parameters'])!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetEditActionMirror.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = AssetEditActionMirror.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetEditActionMirror-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = AssetEditActionMirror.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'action', - 'parameters', - }; -} - diff --git a/mobile/openapi/lib/model/asset_edit_action_rotate.dart b/mobile/openapi/lib/model/asset_edit_action_rotate.dart deleted file mode 100644 index 302e6a0ce6..0000000000 --- a/mobile/openapi/lib/model/asset_edit_action_rotate.dart +++ /dev/null @@ -1,108 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class AssetEditActionRotate { - /// Returns a new [AssetEditActionRotate] instance. - AssetEditActionRotate({ - required this.action, - required this.parameters, - }); - - /// Type of edit action to perform - AssetEditAction action; - - RotateParameters parameters; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetEditActionRotate && - other.action == action && - other.parameters == parameters; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (action.hashCode) + - (parameters.hashCode); - - @override - String toString() => 'AssetEditActionRotate[action=$action, parameters=$parameters]'; - - Map toJson() { - final json = {}; - json[r'action'] = this.action; - json[r'parameters'] = this.parameters; - return json; - } - - /// Returns a new [AssetEditActionRotate] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetEditActionRotate? fromJson(dynamic value) { - upgradeDto(value, "AssetEditActionRotate"); - if (value is Map) { - final json = value.cast(); - - return AssetEditActionRotate( - action: AssetEditAction.fromJson(json[r'action'])!, - parameters: RotateParameters.fromJson(json[r'parameters'])!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetEditActionRotate.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = AssetEditActionRotate.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetEditActionRotate-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = AssetEditActionRotate.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'action', - 'parameters', - }; -} - diff --git a/mobile/openapi/lib/model/asset_edit_action_list_dto.dart b/mobile/openapi/lib/model/asset_edits_create_dto.dart similarity index 57% rename from mobile/openapi/lib/model/asset_edit_action_list_dto.dart rename to mobile/openapi/lib/model/asset_edits_create_dto.dart index e843c66e8f..9f6fc66904 100644 --- a/mobile/openapi/lib/model/asset_edit_action_list_dto.dart +++ b/mobile/openapi/lib/model/asset_edits_create_dto.dart @@ -10,17 +10,17 @@ part of openapi.api; -class AssetEditActionListDto { - /// Returns a new [AssetEditActionListDto] instance. - AssetEditActionListDto({ +class AssetEditsCreateDto { + /// Returns a new [AssetEditsCreateDto] instance. + AssetEditsCreateDto({ this.edits = const [], }); /// List of edit actions to apply (crop, rotate, or mirror) - List edits; + List edits; @override - bool operator ==(Object other) => identical(this, other) || other is AssetEditActionListDto && + bool operator ==(Object other) => identical(this, other) || other is AssetEditsCreateDto && _deepEquality.equals(other.edits, edits); @override @@ -29,7 +29,7 @@ class AssetEditActionListDto { (edits.hashCode); @override - String toString() => 'AssetEditActionListDto[edits=$edits]'; + String toString() => 'AssetEditsCreateDto[edits=$edits]'; Map toJson() { final json = {}; @@ -37,26 +37,26 @@ class AssetEditActionListDto { return json; } - /// Returns a new [AssetEditActionListDto] instance and imports its values from + /// Returns a new [AssetEditsCreateDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static AssetEditActionListDto? fromJson(dynamic value) { - upgradeDto(value, "AssetEditActionListDto"); + static AssetEditsCreateDto? fromJson(dynamic value) { + upgradeDto(value, "AssetEditsCreateDto"); if (value is Map) { final json = value.cast(); - return AssetEditActionListDto( - edits: AssetEditActionListDtoEditsInner.listFromJson(json[r'edits']), + return AssetEditsCreateDto( + edits: AssetEditActionItemDto.listFromJson(json[r'edits']), ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = AssetEditActionListDto.fromJson(row); + final value = AssetEditsCreateDto.fromJson(row); if (value != null) { result.add(value); } @@ -65,12 +65,12 @@ class AssetEditActionListDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = AssetEditActionListDto.fromJson(entry.value); + final value = AssetEditsCreateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -79,14 +79,14 @@ class AssetEditActionListDto { return map; } - // maps a json object with a list of AssetEditActionListDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of AssetEditsCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = AssetEditActionListDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = AssetEditsCreateDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/asset_edits_dto.dart b/mobile/openapi/lib/model/asset_edits_response_dto.dart similarity index 57% rename from mobile/openapi/lib/model/asset_edits_dto.dart rename to mobile/openapi/lib/model/asset_edits_response_dto.dart index 3bfbce8594..322b4c0a4c 100644 --- a/mobile/openapi/lib/model/asset_edits_dto.dart +++ b/mobile/openapi/lib/model/asset_edits_response_dto.dart @@ -10,21 +10,21 @@ part of openapi.api; -class AssetEditsDto { - /// Returns a new [AssetEditsDto] instance. - AssetEditsDto({ +class AssetEditsResponseDto { + /// Returns a new [AssetEditsResponseDto] instance. + AssetEditsResponseDto({ required this.assetId, this.edits = const [], }); - /// Asset ID to apply edits to + /// Asset ID these edits belong to String assetId; - /// List of edit actions to apply (crop, rotate, or mirror) - List edits; + /// List of edit actions applied to the asset + List edits; @override - bool operator ==(Object other) => identical(this, other) || other is AssetEditsDto && + bool operator ==(Object other) => identical(this, other) || other is AssetEditsResponseDto && other.assetId == assetId && _deepEquality.equals(other.edits, edits); @@ -35,7 +35,7 @@ class AssetEditsDto { (edits.hashCode); @override - String toString() => 'AssetEditsDto[assetId=$assetId, edits=$edits]'; + String toString() => 'AssetEditsResponseDto[assetId=$assetId, edits=$edits]'; Map toJson() { final json = {}; @@ -44,27 +44,27 @@ class AssetEditsDto { return json; } - /// Returns a new [AssetEditsDto] instance and imports its values from + /// Returns a new [AssetEditsResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static AssetEditsDto? fromJson(dynamic value) { - upgradeDto(value, "AssetEditsDto"); + static AssetEditsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetEditsResponseDto"); if (value is Map) { final json = value.cast(); - return AssetEditsDto( + return AssetEditsResponseDto( assetId: mapValueOfType(json, r'assetId')!, - edits: AssetEditActionListDtoEditsInner.listFromJson(json[r'edits']), + edits: AssetEditActionItemResponseDto.listFromJson(json[r'edits']), ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = AssetEditsDto.fromJson(row); + final value = AssetEditsResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -73,12 +73,12 @@ class AssetEditsDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = AssetEditsDto.fromJson(entry.value); + final value = AssetEditsResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -87,14 +87,14 @@ class AssetEditsDto { return map; } - // maps a json object with a list of AssetEditsDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of AssetEditsResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = AssetEditsDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = AssetEditsResponseDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 5422ccf55f..078dd0bdaf 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -156,7 +156,7 @@ class AssetResponseDto { List tags; - /// Thumbhash for thumbnail generation + /// Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting. String? thumbhash; /// Asset type diff --git a/mobile/openapi/lib/model/memory_create_dto.dart b/mobile/openapi/lib/model/memory_create_dto.dart index 7fd938b31a..5b8eeed8fb 100644 --- a/mobile/openapi/lib/model/memory_create_dto.dart +++ b/mobile/openapi/lib/model/memory_create_dto.dart @@ -15,9 +15,11 @@ class MemoryCreateDto { MemoryCreateDto({ this.assetIds = const [], required this.data, + this.hideAt, this.isSaved, required this.memoryAt, this.seenAt, + this.showAt, required this.type, }); @@ -26,6 +28,15 @@ class MemoryCreateDto { OnThisDayDto data; + /// Date when memory should be hidden + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? hideAt; + /// Is memory saved /// /// Please note: This property should have been non-nullable! Since the specification file @@ -47,6 +58,15 @@ class MemoryCreateDto { /// DateTime? seenAt; + /// Date when memory should be shown + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? showAt; + /// Memory type MemoryType type; @@ -54,9 +74,11 @@ class MemoryCreateDto { bool operator ==(Object other) => identical(this, other) || other is MemoryCreateDto && _deepEquality.equals(other.assetIds, assetIds) && other.data == data && + other.hideAt == hideAt && other.isSaved == isSaved && other.memoryAt == memoryAt && other.seenAt == seenAt && + other.showAt == showAt && other.type == type; @override @@ -64,18 +86,25 @@ class MemoryCreateDto { // ignore: unnecessary_parenthesis (assetIds.hashCode) + (data.hashCode) + + (hideAt == null ? 0 : hideAt!.hashCode) + (isSaved == null ? 0 : isSaved!.hashCode) + (memoryAt.hashCode) + (seenAt == null ? 0 : seenAt!.hashCode) + + (showAt == null ? 0 : showAt!.hashCode) + (type.hashCode); @override - String toString() => 'MemoryCreateDto[assetIds=$assetIds, data=$data, isSaved=$isSaved, memoryAt=$memoryAt, seenAt=$seenAt, type=$type]'; + String toString() => 'MemoryCreateDto[assetIds=$assetIds, data=$data, hideAt=$hideAt, isSaved=$isSaved, memoryAt=$memoryAt, seenAt=$seenAt, showAt=$showAt, type=$type]'; Map toJson() { final json = {}; json[r'assetIds'] = this.assetIds; json[r'data'] = this.data; + if (this.hideAt != null) { + json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); + } else { + // json[r'hideAt'] = null; + } if (this.isSaved != null) { json[r'isSaved'] = this.isSaved; } else { @@ -86,6 +115,11 @@ class MemoryCreateDto { json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; + } + if (this.showAt != null) { + json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); + } else { + // json[r'showAt'] = null; } json[r'type'] = this.type; return json; @@ -104,9 +138,11 @@ class MemoryCreateDto { ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) : const [], data: OnThisDayDto.fromJson(json[r'data'])!, + hideAt: mapDateTime(json, r'hideAt', r''), isSaved: mapValueOfType(json, r'isSaved'), memoryAt: mapDateTime(json, r'memoryAt', r'')!, seenAt: mapDateTime(json, r'seenAt', r''), + showAt: mapDateTime(json, r'showAt', r''), type: MemoryType.fromJson(json[r'type'])!, ); } diff --git a/mobile/openapi/lib/model/sync_asset_face_v2.dart b/mobile/openapi/lib/model/sync_asset_face_v2.dart new file mode 100644 index 0000000000..688d71229f --- /dev/null +++ b/mobile/openapi/lib/model/sync_asset_face_v2.dart @@ -0,0 +1,201 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncAssetFaceV2 { + /// Returns a new [SyncAssetFaceV2] instance. + SyncAssetFaceV2({ + required this.assetId, + required this.boundingBoxX1, + required this.boundingBoxX2, + required this.boundingBoxY1, + required this.boundingBoxY2, + required this.deletedAt, + required this.id, + required this.imageHeight, + required this.imageWidth, + required this.isVisible, + required this.personId, + required this.sourceType, + }); + + /// Asset ID + String assetId; + + int boundingBoxX1; + + int boundingBoxX2; + + int boundingBoxY1; + + int boundingBoxY2; + + /// Face deleted at + DateTime? deletedAt; + + /// Asset face ID + String id; + + int imageHeight; + + int imageWidth; + + /// Is the face visible in the asset + bool isVisible; + + /// Person ID + String? personId; + + /// Source type + String sourceType; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAssetFaceV2 && + other.assetId == assetId && + other.boundingBoxX1 == boundingBoxX1 && + other.boundingBoxX2 == boundingBoxX2 && + other.boundingBoxY1 == boundingBoxY1 && + other.boundingBoxY2 == boundingBoxY2 && + other.deletedAt == deletedAt && + other.id == id && + other.imageHeight == imageHeight && + other.imageWidth == imageWidth && + other.isVisible == isVisible && + other.personId == personId && + other.sourceType == sourceType; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode) + + (boundingBoxX1.hashCode) + + (boundingBoxX2.hashCode) + + (boundingBoxY1.hashCode) + + (boundingBoxY2.hashCode) + + (deletedAt == null ? 0 : deletedAt!.hashCode) + + (id.hashCode) + + (imageHeight.hashCode) + + (imageWidth.hashCode) + + (isVisible.hashCode) + + (personId == null ? 0 : personId!.hashCode) + + (sourceType.hashCode); + + @override + String toString() => 'SyncAssetFaceV2[assetId=$assetId, boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, deletedAt=$deletedAt, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, isVisible=$isVisible, personId=$personId, sourceType=$sourceType]'; + + Map toJson() { + final json = {}; + json[r'assetId'] = this.assetId; + json[r'boundingBoxX1'] = this.boundingBoxX1; + json[r'boundingBoxX2'] = this.boundingBoxX2; + json[r'boundingBoxY1'] = this.boundingBoxY1; + json[r'boundingBoxY2'] = this.boundingBoxY2; + if (this.deletedAt != null) { + json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + } else { + // json[r'deletedAt'] = null; + } + json[r'id'] = this.id; + json[r'imageHeight'] = this.imageHeight; + json[r'imageWidth'] = this.imageWidth; + json[r'isVisible'] = this.isVisible; + if (this.personId != null) { + json[r'personId'] = this.personId; + } else { + // json[r'personId'] = null; + } + json[r'sourceType'] = this.sourceType; + return json; + } + + /// Returns a new [SyncAssetFaceV2] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAssetFaceV2? fromJson(dynamic value) { + upgradeDto(value, "SyncAssetFaceV2"); + if (value is Map) { + final json = value.cast(); + + return SyncAssetFaceV2( + assetId: mapValueOfType(json, r'assetId')!, + boundingBoxX1: mapValueOfType(json, r'boundingBoxX1')!, + boundingBoxX2: mapValueOfType(json, r'boundingBoxX2')!, + boundingBoxY1: mapValueOfType(json, r'boundingBoxY1')!, + boundingBoxY2: mapValueOfType(json, r'boundingBoxY2')!, + deletedAt: mapDateTime(json, r'deletedAt', r''), + id: mapValueOfType(json, r'id')!, + imageHeight: mapValueOfType(json, r'imageHeight')!, + imageWidth: mapValueOfType(json, r'imageWidth')!, + isVisible: mapValueOfType(json, r'isVisible')!, + personId: mapValueOfType(json, r'personId'), + sourceType: mapValueOfType(json, r'sourceType')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SyncAssetFaceV2.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SyncAssetFaceV2.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAssetFaceV2-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SyncAssetFaceV2.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetId', + 'boundingBoxX1', + 'boundingBoxX2', + 'boundingBoxY1', + 'boundingBoxY2', + 'deletedAt', + 'id', + 'imageHeight', + 'imageWidth', + 'isVisible', + 'personId', + 'sourceType', + }; +} + diff --git a/mobile/openapi/lib/model/sync_entity_type.dart b/mobile/openapi/lib/model/sync_entity_type.dart index d1e321f39b..e7605a5dd1 100644 --- a/mobile/openapi/lib/model/sync_entity_type.dart +++ b/mobile/openapi/lib/model/sync_entity_type.dart @@ -64,6 +64,7 @@ class SyncEntityType { static const personV1 = SyncEntityType._(r'PersonV1'); static const personDeleteV1 = SyncEntityType._(r'PersonDeleteV1'); static const assetFaceV1 = SyncEntityType._(r'AssetFaceV1'); + static const assetFaceV2 = SyncEntityType._(r'AssetFaceV2'); static const assetFaceDeleteV1 = SyncEntityType._(r'AssetFaceDeleteV1'); static const userMetadataV1 = SyncEntityType._(r'UserMetadataV1'); static const userMetadataDeleteV1 = SyncEntityType._(r'UserMetadataDeleteV1'); @@ -114,6 +115,7 @@ class SyncEntityType { personV1, personDeleteV1, assetFaceV1, + assetFaceV2, assetFaceDeleteV1, userMetadataV1, userMetadataDeleteV1, @@ -199,6 +201,7 @@ class SyncEntityTypeTypeTransformer { case r'PersonV1': return SyncEntityType.personV1; case r'PersonDeleteV1': return SyncEntityType.personDeleteV1; case r'AssetFaceV1': return SyncEntityType.assetFaceV1; + case r'AssetFaceV2': return SyncEntityType.assetFaceV2; case r'AssetFaceDeleteV1': return SyncEntityType.assetFaceDeleteV1; case r'UserMetadataV1': return SyncEntityType.userMetadataV1; case r'UserMetadataDeleteV1': return SyncEntityType.userMetadataDeleteV1; diff --git a/mobile/openapi/lib/model/sync_request_type.dart b/mobile/openapi/lib/model/sync_request_type.dart index 135af3c7bb..3614394d55 100644 --- a/mobile/openapi/lib/model/sync_request_type.dart +++ b/mobile/openapi/lib/model/sync_request_type.dart @@ -42,6 +42,7 @@ class SyncRequestType { static const usersV1 = SyncRequestType._(r'UsersV1'); static const peopleV1 = SyncRequestType._(r'PeopleV1'); static const assetFacesV1 = SyncRequestType._(r'AssetFacesV1'); + static const assetFacesV2 = SyncRequestType._(r'AssetFacesV2'); static const userMetadataV1 = SyncRequestType._(r'UserMetadataV1'); /// List of all possible values in this [enum][SyncRequestType]. @@ -65,6 +66,7 @@ class SyncRequestType { usersV1, peopleV1, assetFacesV1, + assetFacesV2, userMetadataV1, ]; @@ -123,6 +125,7 @@ class SyncRequestTypeTypeTransformer { case r'UsersV1': return SyncRequestType.usersV1; case r'PeopleV1': return SyncRequestType.peopleV1; case r'AssetFacesV1': return SyncRequestType.assetFacesV1; + case r'AssetFacesV2': return SyncRequestType.assetFacesV2; case r'UserMetadataV1': return SyncRequestType.userMetadataV1; default: if (!allowNull) { diff --git a/mobile/packages/ui/lib/immich_ui.dart b/mobile/packages/ui/lib/immich_ui.dart index 909ab65bce..c9e510a162 100644 --- a/mobile/packages/ui/lib/immich_ui.dart +++ b/mobile/packages/ui/lib/immich_ui.dart @@ -1,6 +1,6 @@ export 'src/components/close_button.dart'; export 'src/components/form.dart'; -export 'src/components/html_text.dart'; +export 'src/components/formatted_text.dart'; export 'src/components/icon_button.dart'; export 'src/components/password_input.dart'; export 'src/components/text_button.dart'; diff --git a/mobile/packages/ui/lib/src/components/formatted_text.dart b/mobile/packages/ui/lib/src/components/formatted_text.dart new file mode 100644 index 0000000000..95e42d834d --- /dev/null +++ b/mobile/packages/ui/lib/src/components/formatted_text.dart @@ -0,0 +1,141 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class FormattedSpan { + final TextStyle? style; + final VoidCallback? onTap; + + const FormattedSpan({this.style, this.onTap}); +} + +/// A widget that renders text with optional HTML-style formatting. +/// +/// Supports the following tags: +/// - `` for bold text +/// - `` or any tag ending with `-link` for tappable links +/// +/// Tags must not be nested. Each tag is matched independently left-to-right. +/// +/// By default, `` renders as [FontWeight.bold] and link tags render with an +/// underline and no tap handler. Provide [spanBuilder] to attach tap callbacks +/// or override styles per tag. +/// +/// Bold-only example (no [spanBuilder] needed): +/// ```dart +/// ImmichFormattedText('Delete {count} items?') +/// ``` +/// +/// Link example: +/// ```dart +/// ImmichFormattedText( +/// 'Refer to docs and other', +/// spanBuilder: (tag) => FormattedSpan( +/// onTap: switch (tag) { +/// 'docs-link' => () => launchUrl(docsUrl), +/// 'other-link' => () => launchUrl(otherUrl), +/// _ => null, +/// }, +/// ), +/// ) +/// ``` +class ImmichFormattedText extends StatefulWidget { + final String text; + final TextStyle? style; + final TextAlign? textAlign; + final TextOverflow? overflow; + final int? maxLines; + final bool? softWrap; + final FormattedSpan Function(String tag)? spanBuilder; + + const ImmichFormattedText( + this.text, { + this.spanBuilder, + super.key, + this.style, + this.textAlign, + this.overflow, + this.maxLines, + this.softWrap, + }); + + @override + State createState() => _ImmichFormattedTextState(); +} + +class _ImmichFormattedTextState extends State { + final _recognizers = []; + + // Matches , , or any *-link tag and its content. + static final _tagPattern = RegExp(r'<(b|link|[\w]+-link)>(.*?)', caseSensitive: false, dotAll: true); + + @override + void dispose() { + _disposeRecognizers(); + super.dispose(); + } + + void _disposeRecognizers() { + for (final recognizer in _recognizers) { + recognizer.dispose(); + } + _recognizers.clear(); + } + + List _buildSpans() { + _disposeRecognizers(); + + final spans = []; + int cursor = 0; + + for (final match in _tagPattern.allMatches(widget.text)) { + if (match.start > cursor) { + spans.add(TextSpan(text: widget.text.substring(cursor, match.start))); + } + + final tag = match.group(1)!.toLowerCase(); + final content = match.group(2)!; + final formattedSpan = (widget.spanBuilder ?? _defaultSpanBuilder)(tag); + final style = formattedSpan.style ?? _defaultTextStyle(tag); + + GestureRecognizer? recognizer; + if (formattedSpan.onTap != null) { + recognizer = TapGestureRecognizer()..onTap = formattedSpan.onTap; + _recognizers.add(recognizer); + } + spans.add(TextSpan(text: content, style: style, recognizer: recognizer)); + + cursor = match.end; + } + + if (cursor < widget.text.length) { + spans.add(TextSpan(text: widget.text.substring(cursor))); + } + + return spans; + } + + FormattedSpan _defaultSpanBuilder(String tag) => switch (tag) { + 'b' => const FormattedSpan(style: TextStyle(fontWeight: FontWeight.bold)), + 'link' => const FormattedSpan(style: TextStyle(decoration: TextDecoration.underline)), + _ when tag.endsWith('-link') => const FormattedSpan(style: TextStyle(decoration: TextDecoration.underline)), + _ => const FormattedSpan(), + }; + + TextStyle? _defaultTextStyle(String tag) => switch (tag) { + 'b' => const TextStyle(fontWeight: FontWeight.bold), + 'link' => const TextStyle(decoration: TextDecoration.underline), + _ when tag.endsWith('-link') => const TextStyle(decoration: TextDecoration.underline), + _ => null, + }; + + @override + Widget build(BuildContext context) { + return Text.rich( + TextSpan(style: widget.style, children: _buildSpans()), + textAlign: widget.textAlign, + overflow: widget.overflow, + maxLines: widget.maxLines, + softWrap: widget.softWrap, + ); + } +} diff --git a/mobile/packages/ui/lib/src/components/html_text.dart b/mobile/packages/ui/lib/src/components/html_text.dart deleted file mode 100644 index 72b54b8da5..0000000000 --- a/mobile/packages/ui/lib/src/components/html_text.dart +++ /dev/null @@ -1,189 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:html/dom.dart' as dom; -import 'package:html/parser.dart' as html_parser; - -enum _HtmlTagType { - bold, - link, - unsupported, -} - -class _HtmlTag { - final _HtmlTagType type; - final String tagName; - - const _HtmlTag._({required this.type, required this.tagName}); - - static const unsupported = _HtmlTag._(type: _HtmlTagType.unsupported, tagName: 'unsupported'); - - static _HtmlTag? fromString(dom.Node node) { - final tagName = (node is dom.Element) ? node.localName : null; - if (tagName == null) { - return null; - } - - final tag = tagName.toLowerCase(); - return switch (tag) { - 'b' || 'strong' => _HtmlTag._(type: _HtmlTagType.bold, tagName: tag), - // Convert back to 'link' for handler lookup - 'a' => const _HtmlTag._(type: _HtmlTagType.link, tagName: 'link'), - _ when tag.endsWith('-link') => _HtmlTag._(type: _HtmlTagType.link, tagName: tag), - _ => _HtmlTag.unsupported, - }; - } -} - -/// A widget that renders text with optional HTML-style formatting. -/// -/// Supports the following tags: -/// - `` or `` for bold text -/// - `` or any tag ending with `-link` for tappable links -/// -/// Example: -/// ```dart -/// ImmichHtmlText( -/// 'Refer to docs and other', -/// linkHandlers: { -/// 'link': () => launchUrl(docsUrl), -/// 'other-link': () => launchUrl(otherUrl), -/// }, -/// ) -/// ``` -class ImmichHtmlText extends StatefulWidget { - final String text; - final TextStyle? style; - final TextAlign? textAlign; - final TextOverflow? overflow; - final int? maxLines; - final bool? softWrap; - final Map? linkHandlers; - final TextStyle? linkStyle; - - const ImmichHtmlText( - this.text, { - super.key, - this.style, - this.textAlign, - this.overflow, - this.maxLines, - this.softWrap, - this.linkHandlers, - this.linkStyle, - }); - - @override - State createState() => _ImmichHtmlTextState(); -} - -class _ImmichHtmlTextState extends State { - final _recognizers = []; - dom.DocumentFragment _document = dom.DocumentFragment(); - - @override - void initState() { - super.initState(); - _document = html_parser.parseFragment(_preprocessHtml(widget.text)); - } - - @override - void didUpdateWidget(covariant ImmichHtmlText oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.text != widget.text) { - _document = html_parser.parseFragment(_preprocessHtml(widget.text)); - } - } - - /// `` tags are preprocessed to `` tags because `` is a - /// void element in HTML5 and cannot have children. The linkHandlers still use - /// 'link' as the key. - String _preprocessHtml(String html) { - return html - .replaceAllMapped( - RegExp(r'<(link)>(.*?)', caseSensitive: false), - (match) => '${match.group(2)}', - ) - .replaceAllMapped( - RegExp(r'<(link)\s*/>', caseSensitive: false), - (match) => '', - ); - } - - @override - void dispose() { - _disposeRecognizers(); - super.dispose(); - } - - void _disposeRecognizers() { - for (final recognizer in _recognizers) { - recognizer.dispose(); - } - _recognizers.clear(); - } - - List _buildSpans() { - _disposeRecognizers(); - - return _document.nodes.expand((node) => _buildNode(node, null, null)).toList(); - } - - Iterable _buildNode( - dom.Node node, - TextStyle? style, - _HtmlTag? parentTag, - ) sync* { - if (node is dom.Text) { - if (node.text.isEmpty) { - return; - } - - GestureRecognizer? recognizer; - if (parentTag?.type == _HtmlTagType.link) { - final handler = widget.linkHandlers?[parentTag?.tagName]; - if (handler != null) { - recognizer = TapGestureRecognizer()..onTap = handler; - _recognizers.add(recognizer); - } - } - - yield TextSpan(text: node.text, style: style, recognizer: recognizer); - } else if (node is dom.Element) { - final htmlTag = _HtmlTag.fromString(node); - final tagStyle = _styleForTag(htmlTag); - final mergedStyle = style?.merge(tagStyle) ?? tagStyle; - final newParentTag = htmlTag?.type == _HtmlTagType.link ? htmlTag : parentTag; - - for (final child in node.nodes) { - yield* _buildNode(child, mergedStyle, newParentTag); - } - } - } - - TextStyle? _styleForTag(_HtmlTag? tag) { - if (tag == null) { - return null; - } - - return switch (tag.type) { - _HtmlTagType.bold => const TextStyle(fontWeight: FontWeight.bold), - _HtmlTagType.link => widget.linkStyle ?? - TextStyle( - color: Theme.of(context).colorScheme.primary, - decoration: TextDecoration.underline, - ), - _HtmlTagType.unsupported => null, - }; - } - - @override - Widget build(BuildContext context) { - return Text.rich( - TextSpan(style: widget.style, children: _buildSpans()), - textAlign: widget.textAlign, - overflow: widget.overflow, - maxLines: widget.maxLines, - softWrap: widget.softWrap, - ); - } -} diff --git a/mobile/packages/ui/pubspec.lock b/mobile/packages/ui/pubspec.lock index c74422dd97..697e1debf5 100644 --- a/mobile/packages/ui/pubspec.lock +++ b/mobile/packages/ui/pubspec.lock @@ -41,14 +41,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" - csslib: - dependency: transitive - description: - name: csslib - sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.dev" - source: hosted - version: "1.0.2" fake_async: dependency: transitive description: @@ -67,14 +59,6 @@ packages: description: flutter source: sdk version: "0.0.0" - html: - dependency: "direct main" - description: - name: html - sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.dev" - source: hosted - version: "0.15.6" leak_tracker: dependency: transitive description: diff --git a/mobile/packages/ui/pubspec.yaml b/mobile/packages/ui/pubspec.yaml index d23f34f1a7..a25dfb6ca4 100644 --- a/mobile/packages/ui/pubspec.yaml +++ b/mobile/packages/ui/pubspec.yaml @@ -7,7 +7,6 @@ environment: dependencies: flutter: sdk: flutter - html: ^0.15.6 dev_dependencies: flutter_test: diff --git a/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_bold_text.dart b/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_bold_text.dart new file mode 100644 index 0000000000..7e36ac7537 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_bold_text.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; + +class FormattedTextBoldText extends StatelessWidget { + const FormattedTextBoldText({super.key}); + + @override + Widget build(BuildContext context) { + return ImmichFormattedText('This is bold text.'); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/examples/html_text_links.dart b/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_links.dart similarity index 53% rename from mobile/packages/ui/showcase/lib/pages/components/examples/html_text_links.dart rename to mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_links.dart index a764d7173e..3910a5117a 100644 --- a/mobile/packages/ui/showcase/lib/pages/components/examples/html_text_links.dart +++ b/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_links.dart @@ -1,25 +1,24 @@ import 'package:flutter/material.dart'; import 'package:immich_ui/immich_ui.dart'; -class HtmlTextLinks extends StatelessWidget { - const HtmlTextLinks({super.key}); +class FormattedTextLinks extends StatelessWidget { + const FormattedTextLinks({super.key}); @override Widget build(BuildContext context) { - return ImmichHtmlText( + return ImmichFormattedText( 'Read the documentation or visit GitHub.', - linkHandlers: { - 'docs-link': () { - ScaffoldMessenger.of( + spanBuilder: (tag) => FormattedSpan( + onTap: switch (tag) { + 'docs-link' => () => ScaffoldMessenger.of( context, - ).showSnackBar(const SnackBar(content: Text('Docs link clicked!'))); - }, - 'github-link': () { - ScaffoldMessenger.of( + ).showSnackBar(const SnackBar(content: Text('Docs link clicked!'))), + 'github-link' => () => ScaffoldMessenger.of( context, - ).showSnackBar(const SnackBar(content: Text('GitHub link clicked!'))); + ).showSnackBar(const SnackBar(content: Text('GitHub link clicked!'))), + _ => null, }, - }, + ), ); } } diff --git a/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_mixed_tags.dart b/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_mixed_tags.dart new file mode 100644 index 0000000000..3490b1c386 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_mixed_tags.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; + +class FormattedTextMixedContent extends StatelessWidget { + const FormattedTextMixedContent({super.key}); + + @override + Widget build(BuildContext context) { + return ImmichFormattedText( + 'You can use bold text and links together.', + spanBuilder: (tag) => switch (tag) { + 'b' => const FormattedSpan( + style: TextStyle(fontWeight: FontWeight.bold), + ), + _ => FormattedSpan( + onTap: () => ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Link clicked!'))), + ), + }, + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/examples/html_text_bold_text.dart b/mobile/packages/ui/showcase/lib/pages/components/examples/html_text_bold_text.dart deleted file mode 100644 index af4c87f40e..0000000000 --- a/mobile/packages/ui/showcase/lib/pages/components/examples/html_text_bold_text.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_ui/immich_ui.dart'; - -class HtmlTextBoldText extends StatelessWidget { - const HtmlTextBoldText({super.key}); - - @override - Widget build(BuildContext context) { - return ImmichHtmlText( - 'This is bold text and strong text.', - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/pages/components/examples/html_text_nested_tags.dart b/mobile/packages/ui/showcase/lib/pages/components/examples/html_text_nested_tags.dart deleted file mode 100644 index 836d949b66..0000000000 --- a/mobile/packages/ui/showcase/lib/pages/components/examples/html_text_nested_tags.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_ui/immich_ui.dart'; - -class HtmlTextNestedTags extends StatelessWidget { - const HtmlTextNestedTags({super.key}); - - @override - Widget build(BuildContext context) { - return ImmichHtmlText( - 'You can combine bold and links together.', - linkHandlers: { - 'link': () { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Nested link clicked!'))); - }, - }, - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/pages/components/formatted_text_page.dart b/mobile/packages/ui/showcase/lib/pages/components/formatted_text_page.dart new file mode 100644 index 0000000000..b827e0340b --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/formatted_text_page.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:showcase/pages/components/examples/formatted_text_bold_text.dart'; +import 'package:showcase/pages/components/examples/formatted_text_links.dart'; +import 'package:showcase/pages/components/examples/formatted_text_mixed_tags.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class FormattedTextPage extends StatelessWidget { + const FormattedTextPage({super.key}); + + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.formattedText.name, + child: ComponentExamples( + title: 'ImmichFormattedText', + subtitle: 'Render text with HTML formatting (bold, links).', + examples: [ + ExampleCard( + title: 'Bold Text', + preview: const FormattedTextBoldText(), + code: 'formatted_text_bold_text.dart', + ), + ExampleCard( + title: 'Links', + preview: const FormattedTextLinks(), + code: 'formatted_text_links.dart', + ), + ExampleCard( + title: 'Mixed Content', + preview: const FormattedTextMixedContent(), + code: 'formatted_text_mixed_tags.dart', + ), + ], + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/html_text_page.dart b/mobile/packages/ui/showcase/lib/pages/components/html_text_page.dart deleted file mode 100644 index 64dbc70597..0000000000 --- a/mobile/packages/ui/showcase/lib/pages/components/html_text_page.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:showcase/pages/components/examples/html_text_bold_text.dart'; -import 'package:showcase/pages/components/examples/html_text_links.dart'; -import 'package:showcase/pages/components/examples/html_text_nested_tags.dart'; -import 'package:showcase/routes.dart'; -import 'package:showcase/widgets/component_examples.dart'; -import 'package:showcase/widgets/example_card.dart'; -import 'package:showcase/widgets/page_title.dart'; - -class HtmlTextPage extends StatelessWidget { - const HtmlTextPage({super.key}); - - @override - Widget build(BuildContext context) { - return PageTitle( - title: AppRoute.htmlText.name, - child: ComponentExamples( - title: 'ImmichHtmlText', - subtitle: 'Render text with HTML formatting (bold, links).', - examples: [ - ExampleCard( - title: 'Bold Text', - preview: const HtmlTextBoldText(), - code: 'html_text_bold_text.dart', - ), - ExampleCard( - title: 'Links', - preview: const HtmlTextLinks(), - code: 'html_text_links.dart', - ), - ExampleCard( - title: 'Nested Tags', - preview: const HtmlTextNestedTags(), - code: 'html_text_nested_tags.dart', - ), - ], - ), - ); - } -} diff --git a/mobile/packages/ui/showcase/lib/router.dart b/mobile/packages/ui/showcase/lib/router.dart index 014de44fd8..34393da508 100644 --- a/mobile/packages/ui/showcase/lib/router.dart +++ b/mobile/packages/ui/showcase/lib/router.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:showcase/pages/components/close_button_page.dart'; import 'package:showcase/pages/components/form_page.dart'; -import 'package:showcase/pages/components/html_text_page.dart'; +import 'package:showcase/pages/components/formatted_text_page.dart'; import 'package:showcase/pages/components/icon_button_page.dart'; import 'package:showcase/pages/components/password_input_page.dart'; import 'package:showcase/pages/components/text_button_page.dart'; @@ -34,7 +34,7 @@ class AppRouter { AppRoute.textInput => const TextInputPage(), AppRoute.passwordInput => const PasswordInputPage(), AppRoute.form => const FormPage(), - AppRoute.htmlText => const HtmlTextPage(), + AppRoute.formattedText => const FormattedTextPage(), AppRoute.constants => const ConstantsPage(), }, ), diff --git a/mobile/packages/ui/showcase/lib/routes.dart b/mobile/packages/ui/showcase/lib/routes.dart index a39fb7bc34..4feeeafdb6 100644 --- a/mobile/packages/ui/showcase/lib/routes.dart +++ b/mobile/packages/ui/showcase/lib/routes.dart @@ -60,10 +60,10 @@ enum AppRoute { category: AppRouteCategory.forms, icon: Icons.description_outlined, ), - htmlText( - name: 'Html Text', + formattedText( + name: 'Formatted Text', description: 'Render text with HTML formatting', - path: '/html-text', + path: '/formatted-text', category: AppRouteCategory.forms, icon: Icons.code_rounded, ), diff --git a/mobile/packages/ui/showcase/pubspec.lock b/mobile/packages/ui/showcase/pubspec.lock index b0725051d3..c79e6c18c7 100644 --- a/mobile/packages/ui/showcase/pubspec.lock +++ b/mobile/packages/ui/showcase/pubspec.lock @@ -49,14 +49,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" - csslib: - dependency: transitive - description: - name: csslib - sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.dev" - source: hosted - version: "1.0.2" device_info_plus: dependency: transitive description: @@ -136,14 +128,6 @@ packages: url: "https://pub.dev" source: hosted version: "17.0.1" - html: - dependency: transitive - description: - name: html - sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.dev" - source: hosted - version: "0.15.6" immich_ui: dependency: "direct main" description: @@ -227,10 +211,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" path: dependency: transitive description: @@ -328,10 +312,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.6" typed_data: dependency: transitive description: diff --git a/mobile/packages/ui/test/html_test.dart b/mobile/packages/ui/test/formatted_text_test.dart similarity index 64% rename from mobile/packages/ui/test/html_test.dart rename to mobile/packages/ui/test/formatted_text_test.dart index 27f68ff66c..54ef343727 100644 --- a/mobile/packages/ui/test/html_test.dart +++ b/mobile/packages/ui/test/formatted_text_test.dart @@ -1,21 +1,16 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_ui/src/components/html_text.dart'; +import 'package:immich_ui/src/components/formatted_text.dart'; import 'test_utils.dart'; -/// Text.rich creates a nested structure: root -> wrapper -> actual children +/// Text.rich creates a nested structure: root (DefaultTextStyle) -> wrapper (ImmichFormattedText) -> actual children List _getContentSpans(WidgetTester tester) { final richText = tester.widget(find.byType(RichText)); final root = richText.text as TextSpan; - - if (root.children?.isNotEmpty ?? false) { - final wrapper = root.children!.first; - if (wrapper is TextSpan && wrapper.children != null) { - return wrapper.children!; - } - } + final wrapper = root.children?.firstOrNull; + if (wrapper is TextSpan) return wrapper.children ?? []; return []; } @@ -38,42 +33,18 @@ void _triggerTap(TextSpan span) { } void main() { - group('ImmichHtmlText', () { + group('ImmichFormattedText', () { testWidgets('renders plain text without HTML tags', (tester) async { await tester.pumpTestWidget( - const ImmichHtmlText('This is plain text'), + const ImmichFormattedText('This is plain text'), ); expect(find.text('This is plain text'), findsOneWidget); }); - testWidgets('handles mixed content with bold and links', (tester) async { - await tester.pumpTestWidget( - ImmichHtmlText( - 'This is an example of HTML text with bold.', - linkHandlers: {'link': () {}}, - ), - ); - - final spans = _getContentSpans(tester); - - final exampleSpan = _findSpan(spans, 'example'); - expect(exampleSpan.style?.fontWeight, FontWeight.bold); - - final boldSpan = _findSpan(spans, 'bold'); - expect(boldSpan.style?.fontWeight, FontWeight.bold); - - final linkSpan = _findSpan(spans, 'HTML text'); - expect(linkSpan.style?.decoration, TextDecoration.underline); - expect(linkSpan.style?.fontWeight, FontWeight.bold); - expect(linkSpan.recognizer, isA()); - - expect(_concatenateText(spans), 'This is an example of HTML text with bold.'); - }); - testWidgets('applies text style properties', (tester) async { await tester.pumpTestWidget( - const ImmichHtmlText( + const ImmichFormattedText( 'Test text', style: TextStyle( fontSize: 16, @@ -97,7 +68,7 @@ void main() { testWidgets('handles text with special characters', (tester) async { await tester.pumpTestWidget( - const ImmichHtmlText('Text with & < > " \' characters'), + const ImmichFormattedText('Text with & < > " \' characters'), ); expect(find.byType(RichText), findsOneWidget); @@ -109,7 +80,7 @@ void main() { group('bold', () { testWidgets('renders bold text with tag', (tester) async { await tester.pumpTestWidget( - const ImmichHtmlText('This is bold text'), + const ImmichFormattedText('This is bold text'), ); final spans = _getContentSpans(tester); @@ -118,41 +89,14 @@ void main() { expect(boldSpan.style?.fontWeight, FontWeight.bold); expect(_concatenateText(spans), 'This is bold text'); }); - - testWidgets('renders bold text with tag', (tester) async { - await tester.pumpTestWidget( - const ImmichHtmlText('This is strong text'), - ); - - final spans = _getContentSpans(tester); - final strongSpan = _findSpan(spans, 'strong'); - - expect(strongSpan.style?.fontWeight, FontWeight.bold); - }); - - testWidgets('handles nested bold tags', (tester) async { - await tester.pumpTestWidget( - const ImmichHtmlText('Text with bold and nested'), - ); - - final spans = _getContentSpans(tester); - - final nestedSpan = _findSpan(spans, 'nested'); - expect(nestedSpan.style?.fontWeight, FontWeight.bold); - - final boldSpan = _findSpan(spans, 'bold and '); - expect(boldSpan.style?.fontWeight, FontWeight.bold); - - expect(_concatenateText(spans), 'Text with bold and nested'); - }); }); group('link', () { testWidgets('renders link text with tag', (tester) async { await tester.pumpTestWidget( - ImmichHtmlText( + ImmichFormattedText( 'This is a custom link text', - linkHandlers: {'link': () {}}, + spanBuilder: (tag) => FormattedSpan(onTap: switch (tag) { 'link' => () {}, _ => null }), ), ); @@ -167,9 +111,9 @@ void main() { var linkTapped = false; await tester.pumpTestWidget( - ImmichHtmlText( + ImmichFormattedText( 'Tap here', - linkHandlers: {'link': () => linkTapped = true}, + spanBuilder: (tag) => FormattedSpan(onTap: switch (tag) { 'link' => () => linkTapped = true, _ => null }), ), ); @@ -183,12 +127,13 @@ void main() { testWidgets('handles custom prefixed link tags', (tester) async { await tester.pumpTestWidget( - ImmichHtmlText( + ImmichFormattedText( 'Refer to docs and other', - linkHandlers: { - 'docs-link': () {}, - 'other-link': () {}, - }, + spanBuilder: (tag) => FormattedSpan(onTap: switch (tag) { + 'docs-link' => () {}, + 'other-link' => () {}, + _ => null, + },), ), ); @@ -207,10 +152,9 @@ void main() { ); await tester.pumpTestWidget( - ImmichHtmlText( + ImmichFormattedText( 'Click here', - linkStyle: customLinkStyle, - linkHandlers: {'link': () {}}, + spanBuilder: (tag) => FormattedSpan(style: customLinkStyle, onTap: () {}), ), ); @@ -223,9 +167,9 @@ void main() { testWidgets('link without handler renders but is not tappable', (tester) async { await tester.pumpTestWidget( - ImmichHtmlText( + ImmichFormattedText( 'Link without handler: click me', - linkHandlers: {'other-link': () {}}, + spanBuilder: (tag) => FormattedSpan(onTap: switch (tag) { 'other-link' => () {}, _ => null }), ), ); @@ -241,12 +185,13 @@ void main() { var secondLinkTapped = false; await tester.pumpTestWidget( - ImmichHtmlText( + ImmichFormattedText( 'Go to docs or help', - linkHandlers: { - 'docs-link': () => firstLinkTapped = true, - 'help-link': () => secondLinkTapped = true, - }, + spanBuilder: (tag) => FormattedSpan(onTap: switch (tag) { + 'docs-link' => () => firstLinkTapped = true, + 'help-link' => () => secondLinkTapped = true, + _ => null, + },), ), ); diff --git a/mobile/test/domain/services/album.service_test.dart b/mobile/test/domain/services/album.service_test.dart index 1a36a811c3..9110a09471 100644 --- a/mobile/test/domain/services/album.service_test.dart +++ b/mobile/test/domain/services/album.service_test.dart @@ -1,4 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; @@ -13,38 +14,6 @@ void main() { late DriftRemoteAlbumRepository mockRemoteAlbumRepo; late DriftAlbumApiRepository mockAlbumApiRepo; - setUp(() { - mockRemoteAlbumRepo = MockRemoteAlbumRepository(); - mockAlbumApiRepo = MockDriftAlbumApiRepository(); - sut = RemoteAlbumService(mockRemoteAlbumRepo, mockAlbumApiRepo); - - when(() => mockRemoteAlbumRepo.getNewestAssetTimestamp(any())).thenAnswer((invocation) { - // Simulate a timestamp for the newest asset in the album - final albumID = invocation.positionalArguments[0] as String; - - if (albumID == '1') { - return Future.value(DateTime(2023, 1, 1)); - } else if (albumID == '2') { - return Future.value(DateTime(2023, 2, 1)); - } - - return Future.value(DateTime.fromMillisecondsSinceEpoch(0)); - }); - - when(() => mockRemoteAlbumRepo.getOldestAssetTimestamp(any())).thenAnswer((invocation) { - // Simulate a timestamp for the oldest asset in the album - final albumID = invocation.positionalArguments[0] as String; - - if (albumID == '1') { - return Future.value(DateTime(2019, 1, 1)); - } else if (albumID == '2') { - return Future.value(DateTime(2019, 2, 1)); - } - - return Future.value(DateTime.fromMillisecondsSinceEpoch(0)); - }); - }); - final albumA = RemoteAlbum( id: '1', name: 'Album A', @@ -73,6 +42,21 @@ void main() { isShared: false, ); + setUp(() { + mockRemoteAlbumRepo = MockRemoteAlbumRepository(); + mockAlbumApiRepo = MockDriftAlbumApiRepository(); + + when( + () => mockRemoteAlbumRepo.getSortedAlbumIds(any(), aggregation: AssetDateAggregation.end), + ).thenAnswer((_) async => ['1', '2']); + + when( + () => mockRemoteAlbumRepo.getSortedAlbumIds(any(), aggregation: AssetDateAggregation.start), + ).thenAnswer((_) async => ['1', '2']); + + sut = RemoteAlbumService(mockRemoteAlbumRepo, mockAlbumApiRepo); + }); + group('sortAlbums', () { test('should sort correctly based on name', () async { final albums = [albumB, albumA]; diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index 0eabf3b612..a182c6cdca 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -18,6 +18,7 @@ import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.da import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; +import 'package:immich_mobile/utils/semver.dart'; import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; @@ -66,6 +67,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); debugDefaultTargetPlatformOverride = TargetPlatform.android; registerFallbackValue(LocalAssetStub.image1); + registerFallbackValue(const SemVer(major: 2, minor: 5, patch: 0)); db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); await StoreService.init(storeRepository: DriftStoreRepository(db)); @@ -94,11 +96,19 @@ void main() { when(() => mockAbortCallbackWrapper()).thenReturn(false); - when(() => mockSyncApiRepo.streamChanges(any())).thenAnswer((invocation) async { + when(() => mockSyncApiRepo.streamChanges(any(), serverVersion: any(named: 'serverVersion'))).thenAnswer(( + invocation, + ) async { handleEventsCallback = invocation.positionalArguments.first; }); - when(() => mockSyncApiRepo.streamChanges(any(), onReset: any(named: 'onReset'))).thenAnswer((invocation) async { + when( + () => mockSyncApiRepo.streamChanges( + any(), + onReset: any(named: 'onReset'), + serverVersion: any(named: 'serverVersion'), + ), + ).thenAnswer((invocation) async { handleEventsCallback = invocation.positionalArguments.first; }); @@ -106,9 +116,9 @@ void main() { when(() => mockSyncApiRepo.deleteSyncAck(any())).thenAnswer((_) async => {}); when(() => mockApi.serverInfoApi).thenReturn(mockServerApi); - when(() => mockServerApi.getServerVersion()).thenAnswer( - (_) async => ServerVersionResponseDto(major: 1, minor: 132, patch_: 0), - ); + when( + () => mockServerApi.getServerVersion(), + ).thenAnswer((_) async => ServerVersionResponseDto(major: 1, minor: 132, patch_: 0)); when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer(successHandler); when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer(successHandler); diff --git a/mobile/test/drift/main/generated/schema.dart b/mobile/test/drift/main/generated/schema.dart index d9f18b3007..2ec39fafde 100644 --- a/mobile/test/drift/main/generated/schema.dart +++ b/mobile/test/drift/main/generated/schema.dart @@ -22,6 +22,7 @@ import 'schema_v16.dart' as v16; import 'schema_v17.dart' as v17; import 'schema_v18.dart' as v18; import 'schema_v19.dart' as v19; +import 'schema_v20.dart' as v20; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -65,6 +66,8 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v18.DatabaseAtV18(db); case 19: return v19.DatabaseAtV19(db); + case 20: + return v20.DatabaseAtV20(db); default: throw MissingSchemaException(version, versions); } @@ -90,5 +93,6 @@ class GeneratedHelper implements SchemaInstantiationHelper { 17, 18, 19, + 20, ]; } diff --git a/mobile/test/drift/main/generated/schema_v20.dart b/mobile/test/drift/main/generated/schema_v20.dart new file mode 100644 index 0000000000..8f7b204f7a --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v20.dart @@ -0,0 +1,8471 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String email; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + const UserEntityData({ + required this.id, + required this.name, + required this.email, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + return map; + } + + factory UserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + }; + } + + UserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + }) => UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + }); + } + + UserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + }) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn localDateTime = + GeneratedColumn( + 'local_date_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn libraryId = GeneratedColumn( + 'library_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isEdited = GeneratedColumn( + 'is_edited', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_edited" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}local_date_time'], + ), + thumbHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_hash'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + stackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stack_id'], + ), + libraryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}library_id'], + ), + isEdited: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_edited'], + )!, + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + final String? libraryId; + final bool isEdited; + const RemoteAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId, + this.libraryId, + required this.isEdited, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + if (!nullToAbsent || libraryId != null) { + map['library_id'] = Variable(libraryId); + } + map['is_edited'] = Variable(isEdited); + return map; + } + + factory RemoteAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + libraryId: serializer.fromJson(json['libraryId']), + isEdited: serializer.fromJson(json['isEdited']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + 'libraryId': serializer.toJson(libraryId), + 'isEdited': serializer.toJson(isEdited), + }; + } + + RemoteAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent(), + Value libraryId = const Value.absent(), + bool? isEdited, + }) => RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime.present + ? localDateTime.value + : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + libraryId: libraryId.present ? libraryId.value : this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: data.visibility.present + ? data.visibility.value + : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId && + other.libraryId == this.libraryId && + other.isEdited == this.isEdited); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + final Value libraryId; + final Value isEdited; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + Expression? libraryId, + Expression? isEdited, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + if (libraryId != null) 'library_id': libraryId, + if (isEdited != null) 'is_edited': isEdited, + }); + } + + RemoteAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId, + Value? libraryId, + Value? isEdited, + }) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + libraryId: libraryId ?? this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + if (libraryId.present) { + map['library_id'] = Variable(libraryId.value); + } + if (isEdited.present) { + map['is_edited'] = Variable(isEdited.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn primaryAssetId = GeneratedColumn( + 'primary_asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + primaryAssetId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + primaryAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}primary_asset_id'], + )!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId, + }) => StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present + ? data.primaryAssetId.value + : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId, + }) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn iCloudId = GeneratedColumn( + 'i_cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}orientation'], + )!, + iCloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}i_cloud_id'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @override + LocalAssetEntity createAlias(String alias) { + return LocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + final int orientation; + final String? iCloudId; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + const LocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite, + required this.orientation, + this.iCloudId, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + if (!nullToAbsent || iCloudId != null) { + map['i_cloud_id'] = Variable(iCloudId); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + return map; + } + + factory LocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + iCloudId: serializer.fromJson(json['iCloudId']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'iCloudId': serializer.toJson(iCloudId), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + LocalAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + Value iCloudId = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + iCloudId: iCloudId.present ? iCloudId.value : this.iCloudId, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + LocalAssetEntityData copyWithCompanion(LocalAssetEntityCompanion data) { + return LocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + iCloudId: data.iCloudId.present ? data.iCloudId.value : this.iCloudId, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.iCloudId == this.iCloudId && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +class LocalAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value iCloudId; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + const LocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? iCloudId, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (iCloudId != null) 'i_cloud_id': iCloudId, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + LocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? iCloudId, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + return LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + iCloudId: iCloudId ?? this.iCloudId, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (iCloudId.present) { + map['i_cloud_id'] = Variable(iCloudId.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn order = GeneratedColumn( + 'order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumbnail_asset_id'], + ), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_activity_enabled'], + )!, + order: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order'], + )!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith({ + String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order, + }) => RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order, + }) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn linkedRemoteAlbumId = + GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + backupSelection: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}backup_selection'], + )!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_ios_shared_album'], + )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final String? linkedRemoteAlbumId; + final bool? marker_; + const LocalAlbumEntityData({ + required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = Variable(linkedRemoteAlbumId); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value linkedRemoteAlbumId = const Value.absent(), + Value marker_ = const Value.absent(), + }) => LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value linkedRemoteAlbumId; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? linkedRemoteAlbumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? linkedRemoteAlbumId, + Value? marker_, + }) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = Variable( + linkedRemoteAlbumId.value, + ); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [assetId, albumId, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + final bool? marker_; + const LocalAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumAssetEntityData copyWith({ + String? assetId, + String? albumId, + Value marker_ = const Value.absent(), + }) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data, + ) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId && + other.marker_ == this.marker_); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + final Value marker_; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + this.marker_ = const Value.absent(), + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + Value? marker_, + }) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class AuthUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_admin" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinCode = GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'auth_user_entity'; + @override + Set get $primaryKey => {id}; + @override + AuthUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthUserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_admin'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_size_in_bytes'], + )!, + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_usage_in_bytes'], + )!, + pinCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pin_code'], + ), + ); + } + + @override + AuthUserEntity createAlias(String alias) { + return AuthUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AuthUserEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String email; + final bool isAdmin; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + final int quotaSizeInBytes; + final int quotaUsageInBytes; + final String? pinCode; + const AuthUserEntityData({ + required this.id, + required this.name, + required this.email, + required this.isAdmin, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + this.pinCode, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['is_admin'] = Variable(isAdmin); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + if (!nullToAbsent || pinCode != null) { + map['pin_code'] = Variable(pinCode); + } + return map; + } + + factory AuthUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthUserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + isAdmin: serializer.fromJson(json['isAdmin']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + pinCode: serializer.fromJson(json['pinCode']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'isAdmin': serializer.toJson(isAdmin), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + 'pinCode': serializer.toJson(pinCode), + }; + } + + AuthUserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? isAdmin, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + int? quotaSizeInBytes, + int? quotaUsageInBytes, + Value pinCode = const Value.absent(), + }) => AuthUserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode.present ? pinCode.value : this.pinCode, + ); + AuthUserEntityData copyWithCompanion(AuthUserEntityCompanion data) { + return AuthUserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode, + ); + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthUserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.isAdmin == this.isAdmin && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes && + other.pinCode == this.pinCode); +} + +class AuthUserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value isAdmin; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + final Value pinCode; + const AuthUserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }); + AuthUserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + required int avatarColor, + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email), + avatarColor = Value(avatarColor); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? isAdmin, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + Expression? pinCode, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (isAdmin != null) 'is_admin': isAdmin, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + if (pinCode != null) 'pin_code': pinCode, + }); + } + + AuthUserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? isAdmin, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes, + Value? pinCode, + }) { + return AuthUserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode ?? this.pinCode, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + if (pinCode.present) { + map['pin_code'] = Variable(pinCode.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + ); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({ + required this.userId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({ + String? userId, + int? key, + Uint8List? value, + }) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({ + Value? userId, + Value? key, + Value? value, + }) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("in_timeline" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_by_id'], + )!, + sharedWithId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_with_id'], + )!, + inTimeline: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}in_timeline'], + )!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({ + required this.sharedById, + required this.sharedWithId, + required this.inTimeline, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({ + String? sharedById, + String? sharedWithId, + bool? inTimeline, + }) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present + ? data.sharedById.value + : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: data.inTimeline.present + ? data.inTimeline.value + : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({ + Value? sharedById, + Value? sharedWithId, + Value? inTimeline, + }) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn city = GeneratedColumn( + 'city', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn( + 'date_time_original', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn make = GeneratedColumn( + 'make', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn model = GeneratedColumn( + 'model', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + city: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}city'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + exposureTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exposure_time'], + ), + fNumber: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}f_number'], + ), + fileSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}file_size'], + ), + focalLength: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}focal_length'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + iso: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}iso'], + ), + make: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}make'], + ), + model: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}model'], + ), + lens: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}lens'], + ), + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}orientation'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + ), + projectionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_type'], + ), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData({ + required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: serializer.fromJson( + json['dateTimeOriginal'], + ), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith({ + String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent(), + }) => RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: projectionType.present + ? projectionType.value + : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: data.description.present + ? data.description.value + : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: data.focalLength.present + ? data.focalLength.value + : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith({ + Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType, + }) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data, + ) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + role: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role'], + )!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({ + required this.albumId, + required this.userId, + required this.role, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({ + String? albumId, + String? userId, + int? role, + }) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data, + ) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({ + Value? albumId, + Value? userId, + Value? role, + }) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class RemoteAssetCloudIdEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetCloudIdEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn cloudId = GeneratedColumn( + 'cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_cloud_id_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteAssetCloudIdEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetCloudIdEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + cloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cloud_id'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @override + RemoteAssetCloudIdEntity createAlias(String alias) { + return RemoteAssetCloudIdEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetCloudIdEntityData extends DataClass + implements Insertable { + final String assetId; + final String? cloudId; + final DateTime? createdAt; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + const RemoteAssetCloudIdEntityData({ + required this.assetId, + this.cloudId, + this.createdAt, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || cloudId != null) { + map['cloud_id'] = Variable(cloudId); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + return map; + } + + factory RemoteAssetCloudIdEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetCloudIdEntityData( + assetId: serializer.fromJson(json['assetId']), + cloudId: serializer.fromJson(json['cloudId']), + createdAt: serializer.fromJson(json['createdAt']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'cloudId': serializer.toJson(cloudId), + 'createdAt': serializer.toJson(createdAt), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + RemoteAssetCloudIdEntityData copyWith({ + String? assetId, + Value cloudId = const Value.absent(), + Value createdAt = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => RemoteAssetCloudIdEntityData( + assetId: assetId ?? this.assetId, + cloudId: cloudId.present ? cloudId.value : this.cloudId, + createdAt: createdAt.present ? createdAt.value : this.createdAt, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + RemoteAssetCloudIdEntityData copyWithCompanion( + RemoteAssetCloudIdEntityCompanion data, + ) { + return RemoteAssetCloudIdEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + cloudId: data.cloudId.present ? data.cloudId.value : this.cloudId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityData(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetCloudIdEntityData && + other.assetId == this.assetId && + other.cloudId == this.cloudId && + other.createdAt == this.createdAt && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +class RemoteAssetCloudIdEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value cloudId; + final Value createdAt; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + const RemoteAssetCloudIdEntityCompanion({ + this.assetId = const Value.absent(), + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }); + RemoteAssetCloudIdEntityCompanion.insert({ + required String assetId, + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? cloudId, + Expression? createdAt, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (cloudId != null) 'cloud_id': cloudId, + if (createdAt != null) 'created_at': createdAt, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + RemoteAssetCloudIdEntityCompanion copyWith({ + Value? assetId, + Value? cloudId, + Value? createdAt, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + return RemoteAssetCloudIdEntityCompanion( + assetId: assetId ?? this.assetId, + cloudId: cloudId ?? this.cloudId, + createdAt: createdAt ?? this.createdAt, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (cloudId.present) { + map['cloud_id'] = Variable(cloudId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_saved" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + isSaved: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_saved'], + )!, + memoryAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}memory_at'], + )!, + seenAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}seen_at'], + ), + showAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}show_at'], + ), + hideAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}hide_at'], + ), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent(), + }) => MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt, + }) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + memoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}memory_id'], + )!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({ + Value? assetId, + Value? memoryId, + }) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_hidden" IN (0, 1))', + ), + ); + late final GeneratedColumn color = GeneratedColumn( + 'color', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + faceAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}face_asset_id'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_hidden'], + )!, + color: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color'], + ), + birthDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}birth_date'], + ), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent(), + }) => PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present + ? data.faceAssetId.value + : this.faceAssetId, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate, + }) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class AssetFaceEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetFaceEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn personId = GeneratedColumn( + 'person_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES person_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn imageWidth = GeneratedColumn( + 'image_width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn imageHeight = GeneratedColumn( + 'image_height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX1 = GeneratedColumn( + 'bounding_box_x1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY1 = GeneratedColumn( + 'bounding_box_y1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX2 = GeneratedColumn( + 'bounding_box_x2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY2 = GeneratedColumn( + 'bounding_box_y2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isVisible = GeneratedColumn( + 'is_visible', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_visible" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + isVisible, + deletedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_face_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetFaceEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetFaceEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + personId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}person_id'], + ), + imageWidth: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_width'], + )!, + imageHeight: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_height'], + )!, + boundingBoxX1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x1'], + )!, + boundingBoxY1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y1'], + )!, + boundingBoxX2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x2'], + )!, + boundingBoxY2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y2'], + )!, + sourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_type'], + )!, + isVisible: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_visible'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @override + AssetFaceEntity createAlias(String alias) { + return AssetFaceEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AssetFaceEntityData extends DataClass + implements Insertable { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + final bool isVisible; + final DateTime? deletedAt; + const AssetFaceEntityData({ + required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType, + required this.isVisible, + this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || personId != null) { + map['person_id'] = Variable(personId); + } + map['image_width'] = Variable(imageWidth); + map['image_height'] = Variable(imageHeight); + map['bounding_box_x1'] = Variable(boundingBoxX1); + map['bounding_box_y1'] = Variable(boundingBoxY1); + map['bounding_box_x2'] = Variable(boundingBoxX2); + map['bounding_box_y2'] = Variable(boundingBoxY2); + map['source_type'] = Variable(sourceType); + map['is_visible'] = Variable(isVisible); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + return map; + } + + factory AssetFaceEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetFaceEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + personId: serializer.fromJson(json['personId']), + imageWidth: serializer.fromJson(json['imageWidth']), + imageHeight: serializer.fromJson(json['imageHeight']), + boundingBoxX1: serializer.fromJson(json['boundingBoxX1']), + boundingBoxY1: serializer.fromJson(json['boundingBoxY1']), + boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), + boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), + sourceType: serializer.fromJson(json['sourceType']), + isVisible: serializer.fromJson(json['isVisible']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'personId': serializer.toJson(personId), + 'imageWidth': serializer.toJson(imageWidth), + 'imageHeight': serializer.toJson(imageHeight), + 'boundingBoxX1': serializer.toJson(boundingBoxX1), + 'boundingBoxY1': serializer.toJson(boundingBoxY1), + 'boundingBoxX2': serializer.toJson(boundingBoxX2), + 'boundingBoxY2': serializer.toJson(boundingBoxY2), + 'sourceType': serializer.toJson(sourceType), + 'isVisible': serializer.toJson(isVisible), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + AssetFaceEntityData copyWith({ + String? id, + String? assetId, + Value personId = const Value.absent(), + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType, + bool? isVisible, + Value deletedAt = const Value.absent(), + }) => AssetFaceEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId.present ? personId.value : this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + isVisible: isVisible ?? this.isVisible, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + AssetFaceEntityData copyWithCompanion(AssetFaceEntityCompanion data) { + return AssetFaceEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + personId: data.personId.present ? data.personId.value : this.personId, + imageWidth: data.imageWidth.present + ? data.imageWidth.value + : this.imageWidth, + imageHeight: data.imageHeight.present + ? data.imageHeight.value + : this.imageHeight, + boundingBoxX1: data.boundingBoxX1.present + ? data.boundingBoxX1.value + : this.boundingBoxX1, + boundingBoxY1: data.boundingBoxY1.present + ? data.boundingBoxY1.value + : this.boundingBoxY1, + boundingBoxX2: data.boundingBoxX2.present + ? data.boundingBoxX2.value + : this.boundingBoxX2, + boundingBoxY2: data.boundingBoxY2.present + ? data.boundingBoxY2.value + : this.boundingBoxY2, + sourceType: data.sourceType.present + ? data.sourceType.value + : this.sourceType, + isVisible: data.isVisible.present ? data.isVisible.value : this.isVisible, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType, ') + ..write('isVisible: $isVisible, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + isVisible, + deletedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetFaceEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.personId == this.personId && + other.imageWidth == this.imageWidth && + other.imageHeight == this.imageHeight && + other.boundingBoxX1 == this.boundingBoxX1 && + other.boundingBoxY1 == this.boundingBoxY1 && + other.boundingBoxX2 == this.boundingBoxX2 && + other.boundingBoxY2 == this.boundingBoxY2 && + other.sourceType == this.sourceType && + other.isVisible == this.isVisible && + other.deletedAt == this.deletedAt); +} + +class AssetFaceEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value personId; + final Value imageWidth; + final Value imageHeight; + final Value boundingBoxX1; + final Value boundingBoxY1; + final Value boundingBoxX2; + final Value boundingBoxY2; + final Value sourceType; + final Value isVisible; + final Value deletedAt; + const AssetFaceEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.personId = const Value.absent(), + this.imageWidth = const Value.absent(), + this.imageHeight = const Value.absent(), + this.boundingBoxX1 = const Value.absent(), + this.boundingBoxY1 = const Value.absent(), + this.boundingBoxX2 = const Value.absent(), + this.boundingBoxY2 = const Value.absent(), + this.sourceType = const Value.absent(), + this.isVisible = const Value.absent(), + this.deletedAt = const Value.absent(), + }); + AssetFaceEntityCompanion.insert({ + required String id, + required String assetId, + this.personId = const Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + this.isVisible = const Value.absent(), + this.deletedAt = const Value.absent(), + }) : id = Value(id), + assetId = Value(assetId), + imageWidth = Value(imageWidth), + imageHeight = Value(imageHeight), + boundingBoxX1 = Value(boundingBoxX1), + boundingBoxY1 = Value(boundingBoxY1), + boundingBoxX2 = Value(boundingBoxX2), + boundingBoxY2 = Value(boundingBoxY2), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? personId, + Expression? imageWidth, + Expression? imageHeight, + Expression? boundingBoxX1, + Expression? boundingBoxY1, + Expression? boundingBoxX2, + Expression? boundingBoxY2, + Expression? sourceType, + Expression? isVisible, + Expression? deletedAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (personId != null) 'person_id': personId, + if (imageWidth != null) 'image_width': imageWidth, + if (imageHeight != null) 'image_height': imageHeight, + if (boundingBoxX1 != null) 'bounding_box_x1': boundingBoxX1, + if (boundingBoxY1 != null) 'bounding_box_y1': boundingBoxY1, + if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, + if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, + if (sourceType != null) 'source_type': sourceType, + if (isVisible != null) 'is_visible': isVisible, + if (deletedAt != null) 'deleted_at': deletedAt, + }); + } + + AssetFaceEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType, + Value? isVisible, + Value? deletedAt, + }) { + return AssetFaceEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + isVisible: isVisible ?? this.isVisible, + deletedAt: deletedAt ?? this.deletedAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (personId.present) { + map['person_id'] = Variable(personId.value); + } + if (imageWidth.present) { + map['image_width'] = Variable(imageWidth.value); + } + if (imageHeight.present) { + map['image_height'] = Variable(imageHeight.value); + } + if (boundingBoxX1.present) { + map['bounding_box_x1'] = Variable(boundingBoxX1.value); + } + if (boundingBoxY1.present) { + map['bounding_box_y1'] = Variable(boundingBoxY1.value); + } + if (boundingBoxX2.present) { + map['bounding_box_x2'] = Variable(boundingBoxX2.value); + } + if (boundingBoxY2.present) { + map['bounding_box_y2'] = Variable(boundingBoxY2.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + if (isVisible.present) { + map['is_visible'] = Variable(isVisible.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType, ') + ..write('isVisible: $isVisible, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } +} + +class StoreEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StoreEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stringValue = GeneratedColumn( + 'string_value', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn intValue = GeneratedColumn( + 'int_value', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + List get $columns => [id, stringValue, intValue]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'store_entity'; + @override + Set get $primaryKey => {id}; + @override + StoreEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StoreEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + stringValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}string_value'], + ), + intValue: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}int_value'], + ), + ); + } + + @override + StoreEntity createAlias(String alias) { + return StoreEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StoreEntityData extends DataClass implements Insertable { + final int id; + final String? stringValue; + final int? intValue; + const StoreEntityData({required this.id, this.stringValue, this.intValue}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || stringValue != null) { + map['string_value'] = Variable(stringValue); + } + if (!nullToAbsent || intValue != null) { + map['int_value'] = Variable(intValue); + } + return map; + } + + factory StoreEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StoreEntityData( + id: serializer.fromJson(json['id']), + stringValue: serializer.fromJson(json['stringValue']), + intValue: serializer.fromJson(json['intValue']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'stringValue': serializer.toJson(stringValue), + 'intValue': serializer.toJson(intValue), + }; + } + + StoreEntityData copyWith({ + int? id, + Value stringValue = const Value.absent(), + Value intValue = const Value.absent(), + }) => StoreEntityData( + id: id ?? this.id, + stringValue: stringValue.present ? stringValue.value : this.stringValue, + intValue: intValue.present ? intValue.value : this.intValue, + ); + StoreEntityData copyWithCompanion(StoreEntityCompanion data) { + return StoreEntityData( + id: data.id.present ? data.id.value : this.id, + stringValue: data.stringValue.present + ? data.stringValue.value + : this.stringValue, + intValue: data.intValue.present ? data.intValue.value : this.intValue, + ); + } + + @override + String toString() { + return (StringBuffer('StoreEntityData(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, stringValue, intValue); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StoreEntityData && + other.id == this.id && + other.stringValue == this.stringValue && + other.intValue == this.intValue); +} + +class StoreEntityCompanion extends UpdateCompanion { + final Value id; + final Value stringValue; + final Value intValue; + const StoreEntityCompanion({ + this.id = const Value.absent(), + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }); + StoreEntityCompanion.insert({ + required int id, + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? stringValue, + Expression? intValue, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (stringValue != null) 'string_value': stringValue, + if (intValue != null) 'int_value': intValue, + }); + } + + StoreEntityCompanion copyWith({ + Value? id, + Value? stringValue, + Value? intValue, + }) { + return StoreEntityCompanion( + id: id ?? this.id, + stringValue: stringValue ?? this.stringValue, + intValue: intValue ?? this.intValue, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (stringValue.present) { + map['string_value'] = Variable(stringValue.value); + } + if (intValue.present) { + map['int_value'] = Variable(intValue.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StoreEntityCompanion(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } +} + +class TrashedLocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TrashedLocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn source = GeneratedColumn( + 'source', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'trashed_local_asset_entity'; + @override + Set get $primaryKey => {id, albumId}; + @override + TrashedLocalAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TrashedLocalAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}orientation'], + )!, + source: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}source'], + )!, + ); + } + + @override + TrashedLocalAssetEntity createAlias(String alias) { + return TrashedLocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class TrashedLocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String albumId; + final String? checksum; + final bool isFavorite; + final int orientation; + final int source; + const TrashedLocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.albumId, + this.checksum, + required this.isFavorite, + required this.orientation, + required this.source, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + map['source'] = Variable(source); + return map; + } + + factory TrashedLocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TrashedLocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + albumId: serializer.fromJson(json['albumId']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + source: serializer.fromJson(json['source']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'albumId': serializer.toJson(albumId), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'source': serializer.toJson(source), + }; + } + + TrashedLocalAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? albumId, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + int? source, + }) => TrashedLocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + albumId: albumId ?? this.albumId, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + TrashedLocalAssetEntityData copyWithCompanion( + TrashedLocalAssetEntityCompanion data, + ) { + return TrashedLocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + source: data.source.present ? data.source.value : this.source, + ); + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TrashedLocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.albumId == this.albumId && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.source == this.source); +} + +class TrashedLocalAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value albumId; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value source; + const TrashedLocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.albumId = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.source = const Value.absent(), + }); + TrashedLocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String albumId, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + required int source, + }) : name = Value(name), + type = Value(type), + id = Value(id), + albumId = Value(albumId), + source = Value(source); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? albumId, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? source, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (albumId != null) 'album_id': albumId, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (source != null) 'source': source, + }); + } + + TrashedLocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? albumId, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? source, + }) { + return TrashedLocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + albumId: albumId ?? this.albumId, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (source.present) { + map['source'] = Variable(source.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV20 extends GeneratedDatabase { + DatabaseAtV20(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final Index idxLocalAlbumAssetAlbumAsset = Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', + ); + late final Index idxRemoteAlbumOwnerId = Index( + 'idx_remote_album_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)', + ); + late final Index idxLocalAssetChecksum = Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + late final Index idxLocalAssetCloudId = Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + late final Index idxStackPrimaryAssetId = Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', + ); + late final Index idxRemoteAssetOwnerChecksum = Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + late final Index uQRemoteAssetsOwnerChecksum = Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + late final Index uQRemoteAssetsOwnerLibraryChecksum = Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + late final Index idxRemoteAssetChecksum = Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final Index idxRemoteAssetStackId = Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', + ); + late final Index idxRemoteAssetLocalDateTimeDay = Index( + 'idx_remote_asset_local_date_time_day', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))', + ); + late final Index idxRemoteAssetLocalDateTimeMonth = Index( + 'idx_remote_asset_local_date_time_month', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))', + ); + late final AuthUserEntity authUserEntity = AuthUserEntity(this); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final RemoteAssetCloudIdEntity remoteAssetCloudIdEntity = + RemoteAssetCloudIdEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + late final StoreEntity storeEntity = StoreEntity(this); + late final TrashedLocalAssetEntity trashedLocalAssetEntity = + TrashedLocalAssetEntity(this); + late final Index idxPartnerSharedWithId = Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', + ); + late final Index idxLatLng = Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + late final Index idxRemoteAlbumAssetAlbumAsset = Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', + ); + late final Index idxRemoteAssetCloudId = Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + late final Index idxPersonOwnerId = Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', + ); + late final Index idxAssetFacePersonId = Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', + ); + late final Index idxAssetFaceAssetId = Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', + ); + late final Index idxTrashedLocalAssetChecksum = Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + late final Index idxTrashedLocalAssetAlbum = Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAlbumAssetAlbumAsset, + idxRemoteAlbumOwnerId, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxStackPrimaryAssetId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + idxRemoteAssetStackId, + idxRemoteAssetLocalDateTimeDay, + idxRemoteAssetLocalDateTimeMonth, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxPartnerSharedWithId, + idxLatLng, + idxRemoteAlbumAssetAlbumAsset, + idxRemoteAssetCloudId, + idxPersonOwnerId, + idxAssetFacePersonId, + idxAssetFaceAssetId, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + @override + int get schemaVersion => 20; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/infrastructure/repositories/backup_repository_test.dart b/mobile/test/infrastructure/repositories/backup_repository_test.dart new file mode 100644 index 0000000000..c042685779 --- /dev/null +++ b/mobile/test/infrastructure/repositories/backup_repository_test.dart @@ -0,0 +1,244 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; +import 'package:immich_mobile/utils/option.dart'; + +import '../../medium/repository_context.dart'; + +void main() { + late MediumRepositoryContext ctx; + late DriftBackupRepository sut; + + setUp(() { + ctx = MediumRepositoryContext(); + sut = DriftBackupRepository(ctx.db); + }); + + tearDown(() async { + await ctx.dispose(); + }); + + group('getAllCounts', () { + late String userId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + }); + + test('returns zeros when no albums exist', () async { + final result = await sut.getAllCounts(userId); + expect(result.total, 0); + expect(result.remainder, 0); + expect(result.processing, 0); + }); + + test('returns zeros when no selected albums exist', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.none); + final asset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id); + + final result = await sut.getAllCounts(userId); + expect(result.total, 0); + expect(result.remainder, 0); + expect(result.processing, 0); + }); + + test('counts asset in selected album as total and remainder', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final asset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id); + + final result = await sut.getAllCounts(userId); + expect(result.total, 1); + expect(result.remainder, 1); + expect(result.processing, 0); + }); + + test('backed up asset reduces remainder', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final remote = await ctx.newRemoteAsset(ownerId: userId); + final local = await ctx.newLocalAsset(checksum: remote.checksum); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id); + + final result = await sut.getAllCounts(userId); + expect(result.total, 1); + expect(result.remainder, 0); + expect(result.processing, 0); + }); + + test('asset with null checksum is counted as processing', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final asset = await ctx.newLocalAsset(checksumOption: const Option.none()); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id); + + final result = await sut.getAllCounts(userId); + expect(result.total, 1); + expect(result.remainder, 1); + expect(result.processing, 1); + }); + + test('asset in excluded album is not counted even if also in selected album', () async { + final selectedAlbum = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final excludedAlbum = await ctx.newLocalAlbum(backupSelection: BackupSelection.excluded); + final asset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: selectedAlbum.id, assetId: asset.id); + await ctx.newLocalAlbumAsset(albumId: excludedAlbum.id, assetId: asset.id); + + final result = await sut.getAllCounts(userId); + expect(result.total, 0); + expect(result.remainder, 0); + }); + + test('counts assets across multiple selected albums without duplicates', () async { + final album1 = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final album2 = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final asset = await ctx.newLocalAsset(); + // Same asset in two selected albums + await ctx.newLocalAlbumAsset(albumId: album1.id, assetId: asset.id); + await ctx.newLocalAlbumAsset(albumId: album2.id, assetId: asset.id); + + final result = await sut.getAllCounts(userId); + expect(result.total, 1); + }); + + test('backed up asset for different user is still counted as remainder', () async { + final otherUser = await ctx.newUser(); + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final remote = await ctx.newRemoteAsset(ownerId: otherUser.id); + final local = await ctx.newLocalAsset(checksum: remote.checksum); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id); + + final result = await sut.getAllCounts(userId); + expect(result.total, 1); + expect(result.remainder, 1); + }); + + test('mixed assets produce correct combined counts', () async { + final selectedAlbum = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + + // backed up + final remote1 = await ctx.newRemoteAsset(ownerId: userId); + final local1 = await ctx.newLocalAsset(checksum: remote1.checksum); + await ctx.newLocalAlbumAsset(albumId: selectedAlbum.id, assetId: local1.id); + + // not backed up, has checksum + final local2 = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: selectedAlbum.id, assetId: local2.id); + + // processing (null checksum) + final local3 = await ctx.newLocalAsset(checksumOption: const Option.none()); + await ctx.newLocalAlbumAsset(albumId: selectedAlbum.id, assetId: local3.id); + + final result = await sut.getAllCounts(userId); + expect(result.total, 3); + expect(result.remainder, 2); // local2 + local3 + expect(result.processing, 1); // local3 + }); + }); + + group('getCandidates', () { + late String userId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + }); + + test('returns empty list when no selected albums exist', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.none); + final asset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id); + + final result = await sut.getCandidates(userId); + expect(result, isEmpty); + }); + + test('returns asset in selected album that is not backed up', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final asset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id); + + final result = await sut.getCandidates(userId); + expect(result.length, 1); + expect(result.first.id, asset.id); + }); + + test('excludes asset already backed up for the same user', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final remote = await ctx.newRemoteAsset(ownerId: userId); + final local = await ctx.newLocalAsset(checksum: remote.checksum); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id); + + final result = await sut.getCandidates(userId); + expect(result, isEmpty); + }); + + test('includes asset backed up for a different user', () async { + final otherUser = await ctx.newUser(); + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final remote = await ctx.newRemoteAsset(ownerId: otherUser.id); + final local = await ctx.newLocalAsset(checksum: remote.checksum); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id); + + final result = await sut.getCandidates(userId); + expect(result.length, 1); + expect(result.first.id, local.id); + }); + + test('excludes asset in excluded album even if also in selected album', () async { + final selectedAlbum = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final excludedAlbum = await ctx.newLocalAlbum(backupSelection: BackupSelection.excluded); + final asset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: selectedAlbum.id, assetId: asset.id); + await ctx.newLocalAlbumAsset(albumId: excludedAlbum.id, assetId: asset.id); + + final result = await sut.getCandidates(userId); + expect(result, isEmpty); + }); + + test('excludes asset with null checksum when onlyHashed is true', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final asset = await ctx.newLocalAsset(checksumOption: const Option.none()); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id); + + final result = await sut.getCandidates(userId); + expect(result, isEmpty); + }); + + test('includes asset with null checksum when onlyHashed is false', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final asset = await ctx.newLocalAsset(checksumOption: const Option.none()); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id); + + final result = await sut.getCandidates(userId, onlyHashed: false); + expect(result.length, 1); + expect(result.first.id, asset.id); + }); + + test('returns assets ordered by createdAt descending', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final asset1 = await ctx.newLocalAsset(createdAt: DateTime(2024, 1, 1)); + final asset2 = await ctx.newLocalAsset(createdAt: DateTime(2024, 3, 1)); + final asset3 = await ctx.newLocalAsset(createdAt: DateTime(2024, 2, 1)); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset1.id); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset2.id); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset3.id); + + final result = await sut.getCandidates(userId); + expect(result.map((a) => a.id).toList(), [asset2.id, asset3.id, asset1.id]); + }); + + test('does not return duplicate when asset is in multiple selected albums', () async { + final album1 = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final album2 = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final asset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: album1.id, assetId: asset.id); + await ctx.newLocalAlbumAsset(albumId: album2.id, assetId: asset.id); + + final result = await sut.getCandidates(userId); + expect(result.length, 1); + expect(result.first.id, asset.id); + }); + }); +} diff --git a/mobile/test/infrastructure/repositories/local_asset_repository_test.dart b/mobile/test/infrastructure/repositories/local_asset_repository_test.dart index 245cc86a98..88f8d00e03 100644 --- a/mobile/test/infrastructure/repositories/local_asset_repository_test.dart +++ b/mobile/test/infrastructure/repositories/local_asset_repository_test.dart @@ -1,975 +1,569 @@ -import 'package:drift/drift.dart' hide isNull; -import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; +import 'package:immich_mobile/utils/option.dart'; + +import '../../medium/repository_context.dart'; void main() { - final now = DateTime(2024, 1, 15); - late Drift db; - late DriftLocalAssetRepository repository; + late MediumRepositoryContext ctx; + late DriftLocalAssetRepository sut; setUp(() { - db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - repository = DriftLocalAssetRepository(db); + ctx = MediumRepositoryContext(); + sut = DriftLocalAssetRepository(ctx.db); }); tearDown(() async { - await db.close(); + await ctx.dispose(); }); - Future insertLocalAsset({ - required String id, - String? checksum, - DateTime? createdAt, - AssetType type = AssetType.image, - bool isFavorite = false, - String? iCloudId, - DateTime? adjustmentTime, - double? latitude, - double? longitude, - }) async { - final created = createdAt ?? now; - await db - .into(db.localAssetEntity) - .insert( - LocalAssetEntityCompanion.insert( - id: id, - name: 'asset_$id.jpg', - checksum: Value(checksum), - type: type, - createdAt: Value(created), - updatedAt: Value(created), - isFavorite: Value(isFavorite), - iCloudId: Value(iCloudId), - adjustmentTime: Value(adjustmentTime), - latitude: Value(latitude), - longitude: Value(longitude), - ), - ); - } - - Future insertRemoteAsset({ - required String id, - required String checksum, - required String ownerId, - DateTime? deletedAt, - }) async { - await db - .into(db.remoteAssetEntity) - .insert( - RemoteAssetEntityCompanion.insert( - id: id, - name: 'remote_$id.jpg', - checksum: checksum, - type: AssetType.image, - createdAt: Value(now), - updatedAt: Value(now), - ownerId: ownerId, - visibility: AssetVisibility.timeline, - deletedAt: Value(deletedAt), - ), - ); - } - - Future insertRemoteAssetCloudId({ - required String assetId, - required String? cloudId, - DateTime? createdAt, - DateTime? adjustmentTime, - double? latitude, - double? longitude, - }) async { - await db - .into(db.remoteAssetCloudIdEntity) - .insert( - RemoteAssetCloudIdEntityCompanion.insert( - assetId: assetId, - cloudId: Value(cloudId), - createdAt: Value(createdAt), - adjustmentTime: Value(adjustmentTime), - latitude: Value(latitude), - longitude: Value(longitude), - ), - ); - } - - Future insertUser(String id, String email) async { - await db.into(db.userEntity).insert(UserEntityCompanion.insert(id: id, email: email, name: email)); - } - group('getRemovalCandidates', () { - final userId = 'user-123'; - final otherUserId = 'user-456'; - final cutoffDate = DateTime(2024, 1, 10); - final beforeCutoff = DateTime(2024, 1, 5); - final afterCutoff = DateTime(2024, 1, 12); + final cutoffDate = DateTime(2024, 1, 1); + final beforeCutoff = DateTime(2023, 12, 31); + final afterCutoff = DateTime(2024, 1, 2); + late String userId; setUp(() async { - await insertUser(userId, 'user@test.com'); - await insertUser(otherUserId, 'other@test.com'); + final user = await ctx.newUser(); + userId = user.id; }); - Future insertLocalAlbum({required String id, required String name, required bool isIosSharedAlbum}) async { - await db - .into(db.localAlbumEntity) - .insert( - LocalAlbumEntityCompanion.insert( - id: id, - name: name, - updatedAt: Value(now), - backupSelection: BackupSelection.none, - isIosSharedAlbum: Value(isIosSharedAlbum), - ), - ); - } - - Future insertLocalAlbumAsset({required String albumId, required String assetId}) async { - await db - .into(db.localAlbumAssetEntity) - .insert(LocalAlbumAssetEntityCompanion.insert(albumId: albumId, assetId: assetId)); - } - test('returns only assets that match all criteria', () async { + final otherUser = await ctx.newUser(); + // Asset 1: Should be included - backed up, before cutoff, correct owner, not deleted, not favorite - await insertLocalAsset( - id: 'local-1', - checksum: 'checksum-1', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-1', checksum: 'checksum-1', ownerId: userId); + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final includedAsset = await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); // Asset 2: Should NOT be included - not backed up (no remote asset) - await insertLocalAsset( - id: 'local-2', - checksum: 'checksum-2', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); + await ctx.newLocalAsset(createdAt: beforeCutoff); // Asset 3: Should NOT be included - after cutoff date - await insertLocalAsset( - id: 'local-3', - checksum: 'checksum-3', - createdAt: afterCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-3', checksum: 'checksum-3', ownerId: userId); + await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: afterCutoff); // Asset 4: Should NOT be included - different owner - await insertLocalAsset( - id: 'local-4', - checksum: 'checksum-4', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-4', checksum: 'checksum-4', ownerId: otherUserId); + final otherRemoteAsset = await ctx.newRemoteAsset(ownerId: otherUser.id); + await ctx.newLocalAsset(checksum: otherRemoteAsset.checksum, createdAt: beforeCutoff); // Asset 5: Should NOT be included - remote asset is deleted - await insertLocalAsset( - id: 'local-5', - checksum: 'checksum-5', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-5', checksum: 'checksum-5', ownerId: userId, deletedAt: now); + final deletedAsset = await ctx.newRemoteAsset(ownerId: userId, deletedAt: DateTime(2024, 1, 1)); + await ctx.newLocalAsset(checksum: deletedAsset.checksum, createdAt: beforeCutoff); // Asset 6: Should NOT be included - is favorite (when keepFavorites=true) - await insertLocalAsset( - id: 'local-6', - checksum: 'checksum-6', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: true, - ); - await insertRemoteAsset(id: 'remote-6', checksum: 'checksum-6', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepFavorites: true); + final favoriteAsset = await ctx.newRemoteAsset(ownerId: userId, isFavorite: true); + await ctx.newLocalAsset(checksum: favoriteAsset.checksum, createdAt: beforeCutoff, isFavorite: true); + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepFavorites: true); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-1'); + expect(result.assets.first.id, includedAsset.id); }); test('includes favorites when keepFavorites is false', () async { - await insertLocalAsset( - id: 'local-favorite', - checksum: 'checksum-fav', + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final favoriteAsset = await ctx.newLocalAsset( + checksum: remoteAsset.checksum, createdAt: beforeCutoff, - type: AssetType.image, isFavorite: true, ); - await insertRemoteAsset(id: 'remote-favorite', checksum: 'checksum-fav', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepFavorites: false); + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepFavorites: false); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-favorite'); - expect(result.assets[0].isFavorite, true); + expect(result.assets.first.id, favoriteAsset.id); + expect(result.assets.first.isFavorite, true); + }); + + test('excludes asset when both local and remote are favorites', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId, isFavorite: true); + await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff, isFavorite: true); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepFavorites: true); + expect(result.assets, isEmpty); + }); + + test('excludes asset when only local is favorite', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff, isFavorite: true); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepFavorites: true); + expect(result.assets, isEmpty); + }); + + test('excludes asset when only remote is favorite', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId, isFavorite: true); + await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepFavorites: true); + expect(result.assets, isEmpty); + }); + + test('includes asset when neither local nor remote is favorite', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final localAsset = await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepFavorites: true); + expect(result.assets.length, 1); + expect(result.assets.first.id, localAsset.id); }); test('keepMediaType photosOnly returns only videos for deletion', () async { + final photoAsset = await ctx.newRemoteAsset(ownerId: userId); // Photo - should be kept - await insertLocalAsset( - id: 'local-photo', - checksum: 'checksum-photo', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-photo', checksum: 'checksum-photo', ownerId: userId); + await ctx.newLocalAsset(checksum: photoAsset.checksum, createdAt: beforeCutoff); + final videoRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); // Video - should be deleted - await insertLocalAsset( - id: 'local-video', - checksum: 'checksum-video', + final videoLocalAsset = await ctx.newLocalAsset( + checksum: videoRemoteAsset.checksum, createdAt: beforeCutoff, type: AssetType.video, - isFavorite: false, ); - await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepMediaType: AssetKeepType.photosOnly); + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepMediaType: AssetKeepType.photosOnly); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-video'); - expect(result.assets[0].type, AssetType.video); + expect(result.assets.first.id, videoLocalAsset.id); + expect(result.assets.first.type, AssetType.video); }); test('keepMediaType videosOnly returns only photos for deletion', () async { // Photo - should be deleted - await insertLocalAsset( - id: 'local-photo', - checksum: 'checksum-photo', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-photo', checksum: 'checksum-photo', ownerId: userId); + final photoRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final photoAsset = await ctx.newLocalAsset(checksum: photoRemoteAsset.checksum, createdAt: beforeCutoff); // Video - should be kept - await insertLocalAsset( - id: 'local-video', - checksum: 'checksum-video', - createdAt: beforeCutoff, - type: AssetType.video, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepMediaType: AssetKeepType.videosOnly); + final videoRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + await ctx.newLocalAsset(checksum: videoRemoteAsset.checksum, createdAt: beforeCutoff, type: AssetType.video); + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepMediaType: AssetKeepType.videosOnly); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-photo'); - expect(result.assets[0].type, AssetType.image); + expect(result.assets.first.id, photoAsset.id); + expect(result.assets.first.type, AssetType.image); }); test('returns both photos and videos with keepMediaType.all', () async { // Photo - await insertLocalAsset( - id: 'local-photo', - checksum: 'checksum-photo', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-photo', checksum: 'checksum-photo', ownerId: userId); + final photoRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final photoAsset = await ctx.newLocalAsset(checksum: photoRemoteAsset.checksum, createdAt: beforeCutoff); // Video - await insertLocalAsset( - id: 'local-video', - checksum: 'checksum-video', + final videoRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final videoAsset = await ctx.newLocalAsset( + checksum: videoRemoteAsset.checksum, createdAt: beforeCutoff, type: AssetType.video, - isFavorite: false, ); - await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepMediaType: AssetKeepType.none); + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepMediaType: AssetKeepType.none); expect(result.assets.length, 2); final ids = result.assets.map((a) => a.id).toSet(); - expect(ids, containsAll(['local-photo', 'local-video'])); + expect(ids, containsAll([photoAsset.id, videoAsset.id])); }); test('excludes assets in iOS shared albums', () async { // Regular album - await insertLocalAlbum(id: 'album-regular', name: 'Regular Album', isIosSharedAlbum: false); + final regularAlbum = await ctx.newLocalAlbum(); // iOS shared album - await insertLocalAlbum(id: 'album-shared', name: 'Shared Album', isIosSharedAlbum: true); + final sharedAlbum = await ctx.newLocalAlbum(isIosSharedAlbum: true); // Asset in regular album (should be included) - await insertLocalAsset( - id: 'local-regular', - checksum: 'checksum-regular', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-regular', checksum: 'checksum-regular', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-regular', assetId: 'local-regular'); + final regularRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final regularAsset = await ctx.newLocalAsset(checksum: regularRemoteAsset.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: regularAlbum.id, assetId: regularAsset.id); // Asset in iOS shared album (should be excluded) - await insertLocalAsset( - id: 'local-shared', - checksum: 'checksum-shared', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-shared', checksum: 'checksum-shared', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-shared', assetId: 'local-shared'); - - final result = await repository.getRemovalCandidates(userId, cutoffDate); + final sharedRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final sharedAsset = await ctx.newLocalAsset(checksum: sharedRemoteAsset.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: sharedAlbum.id, assetId: sharedAsset.id); + final result = await sut.getRemovalCandidates(userId, cutoffDate); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-regular'); + expect(result.assets.first.id, regularAsset.id); }); test('includes assets at exact cutoff date', () async { - await insertLocalAsset( - id: 'local-exact', - checksum: 'checksum-exact', - createdAt: cutoffDate, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-exact', checksum: 'checksum-exact', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate); + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final localAsset = await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: cutoffDate); + final result = await sut.getRemovalCandidates(userId, cutoffDate); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-exact'); + expect(result.assets.first.id, localAsset.id); }); test('returns empty list when no assets match criteria', () async { // Only assets after cutoff - await insertLocalAsset( - id: 'local-after', - checksum: 'checksum-after', - createdAt: afterCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-after', checksum: 'checksum-after', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate); + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: afterCutoff); + final result = await sut.getRemovalCandidates(userId, cutoffDate); expect(result.assets, isEmpty); }); test('handles multiple assets with same checksum', () async { // Two local assets with same checksum (edge case, but should handle it) - await insertLocalAsset( - id: 'local-dup1', - checksum: 'checksum-dup', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertLocalAsset( - id: 'local-dup2', - checksum: 'checksum-dup', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-dup', checksum: 'checksum-dup', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate); + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + final result = await sut.getRemovalCandidates(userId, cutoffDate); expect(result.assets.length, 2); - expect(result.assets.map((a) => a.checksum).toSet(), equals({'checksum-dup'})); + expect(result.assets.map((a) => a.checksum).toSet(), equals({remoteAsset.checksum})); }); test('includes assets not in any album', () async { // Asset not in any album should be included - await insertLocalAsset( - id: 'local-no-album', - checksum: 'checksum-no-album', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-no-album', checksum: 'checksum-no-album', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate); + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final localAsset = await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + final result = await sut.getRemovalCandidates(userId, cutoffDate); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-no-album'); + expect(result.assets.first.id, localAsset.id); }); test('excludes asset that is in both regular and iOS shared album', () async { // Regular album - await insertLocalAlbum(id: 'album-regular', name: 'Regular Album', isIosSharedAlbum: false); + final regularAlbum = await ctx.newLocalAlbum(); // iOS shared album - await insertLocalAlbum(id: 'album-shared', name: 'Shared Album', isIosSharedAlbum: true); + final sharedAlbum = await ctx.newLocalAlbum(isIosSharedAlbum: true); // Asset in BOTH albums - should be excluded because it's in an iOS shared album - await insertLocalAsset( - id: 'local-both', - checksum: 'checksum-both', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-both', checksum: 'checksum-both', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-regular', assetId: 'local-both'); - await insertLocalAlbumAsset(albumId: 'album-shared', assetId: 'local-both'); - - final result = await repository.getRemovalCandidates(userId, cutoffDate); + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final localAsset = await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: regularAlbum.id, assetId: localAsset.id); + await ctx.newLocalAlbumAsset(albumId: sharedAlbum.id, assetId: localAsset.id); + final result = await sut.getRemovalCandidates(userId, cutoffDate); expect(result.assets, isEmpty); }); test('excludes assets with null checksum (not backed up)', () async { // Asset with null checksum cannot be matched to remote asset - await db - .into(db.localAssetEntity) - .insert( - LocalAssetEntityCompanion.insert( - id: 'local-null-checksum', - name: 'asset_null.jpg', - checksum: const Value.absent(), // null checksum - type: AssetType.image, - createdAt: Value(beforeCutoff), - updatedAt: Value(beforeCutoff), - isFavorite: const Value(false), - ), - ); - - final result = await repository.getRemovalCandidates(userId, cutoffDate); + await ctx.newLocalAsset(checksumOption: const Option.none()); + final result = await sut.getRemovalCandidates(userId, cutoffDate); expect(result.assets, isEmpty); }); test('excludes assets in user-excluded albums', () async { // Create two regular albums - await insertLocalAlbum(id: 'album-include', name: 'Include Album', isIosSharedAlbum: false); - await insertLocalAlbum(id: 'album-exclude', name: 'Exclude Album', isIosSharedAlbum: false); + final includeAlbum = await ctx.newLocalAlbum(); + final excludeAlbum = await ctx.newLocalAlbum(); // Asset in included album - should be included - await insertLocalAsset( - id: 'local-in-included', - checksum: 'checksum-included', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-included', checksum: 'checksum-included', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-include', assetId: 'local-in-included'); + final includedRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final includedAsset = await ctx.newLocalAsset(checksum: includedRemoteAsset.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: includeAlbum.id, assetId: includedAsset.id); // Asset in excluded album - should NOT be included - await insertLocalAsset( - id: 'local-in-excluded', - checksum: 'checksum-excluded', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-excluded', checksum: 'checksum-excluded', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-exclude', assetId: 'local-in-excluded'); + final excludedRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final excludedAsset = await ctx.newLocalAsset(checksum: excludedRemoteAsset.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: excludeAlbum.id, assetId: excludedAsset.id); - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {'album-exclude'}); + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {excludeAlbum.id}); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-in-included'); + expect(result.assets.first.id, includedAsset.id); }); test('excludes assets that are in any of multiple excluded albums', () async { // Create multiple albums - await insertLocalAlbum(id: 'album-1', name: 'Album 1', isIosSharedAlbum: false); - await insertLocalAlbum(id: 'album-2', name: 'Album 2', isIosSharedAlbum: false); - await insertLocalAlbum(id: 'album-3', name: 'Album 3', isIosSharedAlbum: false); + final album1 = await ctx.newLocalAlbum(); + final album2 = await ctx.newLocalAlbum(); + final album3 = await ctx.newLocalAlbum(); // Asset in album-1 (excluded) - should NOT be included - await insertLocalAsset( - id: 'local-1', - checksum: 'checksum-1', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-1', checksum: 'checksum-1', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-1', assetId: 'local-1'); + final remote1 = await ctx.newRemoteAsset(ownerId: userId); + final local1 = await ctx.newLocalAsset(checksum: remote1.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: album1.id, assetId: local1.id); // Asset in album-2 (excluded) - should NOT be included - await insertLocalAsset( - id: 'local-2', - checksum: 'checksum-2', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-2', checksum: 'checksum-2', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-2', assetId: 'local-2'); + final remote2 = await ctx.newRemoteAsset(ownerId: userId); + final local2 = await ctx.newLocalAsset(checksum: remote2.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: album2.id, assetId: local2.id); // Asset in album-3 (not excluded) - should be included - await insertLocalAsset( - id: 'local-3', - checksum: 'checksum-3', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-3', checksum: 'checksum-3', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-3', assetId: 'local-3'); - - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {'album-1', 'album-2'}); + final remote3 = await ctx.newRemoteAsset(ownerId: userId); + final local3 = await ctx.newLocalAsset(checksum: remote3.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: album3.id, assetId: local3.id); + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {album1.id, album2.id}); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-3'); + expect(result.assets.first.id, local3.id); }); test('excludes asset that is in both excluded and non-excluded album', () async { - await insertLocalAlbum(id: 'album-included', name: 'Included Album', isIosSharedAlbum: false); - await insertLocalAlbum(id: 'album-excluded', name: 'Excluded Album', isIosSharedAlbum: false); + final includedAlbum = await ctx.newLocalAlbum(); + final excludedAlbum = await ctx.newLocalAlbum(); // Asset in BOTH albums - should be excluded because it's in an excluded album - await insertLocalAsset( - id: 'local-both', - checksum: 'checksum-both', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-both', checksum: 'checksum-both', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-included', assetId: 'local-both'); - await insertLocalAlbumAsset(albumId: 'album-excluded', assetId: 'local-both'); - - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {'album-excluded'}); + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final localAsset = await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: includedAlbum.id, assetId: localAsset.id); + await ctx.newLocalAlbumAsset(albumId: excludedAlbum.id, assetId: localAsset.id); + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {excludedAlbum.id}); expect(result.assets, isEmpty); }); test('includes all assets when excludedAlbumIds is empty', () async { - await insertLocalAlbum(id: 'album-1', name: 'Album 1', isIosSharedAlbum: false); + final album1 = await ctx.newLocalAlbum(); - await insertLocalAsset( - id: 'local-1', - checksum: 'checksum-1', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-1', checksum: 'checksum-1', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-1', assetId: 'local-1'); + final remote1 = await ctx.newRemoteAsset(ownerId: userId); + final local1 = await ctx.newLocalAsset(checksum: remote1.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: album1.id, assetId: local1.id); - await insertLocalAsset( - id: 'local-2', - checksum: 'checksum-2', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-2', checksum: 'checksum-2', ownerId: userId); + final remote2 = await ctx.newRemoteAsset(ownerId: userId); + await ctx.newLocalAsset(checksum: remote2.checksum, createdAt: beforeCutoff); // Empty excludedAlbumIds should include all eligible assets - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {}); - + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {}); expect(result.assets.length, 2); }); test('excludes asset not in any album when album is excluded', () async { - await insertLocalAlbum(id: 'album-excluded', name: 'Excluded Album', isIosSharedAlbum: false); + final excludedAlbum = await ctx.newLocalAlbum(); // Asset NOT in any album - should be included - await insertLocalAsset( - id: 'local-no-album', - checksum: 'checksum-no-album', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-no-album', checksum: 'checksum-no-album', ownerId: userId); + final noAlbumRemote = await ctx.newRemoteAsset(ownerId: userId); + final noAlbumAsset = await ctx.newLocalAsset(checksum: noAlbumRemote.checksum, createdAt: beforeCutoff); // Asset in excluded album - should NOT be included - await insertLocalAsset( - id: 'local-in-excluded', - checksum: 'checksum-in-excluded', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-in-excluded', checksum: 'checksum-in-excluded', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-excluded', assetId: 'local-in-excluded'); - - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {'album-excluded'}); + final excludedRemote = await ctx.newRemoteAsset(ownerId: userId); + final excludedAsset = await ctx.newLocalAsset(checksum: excludedRemote.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: excludedAlbum.id, assetId: excludedAsset.id); + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {excludedAlbum.id}); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-no-album'); + expect(result.assets.first.id, noAlbumAsset.id); }); test('combines excludedAlbumIds with keepMediaType correctly', () async { - await insertLocalAlbum(id: 'album-excluded', name: 'Excluded Album', isIosSharedAlbum: false); - await insertLocalAlbum(id: 'album-regular', name: 'Regular Album', isIosSharedAlbum: false); + final excludedAlbum = await ctx.newLocalAlbum(); + final regularAlbum = await ctx.newLocalAlbum(); // Photo in excluded album - should NOT be included (album excluded) - await insertLocalAsset( - id: 'local-photo-excluded', - checksum: 'checksum-photo-excluded', + final photoExcludedRemote = await ctx.newRemoteAsset(ownerId: userId); + final photoExcludedAsset = await ctx.newLocalAsset( + checksum: photoExcludedRemote.checksum, createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, ); - await insertRemoteAsset(id: 'remote-photo-excluded', checksum: 'checksum-photo-excluded', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-excluded', assetId: 'local-photo-excluded'); + await ctx.newLocalAlbumAsset(albumId: excludedAlbum.id, assetId: photoExcludedAsset.id); // Video in regular album - should be included (keepMediaType photosOnly = delete videos) - await insertLocalAsset( - id: 'local-video', - checksum: 'checksum-video', + final videoRemote = await ctx.newRemoteAsset(ownerId: userId); + final videoAsset = await ctx.newLocalAsset( + checksum: videoRemote.checksum, createdAt: beforeCutoff, type: AssetType.video, - isFavorite: false, ); - await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-regular', assetId: 'local-video'); + await ctx.newLocalAlbumAsset(albumId: regularAlbum.id, assetId: videoAsset.id); // Photo in regular album - should NOT be included (keepMediaType photosOnly = keep photos) - await insertLocalAsset( - id: 'local-photo-regular', - checksum: 'checksum-photo-regular', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-photo-regular', checksum: 'checksum-photo-regular', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-regular', assetId: 'local-photo-regular'); + final photoRegularRemote = await ctx.newRemoteAsset(ownerId: userId); + final photoRegularAsset = await ctx.newLocalAsset(checksum: photoRegularRemote.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: regularAlbum.id, assetId: photoRegularAsset.id); - final result = await repository.getRemovalCandidates( + final result = await sut.getRemovalCandidates( userId, cutoffDate, keepMediaType: AssetKeepType.photosOnly, - keepAlbumIds: {'album-excluded'}, + keepAlbumIds: {excludedAlbum.id}, ); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-video'); + expect(result.assets.first.id, videoAsset.id); }); }); group('reconcileHashesFromCloudId', () { - final userId = 'user-123'; - final createdAt = DateTime(2024, 1, 10); - final adjustmentTime = DateTime(2024, 1, 11); - const latitude = 37.7749; - const longitude = -122.4194; + late String userId; setUp(() async { - await insertUser(userId, 'user@test.com'); + final user = await ctx.newUser(); + userId = user.id; }); test('updates local asset checksum when all metadata matches', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final remoteCloudAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: remoteCloudAsset.cloudId, + createdAt: remoteCloudAsset.createdAt, + adjustmentTime: remoteCloudAsset.adjustmentTime, + latitude: remoteCloudAsset.latitude, + longitude: remoteCloudAsset.longitude, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); - expect(updated?.checksum, 'hash-abc123'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, remoteAsset.checksum); }); test('does not update when local asset already has checksum', () async { - await insertLocalAsset( - id: 'local-1', - checksum: 'existing-checksum', - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final remoteCloudAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id); + + final localAsset = await ctx.newLocalAsset( + checksum: 'existing', + iCloudId: remoteCloudAsset.cloudId, + createdAt: remoteCloudAsset.createdAt, + adjustmentTime: remoteCloudAsset.adjustmentTime, + latitude: remoteCloudAsset.latitude, + longitude: remoteCloudAsset.longitude, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); - expect(updated?.checksum, 'existing-checksum'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, 'existing'); }); test('does not update when adjustment_time does not match', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, adjustmentTime: DateTime(2024, 1, 12)); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: cloudIdAsset.createdAt, + adjustmentTime: DateTime(2026, 1, 12), + latitude: cloudIdAsset.latitude, + longitude: cloudIdAsset.longitude, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: DateTime(2024, 1, 12), - latitude: latitude, - longitude: longitude, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); expect(updated?.checksum, isNull); }); test('does not update when latitude does not match', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); - - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, latitude: const Option.none()); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: cloudIdAsset.createdAt, + adjustmentTime: cloudIdAsset.adjustmentTime, latitude: 40.7128, - longitude: longitude, + longitude: cloudIdAsset.longitude, ); - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); expect(updated?.checksum, isNull); }); test('does not update when longitude does not match', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, longitude: (-74.006).toOption()); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: cloudIdAsset.createdAt, + adjustmentTime: cloudIdAsset.adjustmentTime, + latitude: cloudIdAsset.latitude, + longitude: 0.0, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: -74.0060, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); expect(updated?.checksum, isNull); }); test('does not update when createdAt does not match', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, createdAt: DateTime(2024, 1, 5)); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: DateTime(2024, 6, 1), + adjustmentTime: cloudIdAsset.adjustmentTime, + latitude: cloudIdAsset.latitude, + longitude: cloudIdAsset.longitude, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: DateTime(2024, 1, 5), - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); expect(updated?.checksum, isNull); }); test('does not update when iCloudId is null', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), iCloudId: null, - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, + createdAt: cloudIdAsset.createdAt, + adjustmentTime: cloudIdAsset.adjustmentTime, + latitude: cloudIdAsset.latitude, + longitude: cloudIdAsset.longitude, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); expect(updated?.checksum, isNull); }); test('does not update when cloudId does not match iCloudId', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: 'different-cloud-id', + createdAt: cloudIdAsset.createdAt, + adjustmentTime: cloudIdAsset.adjustmentTime, + latitude: cloudIdAsset.latitude, + longitude: cloudIdAsset.longitude, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-456', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); expect(updated?.checksum, isNull); }); test('handles partial null metadata fields matching correctly', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: null, - latitude: latitude, - longitude: longitude, + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final cloudIdAsset = await ctx.newRemoteAssetCloudId( + id: remoteAsset.id, + adjustmentTimeOption: const Option.none(), + ); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: cloudIdAsset.createdAt, + adjustmentTimeOption: const Option.none(), + latitude: cloudIdAsset.latitude, + longitude: cloudIdAsset.longitude, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: null, - latitude: latitude, - longitude: longitude, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); - expect(updated?.checksum, 'hash-abc123'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, remoteAsset.checksum); }); test('does not update when one has null and other has value', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: cloudIdAsset.createdAt, + adjustmentTime: cloudIdAsset.adjustmentTime, latitude: null, - longitude: longitude, + longitude: cloudIdAsset.longitude, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); expect(updated?.checksum, isNull); }); test('handles no matching assets gracefully', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-999', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); + final localAsset = await ctx.newLocalAsset(checksumOption: const Option.none(), iCloudId: 'cloud-no-match'); - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); expect(updated?.checksum, isNull); }); }); diff --git a/mobile/test/infrastructure/repositories/remote_album_repository_test.dart b/mobile/test/infrastructure/repositories/remote_album_repository_test.dart new file mode 100644 index 0000000000..1bc797f6e1 --- /dev/null +++ b/mobile/test/infrastructure/repositories/remote_album_repository_test.dart @@ -0,0 +1,210 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; + +import '../../medium/repository_context.dart'; + +void main() { + late MediumRepositoryContext ctx; + late DriftRemoteAlbumRepository sut; + + setUp(() async { + ctx = MediumRepositoryContext(); + sut = DriftRemoteAlbumRepository(ctx.db); + }); + + tearDown(() async { + await ctx.dispose(); + }); + + group('getSortedAlbumIds', () { + late String userId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + }); + + test('returns empty list when albumIds is empty', () async { + final result = await sut.getSortedAlbumIds([], aggregation: AssetDateAggregation.start); + expect(result, isEmpty); + }); + + test('returns single album when only one album exists', () async { + final album = await ctx.newRemoteAlbum(ownerId: userId); + final asset = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 1)); + await ctx.insertRemoteAlbumAsset(albumId: album.id, assetId: asset.id); + + final result = await sut.getSortedAlbumIds([album.id], aggregation: AssetDateAggregation.start); + expect(result, [album.id]); + }); + + test('sorts albums by start date (MIN) ascending', () async { + // Album 1: Assets from Jan 10 to Jan 20 (start: Jan 10) + final album1 = await ctx.newRemoteAlbum(ownerId: userId); + final asset1 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 10)); + final asset2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 20)); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset1.id); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset2.id); + + // Album 2: Assets from Jan 5 to Jan 15 (start: Jan 5) + final album2 = await ctx.newRemoteAlbum(ownerId: userId); + final asset3 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 5)); + final asset4 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 15)); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset3.id); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset4.id); + + // Album 3: Assets from Jan 25 to Jan 30 (start: Jan 25) + final album3 = await ctx.newRemoteAlbum(ownerId: userId); + final asset5 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 25)); + final asset6 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 30)); + await ctx.insertRemoteAlbumAsset(albumId: album3.id, assetId: asset5.id); + await ctx.insertRemoteAlbumAsset(albumId: album3.id, assetId: asset6.id); + + final result = await sut.getSortedAlbumIds([ + album1.id, + album2.id, + album3.id, + ], aggregation: AssetDateAggregation.start); + + // Expected order: album2 (Jan 5), album1 (Jan 10), album3 (Jan 25) + expect(result, [album2.id, album1.id, album3.id]); + }); + + test('sorts albums by end date (MAX) ascending', () async { + // Album 1: Assets from Jan 10 to Jan 20 (end: Jan 20) + final album1 = await ctx.newRemoteAlbum(ownerId: userId); + final asset1 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 10)); + final asset2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 20)); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset1.id); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset2.id); + + // Album 2: Assets from Jan 5 to Jan 15 (end: Jan 15) + final album2 = await ctx.newRemoteAlbum(ownerId: userId); + final asset3 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 5)); + final asset4 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 15)); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset3.id); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset4.id); + + // Album 3: Assets from Jan 25 to Jan 30 (end: Jan 30) + final album3 = await ctx.newRemoteAlbum(ownerId: userId); + final asset5 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 25)); + final asset6 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 30)); + await ctx.insertRemoteAlbumAsset(albumId: album3.id, assetId: asset5.id); + await ctx.insertRemoteAlbumAsset(albumId: album3.id, assetId: asset6.id); + + final result = await sut.getSortedAlbumIds([ + album1.id, + album2.id, + album3.id, + ], aggregation: AssetDateAggregation.end); + + // Expected order: album2 (Jan 15), album1 (Jan 20), album3 (Jan 30) + expect(result, [album2.id, album1.id, album3.id]); + }); + + test('handles albums with single asset', () async { + final album1 = await ctx.newRemoteAlbum(ownerId: userId); + final asset1 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 15)); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset1.id); + + final album2 = await ctx.newRemoteAlbum(ownerId: userId); + final asset2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 10)); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset2.id); + + final result = await sut.getSortedAlbumIds([album1.id, album2.id], aggregation: AssetDateAggregation.start); + + expect(result, [album2.id, album1.id]); + }); + + test('only returns requested album IDs in the result', () async { + // Create 3 albums + final album1 = await ctx.newRemoteAlbum(ownerId: userId); + final asset1 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 10)); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset1.id); + + final album2 = await ctx.newRemoteAlbum(ownerId: userId); + final asset2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 5)); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset2.id); + + final album3 = await ctx.newRemoteAlbum(ownerId: userId); + final asset3 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 15)); + await ctx.insertRemoteAlbumAsset(albumId: album3.id, assetId: asset3.id); + + // Only request album1 and album3 + final result = await sut.getSortedAlbumIds([album1.id, album3.id], aggregation: AssetDateAggregation.start); + + // Should only return album1 and album3, not album2 + expect(result, [album1.id, album3.id]); + }); + + test('handles albums with same date correctly', () async { + final sameDate = DateTime(2024, 1, 10); + + final album1 = await ctx.newRemoteAlbum(ownerId: userId); + final asset1 = await ctx.newRemoteAsset(ownerId: userId, createdAt: sameDate); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset1.id); + + final album2 = await ctx.newRemoteAlbum(ownerId: userId); + final asset2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: sameDate); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset2.id); + + final result = await sut.getSortedAlbumIds([album1.id, album2.id], aggregation: AssetDateAggregation.start); + + // Both albums have the same date, so both should be returned + expect(result, hasLength(2)); + expect(result, containsAll([album1.id, album2.id])); + }); + + test('handles albums across different years', () async { + final album1 = await ctx.newRemoteAlbum(ownerId: userId); + final asset1 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2023, 12, 25)); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset1.id); + + final album2 = await ctx.newRemoteAlbum(ownerId: userId); + final asset2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 5)); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset2.id); + + final album3 = await ctx.newRemoteAlbum(ownerId: userId); + final asset3 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2025, 1, 1)); + await ctx.insertRemoteAlbumAsset(albumId: album3.id, assetId: asset3.id); + + final result = await sut.getSortedAlbumIds([ + album1.id, + album2.id, + album3.id, + ], aggregation: AssetDateAggregation.start); + + expect(result, [album1.id, album2.id, album3.id]); + }); + + test('handles album with multiple assets correctly', () async { + final album1 = await ctx.newRemoteAlbum(ownerId: userId); + // Album 1 has 5 assets from Jan 5 to Jan 25 + final asset1 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 5)); + final asset2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 10)); + final asset3 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 15)); + final asset4 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 20)); + final asset5 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 25)); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset1.id); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset2.id); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset3.id); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset4.id); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset5.id); + + final album2 = await ctx.newRemoteAlbum(ownerId: userId); + final asset6 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 1)); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset6.id); + + final resultStart = await sut.getSortedAlbumIds([album1.id, album2.id], aggregation: AssetDateAggregation.start); + + // album2 (Jan 1) should come before album1 (Jan 5) + expect(resultStart, [album2.id, album1.id]); + + final resultEnd = await sut.getSortedAlbumIds([album1.id, album2.id], aggregation: AssetDateAggregation.end); + + // album2 (Jan 1) should come before album1 (Jan 25) + expect(resultEnd, [album2.id, album1.id]); + }); + }); +} diff --git a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart index 660b8206bb..62aae4c0da 100644 --- a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart +++ b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; +import 'package:immich_mobile/utils/semver.dart'; import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; @@ -72,8 +73,14 @@ void main() { Future streamChanges( Future Function(List, Function() abort, Function() reset) onDataCallback, + SemVer serverVersion, ) { - return sut.streamChanges(onDataCallback, batchSize: testBatchSize, httpClient: mockHttpClient); + return sut.streamChanges( + onDataCallback, + batchSize: testBatchSize, + httpClient: mockHttpClient, + serverVersion: serverVersion, + ); } test('streamChanges stops processing stream when abort is called', () async { @@ -94,7 +101,7 @@ void main() { } } - final streamChangesFuture = streamChanges(onDataCallback); + final streamChangesFuture = streamChanges(onDataCallback, const SemVer(major: 2, minor: 5, patch: 0)); // Give the stream subscription time to start (longer delay to account for mock delay) await Future.delayed(const Duration(milliseconds: 50)); @@ -145,7 +152,7 @@ void main() { } } - final streamChangesFuture = streamChanges(onDataCallback); + final streamChangesFuture = streamChanges(onDataCallback, const SemVer(major: 2, minor: 5, patch: 0)); await Future.delayed(const Duration(milliseconds: 50)); @@ -197,7 +204,7 @@ void main() { } } - final streamChangesFuture = streamChanges(onDataCallback); + final streamChangesFuture = streamChanges(onDataCallback, const SemVer(major: 2, minor: 5, patch: 0)); await Future.delayed(const Duration(milliseconds: 50)); @@ -244,7 +251,7 @@ void main() { onDataCallCount++; } - final streamChangesFuture = streamChanges(onDataCallback); + final streamChangesFuture = streamChanges(onDataCallback, const SemVer(major: 2, minor: 5, patch: 0)); await Future.delayed(const Duration(milliseconds: 50)); @@ -271,7 +278,7 @@ void main() { onDataCallCount++; } - final future = streamChanges(onDataCallback); + final future = streamChanges(onDataCallback, const SemVer(major: 2, minor: 5, patch: 0)); errorBodyController.add(utf8.encode('{"error":"Unauthorized"}')); await errorBodyController.close(); diff --git a/mobile/test/medium/repository_context.dart b/mobile/test/medium/repository_context.dart new file mode 100644 index 0000000000..2c4758400c --- /dev/null +++ b/mobile/test/medium/repository_context.dart @@ -0,0 +1,246 @@ +import 'dart:math'; + +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/utils/option.dart'; +import 'package:uuid/uuid.dart'; + +class MediumRepositoryContext { + final Drift db; + final Random _random = Random(); + + MediumRepositoryContext() : db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + + Future dispose() async { + await db.close(); + } + + static Value _resolveUndefined(T? plain, Option? option, T fallback) { + if (plain != null) { + return Value(plain); + } + + return _resolveOption(option, fallback); + } + + static Value _resolveOption(Option? option, T fallback) { + if (option != null) { + return option.fold(Value.new, Value.absent); + } + + return Value(fallback); + } + + Future newUser({ + String? id, + String? email, + AvatarColor? avatarColor, + DateTime? profileChangedAt, + bool? hasProfileImage, + }) async { + id = id ?? const Uuid().v4(); + return await db + .into(db.userEntity) + .insertReturning( + UserEntityCompanion( + id: Value(id), + email: Value(email ?? '$id@test.com'), + name: Value(email ?? 'user_$id'), + avatarColor: Value(avatarColor ?? AvatarColor.values[_random.nextInt(AvatarColor.values.length)]), + profileChangedAt: Value(profileChangedAt ?? DateTime.now()), + hasProfileImage: Value(hasProfileImage ?? false), + ), + ); + } + + Future newRemoteAsset({ + String? id, + String? checksum, + String? ownerId, + DateTime? createdAt, + DateTime? updatedAt, + DateTime? deletedAt, + AssetType? type, + AssetVisibility? visibility, + int? durationInSeconds, + int? width, + int? height, + bool? isFavorite, + bool? isEdited, + String? livePhotoVideoId, + String? stackId, + String? thumbHash, + String? libraryId, + }) async { + id = id ?? const Uuid().v4(); + createdAt = createdAt ?? DateTime.now(); + return db + .into(db.remoteAssetEntity) + .insertReturning( + RemoteAssetEntityCompanion( + id: Value(id), + name: Value('remote_$id.jpg'), + checksum: Value(checksum ?? const Uuid().v4()), + type: Value(type ?? AssetType.image), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt ?? DateTime.now()), + ownerId: Value(ownerId ?? const Uuid().v4()), + visibility: Value(visibility ?? AssetVisibility.timeline), + deletedAt: Value(deletedAt), + durationInSeconds: Value(durationInSeconds ?? 0), + width: Value(width ?? _random.nextInt(1000)), + height: Value(height ?? _random.nextInt(1000)), + isFavorite: Value(isFavorite ?? false), + isEdited: Value(isEdited ?? false), + livePhotoVideoId: Value(livePhotoVideoId), + stackId: Value(stackId), + localDateTime: Value(createdAt.toLocal()), + thumbHash: Value(thumbHash ?? const Uuid().v4()), + libraryId: Value(libraryId ?? const Uuid().v4()), + ), + ); + } + + Future newRemoteAssetCloudId({ + String? id, + String? cloudId, + DateTime? createdAt, + DateTime? adjustmentTime, + Option? adjustmentTimeOption, + Option? latitude, + Option? longitude, + }) { + return db + .into(db.remoteAssetCloudIdEntity) + .insertReturning( + RemoteAssetCloudIdEntityCompanion( + assetId: Value(id ?? const Uuid().v4()), + cloudId: Value(cloudId ?? const Uuid().v4()), + createdAt: Value(createdAt ?? DateTime.now()), + adjustmentTime: _resolveUndefined(adjustmentTime, adjustmentTimeOption, DateTime.now()), + latitude: _resolveOption(latitude, _random.nextDouble() * 180 - 90), + longitude: _resolveOption(longitude, _random.nextDouble() * 360 - 180), + ), + ); + } + + Future newRemoteAlbum({ + String? id, + String? name, + String? ownerId, + DateTime? createdAt, + DateTime? updatedAt, + String? description, + bool? isActivityEnabled, + AlbumAssetOrder? order, + String? thumbnailAssetId, + }) async { + id = id ?? const Uuid().v4(); + return db + .into(db.remoteAlbumEntity) + .insertReturning( + RemoteAlbumEntityCompanion( + id: Value(id), + name: Value(name ?? 'remote_album_$id'), + ownerId: Value(ownerId ?? const Uuid().v4()), + createdAt: Value(createdAt ?? DateTime.now()), + updatedAt: Value(updatedAt ?? DateTime.now()), + description: Value(description ?? 'Description for album $id'), + isActivityEnabled: Value(isActivityEnabled ?? false), + order: Value(order ?? AlbumAssetOrder.asc), + thumbnailAssetId: Value(thumbnailAssetId), + ), + ); + } + + Future insertRemoteAlbumAsset({required String albumId, required String assetId}) { + return db + .into(db.remoteAlbumAssetEntity) + .insert(RemoteAlbumAssetEntityCompanion.insert(albumId: albumId, assetId: assetId)); + } + + Future newLocalAsset({ + String? id, + String? name, + String? checksum, + Option? checksumOption, + DateTime? createdAt, + AssetType? type, + bool? isFavorite, + String? iCloudId, + DateTime? adjustmentTime, + Option? adjustmentTimeOption, + double? latitude, + double? longitude, + int? width, + int? height, + int? durationInSeconds, + int? orientation, + DateTime? updatedAt, + }) async { + id = id ?? const Uuid().v4(); + return db + .into(db.localAssetEntity) + .insertReturning( + LocalAssetEntityCompanion( + id: Value(id), + name: Value(name ?? 'local_$id.jpg'), + height: Value(height ?? _random.nextInt(1000)), + width: Value(width ?? _random.nextInt(1000)), + durationInSeconds: Value(durationInSeconds ?? 0), + orientation: Value(orientation ?? 0), + updatedAt: Value(updatedAt ?? DateTime.now()), + checksum: _resolveUndefined(checksum, checksumOption, const Uuid().v4()), + createdAt: Value(createdAt ?? DateTime.now()), + type: Value(type ?? AssetType.image), + isFavorite: Value(isFavorite ?? false), + iCloudId: Value(iCloudId ?? const Uuid().v4()), + adjustmentTime: _resolveUndefined(adjustmentTime, adjustmentTimeOption, DateTime.now()), + latitude: Value(latitude ?? _random.nextDouble() * 180 - 90), + longitude: Value(longitude ?? _random.nextDouble() * 360 - 180), + ), + ); + } + + Future newLocalAlbum({ + String? id, + String? name, + DateTime? updatedAt, + BackupSelection? backupSelection, + bool? isIosSharedAlbum, + String? linkedRemoteAlbumId, + }) { + id = id ?? const Uuid().v4(); + return db + .into(db.localAlbumEntity) + .insertReturning( + LocalAlbumEntityCompanion( + id: Value(id), + name: Value(name ?? 'local_album_$id'), + updatedAt: Value(updatedAt ?? DateTime.now()), + backupSelection: Value(backupSelection ?? BackupSelection.none), + isIosSharedAlbum: Value(isIosSharedAlbum ?? false), + linkedRemoteAlbumId: Value(linkedRemoteAlbumId), + ), + ); + } + + Future newLocalAlbumAsset({required String albumId, required String assetId}) { + return db + .into(db.localAlbumAssetEntity) + .insert(LocalAlbumAssetEntityCompanion.insert(albumId: albumId, assetId: assetId)); + } +} diff --git a/mobile/test/utils/option_test.dart b/mobile/test/utils/option_test.dart new file mode 100644 index 0000000000..4fa44a3865 --- /dev/null +++ b/mobile/test/utils/option_test.dart @@ -0,0 +1,116 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/utils/option.dart'; + +void main() { + group('Option', () { + group('constructors', () { + test('Option.some creates a Some instance', () { + const option = Option.some(42); + expect(option, isA>()); + expect((option as Some).value, 42); + }); + + test('Option.none creates a None instance', () { + const option = Option.none(); + expect(option, isA>()); + }); + + test('Option.fromNullable returns Some for non-null value', () { + final option = Option.fromNullable('hello'); + expect(option, isA>()); + expect((option as Some).value, 'hello'); + }); + + test('Option.fromNullable returns None for null value', () { + final option = Option.fromNullable(null); + expect(option, isA()); + }); + }); + + group('isSome / isNone', () { + test('Some.isSome is true', () { + expect(const Option.some(1).isSome, isTrue); + }); + + test('Some.isNone is false', () { + expect(const Option.some(1).isNone, isFalse); + }); + + test('None.isSome is false', () { + expect(const Option.none().isSome, isFalse); + }); + + test('None.isNone is true', () { + expect(const Option.none().isNone, isTrue); + }); + }); + + group('unwrapOrNull', () { + test('returns value for Some', () { + expect(const Option.some('hi').unwrapOrNull, 'hi'); + }); + + test('returns null for None', () { + expect(const Option.none().unwrapOrNull, isNull); + }); + }); + + group('fold', () { + test('calls onSome with value for Some', () { + final result = const Option.some('world').fold((v) => 'some: $v', () => 'none'); + expect(result, 'some: world'); + }); + + test('calls onNone for None', () { + final result = const Option.none().fold((v) => 'some: $v', () => 'none'); + expect(result, 'none'); + }); + }); + + group('equality', () { + test('Some equals Some with same value', () { + expect(const Option.some(1) == const Option.some(1), isTrue); + }); + + test('Some does not equal Some with different value', () { + expect(const Option.some(1) == const Option.some(2), isFalse); + }); + + test('None equals None of same type', () { + expect(const Option.none() == const Option.none(), isTrue); + }); + + test('None does not equal None of different type', () { + expect(const Option.none() == (const Option.none() as Object), isFalse); + }); + + test('Some does not equal None', () { + expect(const Option.some(0) == const Option.none(), isFalse); + }); + }); + + group('hashCode', () { + test('Some hashCode equals value hashCode', () { + expect(const Option.some('abc').hashCode, 'abc'.hashCode); + }); + + test('None hashCode is 0', () { + expect(const Option.none().hashCode, 0); + }); + }); + }); + + group('ObjectOptionExtension', () { + test('non-null value.toOption() returns Some', () { + final option = 'hello'.toOption(); + expect(option, isA>()); + expect((option as Some).value, 'hello'); + }); + + test('null value.toOption() returns None', () { + const String? value = null; + final option = value.toOption(); + expect(option, isA>()); + }); + }); +} diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index ba6deefc30..5418711424 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -3993,7 +3993,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AssetEditsDto" + "$ref": "#/components/schemas/AssetEditsResponseDto" } } }, @@ -4046,7 +4046,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AssetEditActionListDto" + "$ref": "#/components/schemas/AssetEditsCreateDto" } } }, @@ -4057,7 +4057,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AssetEditsDto" + "$ref": "#/components/schemas/AssetEditsResponseDto" } } }, @@ -16376,7 +16376,7 @@ ], "type": "string" }, - "AssetEditActionCrop": { + "AssetEditActionItemDto": { "properties": { "action": { "allOf": [ @@ -16387,7 +16387,18 @@ "description": "Type of edit action to perform" }, "parameters": { - "$ref": "#/components/schemas/CropParameters" + "anyOf": [ + { + "$ref": "#/components/schemas/CropParameters" + }, + { + "$ref": "#/components/schemas/RotateParameters" + }, + { + "$ref": "#/components/schemas/MirrorParameters" + } + ], + "description": "List of edit actions to apply (crop, rotate, or mirror)" } }, "required": [ @@ -16396,30 +16407,48 @@ ], "type": "object" }, - "AssetEditActionListDto": { + "AssetEditActionItemResponseDto": { + "properties": { + "action": { + "allOf": [ + { + "$ref": "#/components/schemas/AssetEditAction" + } + ], + "description": "Type of edit action to perform" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "parameters": { + "anyOf": [ + { + "$ref": "#/components/schemas/CropParameters" + }, + { + "$ref": "#/components/schemas/RotateParameters" + }, + { + "$ref": "#/components/schemas/MirrorParameters" + } + ], + "description": "List of edit actions to apply (crop, rotate, or mirror)" + } + }, + "required": [ + "action", + "id", + "parameters" + ], + "type": "object" + }, + "AssetEditsCreateDto": { "properties": { "edits": { "description": "List of edit actions to apply (crop, rotate, or mirror)", "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/AssetEditActionCrop" - }, - { - "$ref": "#/components/schemas/AssetEditActionRotate" - }, - { - "$ref": "#/components/schemas/AssetEditActionMirror" - } - ], - "discriminator": { - "mapping": { - "crop": "#/components/schemas/AssetEditActionCrop", - "mirror": "#/components/schemas/AssetEditActionMirror", - "rotate": "#/components/schemas/AssetEditActionRotate" - }, - "propertyName": "action" - } + "$ref": "#/components/schemas/AssetEditActionItemDto" }, "minItems": 1, "type": "array" @@ -16430,77 +16459,18 @@ ], "type": "object" }, - "AssetEditActionMirror": { - "properties": { - "action": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetEditAction" - } - ], - "description": "Type of edit action to perform" - }, - "parameters": { - "$ref": "#/components/schemas/MirrorParameters" - } - }, - "required": [ - "action", - "parameters" - ], - "type": "object" - }, - "AssetEditActionRotate": { - "properties": { - "action": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetEditAction" - } - ], - "description": "Type of edit action to perform" - }, - "parameters": { - "$ref": "#/components/schemas/RotateParameters" - } - }, - "required": [ - "action", - "parameters" - ], - "type": "object" - }, - "AssetEditsDto": { + "AssetEditsResponseDto": { "properties": { "assetId": { - "description": "Asset ID to apply edits to", + "description": "Asset ID these edits belong to", "format": "uuid", "type": "string" }, "edits": { - "description": "List of edit actions to apply (crop, rotate, or mirror)", + "description": "List of edit actions applied to the asset", "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/AssetEditActionCrop" - }, - { - "$ref": "#/components/schemas/AssetEditActionRotate" - }, - { - "$ref": "#/components/schemas/AssetEditActionMirror" - } - ], - "discriminator": { - "mapping": { - "crop": "#/components/schemas/AssetEditActionCrop", - "mirror": "#/components/schemas/AssetEditActionMirror", - "rotate": "#/components/schemas/AssetEditActionRotate" - }, - "propertyName": "action" - } + "$ref": "#/components/schemas/AssetEditActionItemResponseDto" }, - "minItems": 1, "type": "array" } }, @@ -17394,7 +17364,7 @@ "type": "array" }, "thumbhash": { - "description": "Thumbhash for thumbnail generation", + "description": "Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting.", "nullable": true, "type": "string" }, @@ -19061,6 +19031,22 @@ "data": { "$ref": "#/components/schemas/OnThisDayDto" }, + "hideAt": { + "description": "Date when memory should be hidden", + "format": "date-time", + "type": "string", + "x-immich-history": [ + { + "version": "v2.6.0", + "state": "Added" + }, + { + "version": "v2.6.0", + "state": "Stable" + } + ], + "x-immich-state": "Stable" + }, "isSaved": { "description": "Is memory saved", "type": "boolean" @@ -19075,6 +19061,22 @@ "format": "date-time", "type": "string" }, + "showAt": { + "description": "Date when memory should be shown", + "format": "date-time", + "type": "string", + "x-immich-history": [ + { + "version": "v2.6.0", + "state": "Added" + }, + { + "version": "v2.6.0", + "state": "Stable" + } + ], + "x-immich-state": "Stable" + }, "type": { "allOf": [ { @@ -23224,6 +23226,70 @@ ], "type": "object" }, + "SyncAssetFaceV2": { + "properties": { + "assetId": { + "description": "Asset ID", + "type": "string" + }, + "boundingBoxX1": { + "type": "integer" + }, + "boundingBoxX2": { + "type": "integer" + }, + "boundingBoxY1": { + "type": "integer" + }, + "boundingBoxY2": { + "type": "integer" + }, + "deletedAt": { + "description": "Face deleted at", + "format": "date-time", + "nullable": true, + "type": "string" + }, + "id": { + "description": "Asset face ID", + "type": "string" + }, + "imageHeight": { + "type": "integer" + }, + "imageWidth": { + "type": "integer" + }, + "isVisible": { + "description": "Is the face visible in the asset", + "type": "boolean" + }, + "personId": { + "description": "Person ID", + "nullable": true, + "type": "string" + }, + "sourceType": { + "description": "Source type", + "type": "string" + } + }, + "required": [ + "assetId", + "boundingBoxX1", + "boundingBoxX2", + "boundingBoxY1", + "boundingBoxY2", + "deletedAt", + "id", + "imageHeight", + "imageWidth", + "isVisible", + "personId", + "sourceType" + ], + "type": "object" + }, "SyncAssetMetadataDeleteV1": { "properties": { "assetId": { @@ -23517,6 +23583,7 @@ "PersonV1", "PersonDeleteV1", "AssetFaceV1", + "AssetFaceV2", "AssetFaceDeleteV1", "UserMetadataV1", "UserMetadataDeleteV1", @@ -23790,6 +23857,7 @@ "UsersV1", "PeopleV1", "AssetFacesV1", + "AssetFacesV2", "UserMetadataV1" ], "type": "string" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index ef60d90b6c..c58b42068a 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -630,7 +630,7 @@ export type AssetResponseDto = { resized?: boolean; stack?: (AssetStackResponseDto) | null; tags?: TagResponseDto[]; - /** Thumbhash for thumbnail generation */ + /** Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting. */ thumbhash: string | null; /** Asset type */ "type": AssetTypeEnum; @@ -973,38 +973,36 @@ export type CropParameters = { /** Top-Left Y coordinate of crop */ y: number; }; -export type AssetEditActionCrop = { - /** Type of edit action to perform */ - action: AssetEditAction; - parameters: CropParameters; -}; export type RotateParameters = { /** Rotation angle in degrees */ angle: number; }; -export type AssetEditActionRotate = { - /** Type of edit action to perform */ - action: AssetEditAction; - parameters: RotateParameters; -}; export type MirrorParameters = { /** Axis to mirror along */ axis: MirrorAxis; }; -export type AssetEditActionMirror = { +export type AssetEditActionItemResponseDto = { /** Type of edit action to perform */ action: AssetEditAction; - parameters: MirrorParameters; + id: string; + /** List of edit actions to apply (crop, rotate, or mirror) */ + parameters: CropParameters | RotateParameters | MirrorParameters; }; -export type AssetEditsDto = { - /** Asset ID to apply edits to */ +export type AssetEditsResponseDto = { + /** Asset ID these edits belong to */ assetId: string; - /** List of edit actions to apply (crop, rotate, or mirror) */ - edits: (AssetEditActionCrop | AssetEditActionRotate | AssetEditActionMirror)[]; + /** List of edit actions applied to the asset */ + edits: AssetEditActionItemResponseDto[]; }; -export type AssetEditActionListDto = { +export type AssetEditActionItemDto = { + /** Type of edit action to perform */ + action: AssetEditAction; /** List of edit actions to apply (crop, rotate, or mirror) */ - edits: (AssetEditActionCrop | AssetEditActionRotate | AssetEditActionMirror)[]; + parameters: CropParameters | RotateParameters | MirrorParameters; +}; +export type AssetEditsCreateDto = { + /** List of edit actions to apply (crop, rotate, or mirror) */ + edits: AssetEditActionItemDto[]; }; export type AssetMetadataResponseDto = { /** Metadata key */ @@ -1421,12 +1419,16 @@ export type MemoryCreateDto = { /** Asset IDs to associate with memory */ assetIds?: string[]; data: OnThisDayDto; + /** Date when memory should be hidden */ + hideAt?: string; /** Is memory saved */ isSaved?: boolean; /** Memory date */ memoryAt: string; /** Date when memory was seen */ seenAt?: string; + /** Date when memory should be shown */ + showAt?: string; /** Memory type */ "type": MemoryType; }; @@ -3069,6 +3071,26 @@ export type SyncAssetFaceV1 = { /** Source type */ sourceType: string; }; +export type SyncAssetFaceV2 = { + /** Asset ID */ + assetId: string; + boundingBoxX1: number; + boundingBoxX2: number; + boundingBoxY1: number; + boundingBoxY2: number; + /** Face deleted at */ + deletedAt: string | null; + /** Asset face ID */ + id: string; + imageHeight: number; + imageWidth: number; + /** Is the face visible in the asset */ + isVisible: boolean; + /** Person ID */ + personId: string | null; + /** Source type */ + sourceType: string; +}; export type SyncAssetMetadataDeleteV1 = { /** Asset ID */ assetId: string; @@ -4228,7 +4250,7 @@ export function getAssetEdits({ id }: { }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: AssetEditsDto; + data: AssetEditsResponseDto; }>(`/assets/${encodeURIComponent(id)}/edits`, { ...opts })); @@ -4236,17 +4258,17 @@ export function getAssetEdits({ id }: { /** * Apply edits to an existing asset */ -export function editAsset({ id, assetEditActionListDto }: { +export function editAsset({ id, assetEditsCreateDto }: { id: string; - assetEditActionListDto: AssetEditActionListDto; + assetEditsCreateDto: AssetEditsCreateDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: AssetEditsDto; + data: AssetEditsResponseDto; }>(`/assets/${encodeURIComponent(id)}/edits`, oazapfts.json({ ...opts, method: "PUT", - body: assetEditActionListDto + body: assetEditsCreateDto }))); } /** @@ -7367,6 +7389,7 @@ export enum SyncEntityType { PersonV1 = "PersonV1", PersonDeleteV1 = "PersonDeleteV1", AssetFaceV1 = "AssetFaceV1", + AssetFaceV2 = "AssetFaceV2", AssetFaceDeleteV1 = "AssetFaceDeleteV1", UserMetadataV1 = "UserMetadataV1", UserMetadataDeleteV1 = "UserMetadataDeleteV1", @@ -7394,6 +7417,7 @@ export enum SyncRequestType { UsersV1 = "UsersV1", PeopleV1 = "PeopleV1", AssetFacesV1 = "AssetFacesV1", + AssetFacesV2 = "AssetFacesV2", UserMetadataV1 = "UserMetadataV1" } export enum TranscodeHWAccel { diff --git a/package.json b/package.json index c50c4e1eb8..b49e12c3e9 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "2.5.6", "description": "Monorepo for Immich", "private": true, - "packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc", + "packageManager": "pnpm@10.30.0+sha512.2b5753de015d480eeb88f5b5b61e0051f05b4301808a82ec8b840c9d2adf7748eb352c83f5c1593ca703ff1017295bc3fdd3119abb9686efc96b9fcb18200937", "engines": { "pnpm": ">=10.0.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e8f0c84b3..b7568e0436 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: version: 4.0.8 devDependencies: '@eslint/js': - specifier: ^9.8.0 - version: 9.39.2 + specifier: ^10.0.0 + version: 10.0.1(eslint@10.0.2(jiti@2.6.1)) '@immich/sdk': specifier: workspace:* version: link:../open-api/typescript-sdk @@ -67,7 +67,7 @@ importers: version: 24.10.13 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) byte-size: specifier: ^9.0.0 version: 9.0.1 @@ -78,20 +78,20 @@ importers: specifier: ^12.0.0 version: 12.1.0 eslint: - specifier: ^9.14.0 - version: 9.39.2(jiti@2.6.1) + specifier: ^10.0.0 + version: 10.0.2(jiti@2.6.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) + version: 10.1.8(eslint@10.0.2(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1) + version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1))(prettier@3.8.1) eslint-plugin-unicorn: - specifier: ^62.0.0 - version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) + specifier: ^63.0.0 + version: 63.0.0(eslint@10.0.2(jiti@2.6.1)) globals: - specifier: ^16.0.0 - version: 16.5.0 + specifier: ^17.0.0 + version: 17.3.0 mock-fs: specifier: ^5.2.0 version: 5.5.0 @@ -106,19 +106,19 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.0.0 - version: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite-tsconfig-paths: specifier: ^6.0.0 - version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vitest-fetch-mock: specifier: ^0.4.0 - version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) yaml: specifier: ^2.3.1 version: 2.8.2 @@ -196,8 +196,8 @@ importers: e2e: devDependencies: '@eslint/js': - specifier: ^9.8.0 - version: 9.39.2 + specifier: ^10.0.0 + version: 10.0.1(eslint@10.0.2(jiti@2.6.1)) '@faker-js/faker': specifier: ^10.1.0 version: 10.3.0 @@ -233,25 +233,25 @@ importers: version: 6.0.3 dotenv: specifier: ^17.2.3 - version: 17.2.4 + version: 17.3.1 eslint: - specifier: ^9.14.0 - version: 9.39.2(jiti@2.6.1) + specifier: ^10.0.0 + version: 10.0.2(jiti@2.6.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) + version: 10.1.8(eslint@10.0.2(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1) + version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1))(prettier@3.8.1) eslint-plugin-unicorn: - specifier: ^62.0.0 - version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) + specifier: ^63.0.0 + version: 63.0.0(eslint@10.0.2(jiti@2.6.1)) exiftool-vendored: - specifier: ^34.3.0 - version: 34.3.0 + specifier: ^35.0.0 + version: 35.10.1 globals: - specifier: ^16.0.0 - version: 16.5.0 + specifier: ^17.0.0 + version: 17.3.0 luxon: specifier: ^3.4.4 version: 3.7.2 @@ -281,13 +281,13 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) utimes: specifier: ^5.2.1 version: 5.2.1(encoding@0.1.13) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) e2e-auth-server: devDependencies: @@ -299,7 +299,7 @@ importers: version: 5.10.0 oidc-provider: specifier: ^9.0.0 - version: 9.6.0 + version: 9.6.1 tsx: specifier: ^4.20.6 version: 4.21.0 @@ -343,60 +343,63 @@ importers: '@extism/extism': specifier: 2.0.0-rc13 version: 2.0.0-rc13 + '@immich/sql-tools': + specifier: ^0.2.0 + version: 0.2.0 '@nestjs/bullmq': specifier: ^11.0.1 - version: 11.0.4(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(bullmq@5.68.0) + version: 11.0.4(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(bullmq@5.69.3) '@nestjs/common': specifier: ^11.0.4 - version: 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.4 - version: 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^11.0.4 - version: 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) + version: 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) '@nestjs/platform-socket.io': specifier: ^11.0.4 - version: 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.13)(rxjs@7.8.2) + version: 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.14)(rxjs@7.8.2) '@nestjs/schedule': specifier: ^6.0.0 - version: 6.1.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) + version: 6.1.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) '@nestjs/swagger': specifier: ^11.0.2 - version: 11.2.6(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) + version: 11.2.6(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) '@nestjs/websockets': specifier: ^11.0.4 - version: 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-socket.io@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-socket.io@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.0 '@opentelemetry/context-async-hooks': specifier: ^2.0.0 - version: 2.5.0(@opentelemetry/api@1.9.0) + version: 2.5.1(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-prometheus': - specifier: ^0.211.0 - version: 0.211.0(@opentelemetry/api@1.9.0) + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-http': - specifier: ^0.211.0 - version: 0.211.0(@opentelemetry/api@1.9.0) + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-ioredis': - specifier: ^0.59.0 - version: 0.59.0(@opentelemetry/api@1.9.0) + specifier: ^0.60.0 + version: 0.60.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-nestjs-core': - specifier: ^0.57.0 - version: 0.57.0(@opentelemetry/api@1.9.0) + specifier: ^0.58.0 + version: 0.58.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-pg': - specifier: ^0.63.0 - version: 0.63.0(@opentelemetry/api@1.9.0) + specifier: ^0.64.0 + version: 0.64.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': specifier: ^2.0.1 - version: 2.5.0(@opentelemetry/api@1.9.0) + version: 2.5.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': specifier: ^2.0.1 - version: 2.5.0(@opentelemetry/api@1.9.0) + version: 2.5.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': - specifier: ^0.211.0 - version: 0.211.0(@opentelemetry/api@1.9.0) + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': specifier: ^1.34.0 version: 1.39.0 @@ -426,7 +429,7 @@ importers: version: 2.2.2 bullmq: specifier: ^5.51.0 - version: 5.68.0 + version: 5.69.3 chokidar: specifier: ^4.0.3 version: 4.0.3 @@ -449,8 +452,8 @@ importers: specifier: 4.4.0 version: 4.4.0 exiftool-vendored: - specifier: ^34.3.0 - version: 34.3.0 + specifier: ^35.0.0 + version: 35.10.1 express: specifier: ^5.1.0 version: 5.2.1 @@ -471,7 +474,7 @@ importers: version: 7.14.0 ioredis: specifier: ^5.8.2 - version: 5.9.2 + version: 5.9.3 jose: specifier: ^5.10.0 version: 5.10.0 @@ -501,16 +504,16 @@ importers: version: 2.0.2 nest-commander: specifier: ^3.16.0 - version: 3.20.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@types/inquirer@8.2.12)(@types/node@24.10.13)(typescript@5.9.3) + version: 3.20.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@types/inquirer@8.2.12)(@types/node@24.10.13)(typescript@5.9.3) nestjs-cls: specifier: ^5.0.0 - version: 5.4.3(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 5.4.3(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) nestjs-kysely: specifier: 3.1.2 - version: 3.1.2(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(kysely@0.28.2)(reflect-metadata@0.2.2) + version: 3.1.2(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(kysely@0.28.2)(reflect-metadata@0.2.2) nestjs-otel: specifier: ^7.0.0 - version: 7.0.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) + version: 7.0.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) nodemailer: specifier: ^7.0.0 version: 7.0.13 @@ -549,7 +552,7 @@ importers: version: 1.6.3 sanitize-html: specifier: ^2.14.0 - version: 2.17.0 + version: 2.17.1 semver: specifier: ^7.6.2 version: 7.7.4 @@ -582,8 +585,8 @@ importers: version: 13.15.26 devDependencies: '@eslint/js': - specifier: ^9.8.0 - version: 9.39.2 + specifier: ^10.0.0 + version: 10.0.1(eslint@10.0.2(jiti@2.6.1)) '@nestjs/cli': specifier: ^11.0.2 version: 11.0.16(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@24.10.13) @@ -592,7 +595,7 @@ importers: version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) '@nestjs/testing': specifier: ^11.0.4 - version: 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-express@11.1.13) + version: 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-express@11.1.14) '@swc/core': specifier: ^1.4.14 version: 1.15.11(@swc/helpers@0.5.17) @@ -643,7 +646,7 @@ importers: version: 24.10.13 '@types/nodemailer': specifier: ^7.0.0 - version: 7.0.9 + version: 7.0.10 '@types/picomatch': specifier: ^4.0.0 version: 4.0.2 @@ -670,22 +673,22 @@ importers: version: 13.15.10 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) eslint: - specifier: ^9.14.0 - version: 9.39.2(jiti@2.6.1) + specifier: ^10.0.0 + version: 10.0.2(jiti@2.6.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) + version: 10.1.8(eslint@10.0.2(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1) + version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1))(prettier@3.8.1) eslint-plugin-unicorn: - specifier: ^62.0.0 - version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) + specifier: ^63.0.0 + version: 63.0.0(eslint@10.0.2(jiti@2.6.1)) globals: - specifier: ^16.0.0 - version: 16.5.0 + specifier: ^17.0.0 + version: 17.3.0 mock-fs: specifier: ^5.2.0 version: 5.5.0 @@ -703,7 +706,7 @@ importers: version: 4.3.0(prettier@3.8.1)(typescript@5.9.3) sql-formatter: specifier: ^15.0.0 - version: 15.7.0 + version: 15.7.2 supertest: specifier: ^7.1.0 version: 7.2.2 @@ -712,22 +715,22 @@ importers: version: 3.4.19(tsx@4.21.0)(yaml@2.8.2) testcontainers: specifier: ^11.0.0 - version: 11.11.0 + version: 11.12.0 typescript: specifier: ^5.9.2 version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) unplugin-swc: specifier: ^1.4.5 version: 1.5.9(@swc/core@1.15.11(@swc/helpers@0.5.17))(rollup@4.55.1) vite-tsconfig-paths: specifier: ^6.0.0 - version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) web: dependencies: @@ -742,7 +745,7 @@ importers: version: link:../open-api/typescript-sdk '@immich/ui': specifier: ^0.64.0 - version: 0.64.0(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5) + version: 0.64.0(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0) '@mapbox/mapbox-gl-rtl-text': specifier: 0.3.0 version: 0.3.0 @@ -775,7 +778,7 @@ importers: version: 0.42.0 '@zoom-image/svelte': specifier: ^0.3.0 - version: 0.3.9(svelte@5.51.5) + version: 0.3.9(svelte@5.53.0) dom-to-image: specifier: ^2.6.0 version: 2.6.0 @@ -793,7 +796,7 @@ importers: version: 4.7.8 happy-dom: specifier: ^20.0.0 - version: 20.6.1 + version: 20.6.3 intl-messageformat: specifier: ^11.0.0 version: 11.1.2 @@ -826,16 +829,16 @@ importers: version: 5.2.2 svelte-i18n: specifier: ^4.0.1 - version: 4.0.1(svelte@5.51.5) + version: 4.0.1(svelte@5.53.0) svelte-jsoneditor: specifier: ^3.10.0 - version: 3.11.0(svelte@5.51.5) + version: 3.11.0(svelte@5.53.0) svelte-maplibre: specifier: ^1.2.5 - version: 1.2.6(svelte@5.51.5) + version: 1.2.6(svelte@5.53.0) svelte-persisted-store: specifier: ^0.12.0 - version: 0.12.0(svelte@5.51.5) + version: 0.12.0(svelte@5.53.0) tabbable: specifier: ^6.2.0 version: 6.4.0 @@ -850,38 +853,38 @@ importers: version: 1.6.32 devDependencies: '@eslint/js': - specifier: ^9.36.0 - version: 9.39.2 + specifier: ^10.0.0 + version: 10.0.1(eslint@10.0.2(jiti@2.6.1)) '@faker-js/faker': specifier: ^10.0.0 version: 10.3.0 '@koddsson/eslint-plugin-tscompat': specifier: ^0.2.0 - version: 0.2.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 0.2.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) '@socket.io/component-emitter': specifier: ^3.1.0 version: 3.1.2 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.10(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))) + version: 3.0.10(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))) '@sveltejs/enhanced-img': specifier: ^0.10.0 - version: 0.10.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 0.10.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@sveltejs/kit': specifier: ^2.27.1 - version: 2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@sveltejs/vite-plugin-svelte': specifier: 6.2.4 - version: 6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tailwindcss/vite': specifier: ^4.1.7 - version: 4.1.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.2.0(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@testing-library/jest-dom': specifier: ^6.4.2 version: 6.9.1 '@testing-library/svelte': specifier: ^5.2.8 - version: 5.3.1(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.3.1(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@testing-library/user-event': specifier: ^14.5.2 version: 14.6.1(@testing-library/dom@10.4.1) @@ -905,31 +908,31 @@ importers: version: 1.5.6 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) dotenv: specifier: ^17.0.0 - version: 17.2.4 + version: 17.3.1 eslint: - specifier: ^9.36.0 - version: 9.39.2(jiti@2.6.1) + specifier: ^10.0.0 + version: 10.0.2(jiti@2.6.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) + version: 10.1.8(eslint@10.0.2(jiti@2.6.1)) eslint-plugin-compat: specifier: ^6.0.2 - version: 6.1.0(eslint@9.39.2(jiti@2.6.1)) + version: 6.2.0(eslint@10.0.2(jiti@2.6.1)) eslint-plugin-svelte: specifier: ^3.12.4 - version: 3.15.0(eslint@9.39.2(jiti@2.6.1))(svelte@5.51.5) + version: 3.15.0(eslint@10.0.2(jiti@2.6.1))(svelte@5.53.0) eslint-plugin-unicorn: - specifier: ^62.0.0 - version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) + specifier: ^63.0.0 + version: 63.0.0(eslint@10.0.2(jiti@2.6.1)) factory.ts: specifier: ^1.4.1 version: 1.4.2 globals: - specifier: ^16.0.0 - version: 16.5.0 + specifier: ^17.0.0 + version: 17.3.0 prettier: specifier: ^3.7.4 version: 3.8.1 @@ -941,34 +944,34 @@ importers: version: 4.2.0(prettier@3.8.1) prettier-plugin-svelte: specifier: ^3.3.3 - version: 3.4.1(prettier@3.8.1)(svelte@5.51.5) + version: 3.5.0(prettier@3.8.1)(svelte@5.53.0) rollup-plugin-visualizer: specifier: ^6.0.0 version: 6.0.5(rollup@4.55.1) svelte: - specifier: 5.51.5 - version: 5.51.5 + specifier: 5.53.0 + version: 5.53.0 svelte-check: specifier: ^4.1.5 - version: 4.3.6(picomatch@4.0.3)(svelte@5.51.5)(typescript@5.9.3) + version: 4.4.1(picomatch@4.0.3)(svelte@5.53.0)(typescript@5.9.3) svelte-eslint-parser: specifier: ^1.3.3 - version: 1.4.1(svelte@5.51.5) + version: 1.4.1(svelte@5.53.0) tailwindcss: specifier: ^4.1.7 - version: 4.1.18 + version: 4.2.0 typescript: specifier: ^5.8.3 version: 5.9.3 typescript-eslint: specifier: ^8.45.0 - version: 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.1.2 - version: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages: @@ -2715,33 +2718,34 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.21.1': - resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-array@0.23.2': + resolution: {integrity: sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.5.2': + resolution: {integrity: sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@1.1.0': + resolution: {integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/eslintrc@3.3.3': - resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true - '@eslint/js@9.39.2': - resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@3.0.2': + resolution: {integrity: sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.6.0': + resolution: {integrity: sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@extism/extism@2.0.0-rc13': resolution: {integrity: sha512-iQ3mrPKOC0WMZ94fuJrKbJmMyz4LQ9Abf8gd4F5ShxKWa+cRKcVzk0EqRQsp5xXsQ2dO3zJTiA6eTc4Ihf7k+A==} @@ -3013,6 +3017,9 @@ packages: '@immich/justified-layout-wasm@0.4.3': resolution: {integrity: sha512-fpcQ7zPhP3Cp1bEXhONVYSUeIANa2uzaQFGKufUZQo5FO7aFT77szTVChhlCy4XaVy5R4ZvgSkA/1TJmeORz7Q==} + '@immich/sql-tools@0.2.0': + resolution: {integrity: sha512-AH0GRIUYrckNKuid5uO33vgRbGaznhRtArdQ91K310A1oUFjaoNzOaZyZhXwEmft3WYeC1bx4fdgUeois2QH5A==} + '@immich/svelte-markdown-preprocess@0.2.1': resolution: {integrity: sha512-mbr/g75lO8Zh+ELCuYrZP0XB4gf2UbK8rJcGYMYxFJJzMMunV+sm9FqtV1dbwW2dpXzCZGz1XPCEZ6oo526TbA==} peerDependencies: @@ -3278,8 +3285,8 @@ packages: resolution: {integrity: sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==} engines: {node: '>= 14.0.0'} - '@koa/router@15.1.0': - resolution: {integrity: sha512-0zCmuapmgBHrfVSFjBfCdgnkBnXwRGcG5qHnxVs8ZoTNEJiwSSspgJ5+2NugiqLJS/S0d96KMeNntLqTNWaioQ==} + '@koa/router@15.3.0': + resolution: {integrity: sha512-s87hWJjFYky2Z97u8jzah73sSHp4IZivD/2PZCuspHRvcKU69OPLoBIbKigVlBmS50yFTh9GHFfr1hDag4+wXw==} engines: {node: '>= 20'} peerDependencies: koa: ^2.0.0 || ^3.0.0 @@ -3287,6 +3294,9 @@ packages: '@koddsson/eslint-plugin-tscompat@0.2.0': resolution: {integrity: sha512-Oqd4kWSX0LiO9wWHjcmDfXZNC7TotFV/tLRhwCFU3XUeb//KYvJ75c9OmeSJ+vBv5lkCeB+xYsqyNrBc5j18XA==} + '@kwsites/file-exists@1.1.1': + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} @@ -3439,8 +3449,8 @@ packages: '@swc/core': optional: true - '@nestjs/common@11.1.13': - resolution: {integrity: sha512-ieqWtipT+VlyDWLz5Rvz0f3E5rXcVAnaAi+D53DEHLjc1kmFxCgZ62qVfTX2vwkywwqNkTNXvBgGR72hYqV//Q==} + '@nestjs/common@11.1.14': + resolution: {integrity: sha512-IN/tlqd7Nl9gl6f0jsWEuOrQDaCI9vHzxv0fisHysfBQzfQIkqlv5A7w4Qge02BUQyczXT9HHPgHtWHCxhjRng==} peerDependencies: class-transformer: '>=0.4.1' class-validator: '>=0.13.2' @@ -3452,8 +3462,8 @@ packages: class-validator: optional: true - '@nestjs/core@11.1.13': - resolution: {integrity: sha512-Tq9EIKiC30EBL8hLK93tNqaToy0hzbuVGYt29V8NhkVJUsDzlmiVf6c3hSPtzx2krIUVbTgQ2KFeaxr72rEyzQ==} + '@nestjs/core@11.1.14': + resolution: {integrity: sha512-7OXPPMoDr6z+5NkoQKu4hOhfjz/YYqM3bNilPqv1WVFWrzSmuNXxvhbX69YMmNmRYascPXiwESqf5jJdjKXEww==} engines: {node: '>= 20'} peerDependencies: '@nestjs/common': ^11.0.0 @@ -3483,14 +3493,14 @@ packages: class-validator: optional: true - '@nestjs/platform-express@11.1.13': - resolution: {integrity: sha512-LYmi43BrAs1n74kLCUfXcHag7s1CmGETcFbf9IVyA/KWXAuAH95G3wEaZZiyabOLFNwq4ifnRGnIwUwW7cz3+w==} + '@nestjs/platform-express@11.1.14': + resolution: {integrity: sha512-Fs+/j+mBSBSXErOQJ/YdUn/HqJGSJ4pGfiJyYOyz04l42uNVnqEakvu1kXLbxMabR6vd6/h9d6Bi4tso9p7o4Q==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 - '@nestjs/platform-socket.io@11.1.13': - resolution: {integrity: sha512-04Rh16IopZzHRXt0ZjFASqt9oNFV/0m0NsYe4kVOSaTEoef3cH7cTFpNpHsfNHcc4QpYL963XE8SvIRcZs5L8A==} + '@nestjs/platform-socket.io@11.1.14': + resolution: {integrity: sha512-LLSIWkYz4FcvUhfepillYQboo9qbjq1YtQj8XC3zyex+EaqNXvxhZntx/1uJhAjc655pJts9HfZwWXei8jrRGw==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/websockets': ^11.0.0 @@ -3524,8 +3534,8 @@ packages: class-validator: optional: true - '@nestjs/testing@11.1.13': - resolution: {integrity: sha512-bOWP8nLEZAOEEX8jAZGBCc1yU0+nv4g2ipc+QEzkVUe3eEEUKHKaeGafJ3GtDuGavlZKfkXEqflZuICdavu5dQ==} + '@nestjs/testing@11.1.14': + resolution: {integrity: sha512-cQxX0ronsTbpfHz8/LYOVWXxoTxv6VoxrnuZoQaVX7QV2PSMqxWE7/9jSQR0GcqAFUEmFP34c6EJqfkjfX/k4Q==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3537,8 +3547,8 @@ packages: '@nestjs/platform-express': optional: true - '@nestjs/websockets@11.1.13': - resolution: {integrity: sha512-8r8EadqBkrTYtH2uog42HfIb5fcP5a3iXymH/ityd9bO/gDson5Q1qbtCQRjuU++6NY12YYteKRu4eP/iErbLw==} + '@nestjs/websockets@11.1.14': + resolution: {integrity: sha512-fVP6RmmrmtLIitTXN9er7BUOIjjxcdIewN/zUtBlwgfng+qKBTxpNFOs3AXXbCu8bQr2xjzhjrBTfqri0Ske7w==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3581,94 +3591,94 @@ packages: '@oazapfts/runtime@1.2.0': resolution: {integrity: sha512-fi7dp7dNayyh/vzqhf0ZdoPfC7tJvYfjaE8MBL1yR+iIsH7cFoqHt+DV70VU49OMCqLc7wQa+yVJcSmIRnV4wA==} - '@opentelemetry/api-logs@0.211.0': - resolution: {integrity: sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==} + '@opentelemetry/api-logs@0.212.0': + resolution: {integrity: sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg==} engines: {node: '>=8.0.0'} '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@opentelemetry/configuration@0.211.0': - resolution: {integrity: sha512-PNsCkzsYQKyv8wiUIsH+loC4RYyblOaDnVASBtKS22hK55ToWs2UP6IsrcfSWWn54wWTvVe2gnfwz67Pvrxf2Q==} + '@opentelemetry/configuration@0.212.0': + resolution: {integrity: sha512-D8sAY6RbqMa1W8lCeiaSL2eMCW2MF87QI3y+I6DQE1j+5GrDMwiKPLdzpa/2/+Zl9v1//74LmooCTCJBvWR8Iw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.9.0 - '@opentelemetry/context-async-hooks@2.5.0': - resolution: {integrity: sha512-uOXpVX0ZjO7heSVjhheW2XEPrhQAWr2BScDPoZ9UDycl5iuHG+Usyc3AIfG6kZeC1GyLpMInpQ6X5+9n69yOFw==} + '@opentelemetry/context-async-hooks@2.5.1': + resolution: {integrity: sha512-MHbu8XxCHcBn6RwvCt2Vpn1WnLMNECfNKYB14LI5XypcgH4IE0/DiVifVR9tAkwPMyLXN8dOoPJfya3IryLQVw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@2.5.0': - resolution: {integrity: sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==} + '@opentelemetry/core@2.5.1': + resolution: {integrity: sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-logs-otlp-grpc@0.211.0': - resolution: {integrity: sha512-UhOoWENNqyaAMP/dL1YXLkXt6ZBtovkDDs1p4rxto9YwJX1+wMjwg+Obfyg2kwpcMoaiIFT3KQIcLNW8nNGNfQ==} + '@opentelemetry/exporter-logs-otlp-grpc@0.212.0': + resolution: {integrity: sha512-/0bk6fQG+eSFZ4L6NlckGTgUous/ib5+OVdg0x4OdwYeHzV3lTEo3it1HgnPY6UKpmX7ki+hJvxjsOql8rCeZA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-http@0.211.0': - resolution: {integrity: sha512-c118Awf1kZirHkqxdcF+rF5qqWwNjJh+BB1CmQvN9AQHC/DUIldy6dIkJn3EKlQnQ3HmuNRKc/nHHt5IusN7mA==} + '@opentelemetry/exporter-logs-otlp-http@0.212.0': + resolution: {integrity: sha512-JidJasLwG/7M9RTxV/64xotDKmFAUSBc9SNlxI32QYuUMK5rVKhHNWMPDzC7E0pCAL3cu+FyiKvsTwLi2KqPYw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-proto@0.211.0': - resolution: {integrity: sha512-kMvfKMtY5vJDXeLnwhrZMEwhZ2PN8sROXmzacFU/Fnl4Z79CMrOaL7OE+5X3SObRYlDUa7zVqaXp9ZetYCxfDQ==} + '@opentelemetry/exporter-logs-otlp-proto@0.212.0': + resolution: {integrity: sha512-RpKB5UVfxc7c6Ta1UaCrxXDTQ0OD7BCGT66a97Q5zR1x3+9fw4dSaiqMXT/6FAWj2HyFbem6Rcu1UzPZikGTWQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-grpc@0.211.0': - resolution: {integrity: sha512-D/U3G8L4PzZp8ot5hX9wpgbTymgtLZCiwR7heMe4LsbGV4OdctS1nfyvaQHLT6CiGZ6FjKc1Vk9s6kbo9SWLXQ==} + '@opentelemetry/exporter-metrics-otlp-grpc@0.212.0': + resolution: {integrity: sha512-/6Gqf9wpBq22XsomR1i0iPGnbQtCq2Vwnrq5oiDPjYSqveBdK1jtQbhGfmpK2mLLxk4cPDtD1ZEYdIou5K8EaA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-http@0.211.0': - resolution: {integrity: sha512-lfHXElPAoDSPpPO59DJdN5FLUnwi1wxluLTWQDayqrSPfWRnluzxRhD+g7rF8wbj1qCz0sdqABl//ug1IZyWvA==} + '@opentelemetry/exporter-metrics-otlp-http@0.212.0': + resolution: {integrity: sha512-8hgBw3aTTRpSTkU4b9MLf/2YVLnfWp+hfnLq/1Fa2cky+vx6HqTodo+Zv1GTIrAKMOOwgysOjufy0gTxngqeBg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-proto@0.211.0': - resolution: {integrity: sha512-61iNbffEpyZv/abHaz3BQM3zUtA2kVIDBM+0dS9RK68ML0QFLRGYa50xVMn2PYMToyfszEPEgFC3ypGae2z8FA==} + '@opentelemetry/exporter-metrics-otlp-proto@0.212.0': + resolution: {integrity: sha512-C7I4WN+ghn3g7SnxXm2RK3/sRD0k/BYcXaK6lGU3yPjiM7a1M25MLuM6zY3PeVPPzzTZPfuS7+wgn/tHk768Xw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-prometheus@0.211.0': - resolution: {integrity: sha512-cD0WleEL3TPqJbvxwz5MVdVJ82H8jl8mvMad4bNU24cB5SH2mRW5aMLDTuV4614ll46R//R3RMmci26mc2L99g==} + '@opentelemetry/exporter-prometheus@0.212.0': + resolution: {integrity: sha512-hJFLhCJba5MW5QHexZMHZdMhBfNqNItxOsN0AZojwD1W2kU9xM+BEICowFGJFo/vNV+I2BJvTtmuKafeDSAo7Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.211.0': - resolution: {integrity: sha512-eFwx4Gvu6LaEiE1rOd4ypgAiWEdZu7Qzm2QNN2nJqPW1XDeAVH1eNwVcVQl+QK9HR/JCDZ78PZgD7xD/DBDqbw==} + '@opentelemetry/exporter-trace-otlp-grpc@0.212.0': + resolution: {integrity: sha512-9xTuYWp8ClBhljDGAoa0NSsJcsxJsC9zCFKMSZJp1Osb9pjXCMRdA6fwXtlubyqe7w8FH16EWtQNKx/FWi+Ghw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-http@0.211.0': - resolution: {integrity: sha512-F1Rv3JeMkgS//xdVjbQMrI3+26e5SXC7vXA6trx8SWEA0OUhw4JHB+qeHtH0fJn46eFItrYbL5m8j4qi9Sfaxw==} + '@opentelemetry/exporter-trace-otlp-http@0.212.0': + resolution: {integrity: sha512-v/0wMozNoiEPRolzC4YoPo4rAT0q8r7aqdnRw3Nu7IDN0CGFzNQazkfAlBJ6N5y0FYJkban7Aw5WnN73//6YlA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-proto@0.211.0': - resolution: {integrity: sha512-DkjXwbPiqpcPlycUojzG2RmR0/SIK8Gi9qWO9znNvSqgzrnAIE9x2n6yPfpZ+kWHZGafvsvA1lVXucTyyQa5Kg==} + '@opentelemetry/exporter-trace-otlp-proto@0.212.0': + resolution: {integrity: sha512-d1ivqPT0V+i0IVOOdzGaLqonjtlk5jYrW7ItutWzXL/Mk+PiYb59dymy/i2reot9dDnBFWfrsvxyqdutGF5Vig==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-zipkin@2.5.0': - resolution: {integrity: sha512-bk9VJgFgUAzkZzU8ZyXBSWiUGLOM3mZEgKJ1+jsZclhRnAoDNf+YBdq+G9R3cP0+TKjjWad+vVrY/bE/vRR9lA==} + '@opentelemetry/exporter-zipkin@2.5.1': + resolution: {integrity: sha512-Me6JVO7WqXGXsgr4+7o+B7qwKJQbt0c8WamFnxpkR43avgG9k/niTntwCaXiXUTjonWy0+61ZuX6CGzj9nn8CQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -3679,62 +3689,62 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-http@0.211.0': - resolution: {integrity: sha512-n0IaQ6oVll9PP84SjbOCwDjaJasWRHi6BLsbMLiT6tNj7QbVOkuA5sk/EfZczwI0j5uTKl1awQPivO/ldVtsqA==} + '@opentelemetry/instrumentation-http@0.212.0': + resolution: {integrity: sha512-t2nt16Uyv9irgR+tqnX96YeToOStc3X5js7Ljn3EKlI2b4Fe76VhMkTXtsTQ0aId6AsYgefrCRnXSCo/Fn/vww==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-ioredis@0.59.0': - resolution: {integrity: sha512-875UxzBHWkW+P4Y45SoFM2AR8f8TzBMD8eO7QXGCyFSCUMP5s9vtt/BS8b/r2kqLyaRPK6mLbdnZznK3XzQWvw==} + '@opentelemetry/instrumentation-ioredis@0.60.0': + resolution: {integrity: sha512-R+nnbPD9l2ruzu248qM3YDWzpdmWVaFFFv08lQqsc0EP4pT/B1GGUg06/tHOSo3L5njB2eejwyzpkvJkjaQEMA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-nestjs-core@0.57.0': - resolution: {integrity: sha512-mzTjjethjuk70o/vWUeV12QwMG9EAFJpkn13/q8zi++sNosf2hoGXTplIdbs81U8S3PJ4GxHKsBjM0bj1CGZ0g==} + '@opentelemetry/instrumentation-nestjs-core@0.58.0': + resolution: {integrity: sha512-0lE9oW8j6nmvBHJoOxIQgKzMQQYNfX1nhiWZdXD0sNAMFsWBtvECWS7NAPSroKrEP53I04TcHCyyhcK4I9voXg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-pg@0.63.0': - resolution: {integrity: sha512-dKm/ODNN3GgIQVlbD6ZPxwRc3kleLf95hrRWXM+l8wYo+vSeXtEpQPT53afEf6VFWDVzJK55VGn8KMLtSve/cg==} + '@opentelemetry/instrumentation-pg@0.64.0': + resolution: {integrity: sha512-NbfB/rlfsRI3zpTjnbvJv3qwuoGLsN8FxR/XoI+ZTn1Rs62x1IenO+TSSvk4NO+7FlXpd2MiOe8LT/oNbydHGA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.211.0': - resolution: {integrity: sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==} + '@opentelemetry/instrumentation@0.212.0': + resolution: {integrity: sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-exporter-base@0.211.0': - resolution: {integrity: sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg==} + '@opentelemetry/otlp-exporter-base@0.212.0': + resolution: {integrity: sha512-HoMv5pQlzbuxiMS0hN7oiUtg8RsJR5T7EhZccumIWxYfNo/f4wFc7LPDfFK6oHdG2JF/+qTocfqIHoom+7kLpw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-grpc-exporter-base@0.211.0': - resolution: {integrity: sha512-mR5X+N4SuphJeb7/K7y0JNMC8N1mB6gEtjyTLv+TSAhl0ZxNQzpSKP8S5Opk90fhAqVYD4R0SQSAirEBlH1KSA==} + '@opentelemetry/otlp-grpc-exporter-base@0.212.0': + resolution: {integrity: sha512-YidOSlzpsun9uw0iyIWrQp6HxpMtBlECE3tiHGAsnpEqJWbAUWcMnIffvIuvTtTQ1OyRtwwaE79dWSQ8+eiB7g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-transformer@0.211.0': - resolution: {integrity: sha512-julhCJ9dXwkOg9svuuYqqjXLhVaUgyUvO2hWbTxwjvLXX2rG3VtAaB0SzxMnGTuoCZizBT7Xqqm2V7+ggrfCXA==} + '@opentelemetry/otlp-transformer@0.212.0': + resolution: {integrity: sha512-bj7zYFOg6Db7NUwsRZQ/WoVXpAf41WY2gsd3kShSfdpZQDRKHWJiRZIg7A8HvWsf97wb05rMFzPbmSHyjEl9tw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/propagator-b3@2.5.0': - resolution: {integrity: sha512-g10m4KD73RjHrSvUge+sUxUl8m4VlgnGc6OKvo68a4uMfaLjdFU+AULfvMQE/APq38k92oGUxEzBsAZ8RN/YHg==} + '@opentelemetry/propagator-b3@2.5.1': + resolution: {integrity: sha512-AU6sZgunZrZv/LTeHP+9IQsSSH5p3PtOfDPe8VTdwYH69nZCfvvvXehhzu+9fMW2mgJMh5RVpiH8M9xuYOu5Dg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-jaeger@2.5.0': - resolution: {integrity: sha512-t70ErZCncAR/zz5AcGkL0TF25mJiK1FfDPEQCgreyAHZ+mRJ/bNUiCnImIBDlP3mSDXy6N09DbUEKq0ktW98Hg==} + '@opentelemetry/propagator-jaeger@2.5.1': + resolution: {integrity: sha512-8+SB94/aSIOVGDUPRFSBRHVUm2A8ye1vC6/qcf/D+TF4qat7PC6rbJhRxiUGDXZtMtKEPM/glgv5cBGSJQymSg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -3743,38 +3753,38 @@ packages: resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==} engines: {node: ^18.19.0 || >=20.6.0} - '@opentelemetry/resources@2.5.0': - resolution: {integrity: sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==} + '@opentelemetry/resources@2.5.1': + resolution: {integrity: sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-logs@0.211.0': - resolution: {integrity: sha512-O5nPwzgg2JHzo59kpQTPUOTzFi0Nv5LxryG27QoXBciX3zWM3z83g+SNOHhiQVYRWFSxoWn1JM2TGD5iNjOwdA==} + '@opentelemetry/sdk-logs@0.212.0': + resolution: {integrity: sha512-qglb5cqTf0mOC1sDdZ7nfrPjgmAqs2OxkzOPIf2+Rqx8yKBK0pS7wRtB1xH30rqahBIut9QJDbDePyvtyqvH/Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' - '@opentelemetry/sdk-metrics@2.5.0': - resolution: {integrity: sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==} + '@opentelemetry/sdk-metrics@2.5.1': + resolution: {integrity: sha512-RKMn3QKi8nE71ULUo0g/MBvq1N4icEBo7cQSKnL3URZT16/YH3nSVgWegOjwx7FRBTrjOIkMJkCUn/ZFIEfn4A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-node@0.211.0': - resolution: {integrity: sha512-+s1eGjoqmPCMptNxcJJD4IxbWJKNLOQFNKhpwkzi2gLkEbCj6LzSHJNhPcLeBrBlBLtlSpibM+FuS7fjZ8SSFQ==} + '@opentelemetry/sdk-node@0.212.0': + resolution: {integrity: sha512-tJzVDk4Lo44MdgJLlP+gdYdMnjxSNsjC/IiTxj5CFSnsjzpHXwifgl3BpUX67Ty3KcdubNVfedeBc/TlqHXwwg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-base@2.5.0': - resolution: {integrity: sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==} + '@opentelemetry/sdk-trace-base@2.5.1': + resolution: {integrity: sha512-iZH3Gw8cxQn0gjpOjJMmKLd9GIaNh/E3v3ST67vyzLSxHBs14HsG4dy7jMYyC5WXGdBVEcM7U/XTF5hCQxjDMw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-node@2.5.0': - resolution: {integrity: sha512-O6N/ejzburFm2C84aKNrwJVPpt6HSTSq8T0ZUMq3xT2XmqT4cwxUItcL5UWGThYuq8RTcbH8u1sfj6dmRci0Ow==} + '@opentelemetry/sdk-trace-node@2.5.1': + resolution: {integrity: sha512-9lopQ6ZoElETOEN0csgmtEV5/9C7BMfA7VtF4Jape3i954b6sTY2k3Xw3CxUTKreDck/vpAuJM+EDo4zheUw+A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -3910,8 +3920,8 @@ packages: peerDependencies: '@photo-sphere-viewer/core': 5.14.1 - '@photostructure/tz-lookup@11.3.0': - resolution: {integrity: sha512-rYGy7ETBHTnXrwbzm47e3LJPKJmzpY7zXnbZhdosNU0lTGWVqzxptSjK4qZkJ1G+Kwy4F6XStNR9ZqMsXAoASQ==} + '@photostructure/tz-lookup@11.4.0': + resolution: {integrity: sha512-yrFaDbQQZVJIzpCTnoghWO8Rttu22Hg7/JkfP3CM8UKniXYzD80cuv4UAsFkzP5Z6XWceWNsQTqUJHKyGNXzLg==} '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} @@ -4310,8 +4320,8 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/enhanced-img@0.10.0': - resolution: {integrity: sha512-+nSrtNfs2dgKQ6RHMoKO6chl1QoO8JsuwKHkj9LkA2fUwzDYkeYoWvJzddOJIbgmowMdhi9cLo6tckSU+Kk7DQ==} + '@sveltejs/enhanced-img@0.10.2': + resolution: {integrity: sha512-HcIX7KFaLe+3ZD+GcMIlOGKODO8zb8p6I5tY8aoM9tz4GwueGyn9gILyTWZHqXYgg7PXto++ELB/q68wC9j4qw==} peerDependencies: '@sveltejs/vite-plugin-svelte': ^6.0.0 svelte: ^5.0.0 @@ -4512,69 +4522,69 @@ packages: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} - '@tailwindcss/node@4.1.18': - resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + '@tailwindcss/node@4.2.0': + resolution: {integrity: sha512-Yv+fn/o2OmL5fh/Ir62VXItdShnUxfpkMA4Y7jdeC8O81WPB8Kf6TT6GSHvnqgSwDzlB5iT7kDpeXxLsUS0T6Q==} - '@tailwindcss/oxide-android-arm64@4.1.18': - resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-android-arm64@4.2.0': + resolution: {integrity: sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw==} + engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.18': - resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-darwin-arm64@4.2.0': + resolution: {integrity: sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA==} + engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.18': - resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-darwin-x64@4.2.0': + resolution: {integrity: sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg==} + engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.18': - resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-freebsd-x64@4.2.0': + resolution: {integrity: sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw==} + engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': - resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.0': + resolution: {integrity: sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg==} + engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': - resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-arm64-gnu@4.2.0': + resolution: {integrity: sha512-Mff5a5Q3WoQR01pGU1gr29hHM1N93xYrKkGXfPw/aRtK4bOc331Ho4Tgfsm5WDGvpevqMpdlkCojT3qlCQbCpA==} + engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.1.18': - resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-arm64-musl@4.2.0': + resolution: {integrity: sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==} + engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.1.18': - resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-x64-gnu@4.2.0': + resolution: {integrity: sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==} + engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-x64-musl@4.1.18': - resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-x64-musl@4.2.0': + resolution: {integrity: sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==} + engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [musl] - '@tailwindcss/oxide-wasm32-wasi@4.1.18': - resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + '@tailwindcss/oxide-wasm32-wasi@4.2.0': + resolution: {integrity: sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -4585,24 +4595,24 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': - resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-win32-arm64-msvc@4.2.0': + resolution: {integrity: sha512-2UU/15y1sWDEDNJXxEIrfWKC2Yb4YgIW5Xz2fKFqGzFWfoMHWFlfa1EJlGO2Xzjkq/tvSarh9ZTjvbxqWvLLXA==} + engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.18': - resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-win32-x64-msvc@4.2.0': + resolution: {integrity: sha512-CrFadmFoc+z76EV6LPG1jx6XceDsaCG3lFhyLNo/bV9ByPrE+FnBPckXQVP4XRkN76h3Fjt/a+5Er/oA/nCBvQ==} + engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.18': - resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} - engines: {node: '>= 10'} + '@tailwindcss/oxide@4.2.0': + resolution: {integrity: sha512-AZqQzADaj742oqn2xjl5JbIOzZB/DGCYF/7bpvhA8KvjUj9HJkag6bBuwZvH1ps6dfgxNHyuJVlzSr2VpMgdTQ==} + engines: {node: '>= 20'} - '@tailwindcss/vite@4.1.18': - resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} + '@tailwindcss/vite@4.2.0': + resolution: {integrity: sha512-da9mFCaHpoOgtQiWtDGIikTrSpUFBtIZCG3jy/u2BGV+l/X1/pbxzmIUxNt6JWm19N3WtGi4KlJdSH/Si83WOA==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 @@ -4829,8 +4839,8 @@ packages: '@types/docker-modem@3.0.6': resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} - '@types/dockerode@3.3.47': - resolution: {integrity: sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==} + '@types/dockerode@4.0.1': + resolution: {integrity: sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==} '@types/dom-to-image@2.6.7': resolution: {integrity: sha512-me5VbCv+fcXozblWwG13krNBvuEOm6kA5xoa4RrjDJCNFOZSWR3/QLtOXimBHk1Fisq69Gx3JtOoXtg1N1tijg==} @@ -4841,6 +4851,9 @@ packages: '@types/eslint@9.6.1': resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -4982,11 +4995,11 @@ packages: '@types/node@24.10.13': resolution: {integrity: sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==} - '@types/node@25.2.3': - resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} + '@types/node@25.3.0': + resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==} - '@types/nodemailer@7.0.9': - resolution: {integrity: sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==} + '@types/nodemailer@7.0.10': + resolution: {integrity: sha512-tP+9WggTFN22Zxh0XFyst7239H0qwiRCogsk7v9aQS79sYAJY+WEbTHbNYcxUMaalHKmsNpxmoTe35hBEMMd6g==} '@types/oidc-provider@9.5.0': resolution: {integrity: sha512-eEzCRVTSqIHD9Bo/qRJ4XQWQ5Z/zBcG+Z2cGJluRsSuWx1RJihqRyPxhIEpMXTwPzHYRTQkVp7hwisQOwzzSAg==} @@ -5114,63 +5127,63 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.55.0': - resolution: {integrity: sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==} + '@typescript-eslint/eslint-plugin@8.56.0': + resolution: {integrity: sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.55.0 - eslint: ^8.57.0 || ^9.0.0 + '@typescript-eslint/parser': ^8.56.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.55.0': - resolution: {integrity: sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==} + '@typescript-eslint/parser@8.56.0': + resolution: {integrity: sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.55.0': - resolution: {integrity: sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==} + '@typescript-eslint/project-service@8.56.0': + resolution: {integrity: sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.55.0': - resolution: {integrity: sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==} + '@typescript-eslint/scope-manager@8.56.0': + resolution: {integrity: sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.55.0': - resolution: {integrity: sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==} + '@typescript-eslint/tsconfig-utils@8.56.0': + resolution: {integrity: sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.55.0': - resolution: {integrity: sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==} + '@typescript-eslint/type-utils@8.56.0': + resolution: {integrity: sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.55.0': - resolution: {integrity: sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==} + '@typescript-eslint/types@8.56.0': + resolution: {integrity: sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.55.0': - resolution: {integrity: sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==} + '@typescript-eslint/typescript-estree@8.56.0': + resolution: {integrity: sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.55.0': - resolution: {integrity: sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==} + '@typescript-eslint/utils@8.56.0': + resolution: {integrity: sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.55.0': - resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==} + '@typescript-eslint/visitor-keys@8.56.0': + resolution: {integrity: sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -5372,6 +5385,9 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} @@ -5570,6 +5586,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + bare-events@2.8.2: resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} peerDependencies: @@ -5578,8 +5598,8 @@ packages: bare-abort-controller: optional: true - bare-fs@4.5.2: - resolution: {integrity: sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw==} + bare-fs@4.5.4: + resolution: {integrity: sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==} engines: {bare: '>=1.16.0'} peerDependencies: bare-buffer: '*' @@ -5594,8 +5614,8 @@ packages: bare-path@3.0.0: resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} - bare-stream@2.7.0: - resolution: {integrity: sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==} + bare-stream@2.8.0: + resolution: {integrity: sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==} peerDependencies: bare-buffer: '*' bare-events: '*' @@ -5619,8 +5639,8 @@ packages: resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true - batch-cluster@16.0.0: - resolution: {integrity: sha512-+T7Ho09ikx/kP4P8M+GEnpuePzRQa4gTUhtPIu6ApFC8+0GY0sri1y1PuB+yfXlQWl5DkHC/e58z3U6g0qCz/A==} + batch-cluster@17.3.1: + resolution: {integrity: sha512-/aWEgZKXgvEseV3WEIRyjDoFka9FTrpt5+FYCxn+giUgveGBKxWjz3cl26V3aD+1kvOBP3nmANZZfcXDmKzcAA==} engines: {node: '>=20'} batch@0.6.1: @@ -5681,6 +5701,10 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.3: + resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -5714,8 +5738,8 @@ packages: resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} engines: {node: '>=18.20'} - bullmq@5.68.0: - resolution: {integrity: sha512-PywC7eTcPrKVQN5iEfhs5ats90nSLr8dzsyIhgviO8qQRTHnTq/SnETq2E8Do1RLg7Qw1Q0p5htBPI/cUGAlHg==} + bullmq@5.69.3: + resolution: {integrity: sha512-P9uLsR7fDvejH/1m6uur6j7U9mqY6nNt+XvhlhStOUe7jdwbZoP/c2oWNtE+8ljOlubw4pRUKymtRqkyvloc4A==} bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} @@ -5800,8 +5824,8 @@ packages: caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-lite@1.0.30001769: - resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} + caniuse-lite@1.0.30001774: + resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==} canvas@2.11.2: resolution: {integrity: sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==} @@ -5897,8 +5921,8 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} - cjs-module-lexer@1.4.3: - resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} @@ -6712,8 +6736,8 @@ packages: resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} engines: {node: '>=6'} - docker-compose@1.3.0: - resolution: {integrity: sha512-7Gevk/5eGD50+eMD+XDnFnOrruFkL0kSd7jEG4cjmqweDSUhB7i0g8is/nBdVpl+Bx338SqIB2GLKm32M+Vs6g==} + docker-compose@1.3.1: + resolution: {integrity: sha512-rF0wH69G3CCcmkN9J1RVMQBaKe8o77LT/3XmqcLIltWWVxcWAzp2TnO7wS3n/umZHN3/EVrlT3exSBMal+Ou1w==} engines: {node: '>= 6.0.0'} docker-modem@5.0.6: @@ -6777,8 +6801,8 @@ packages: resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} engines: {node: '>=10'} - dotenv@17.2.4: - resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==} + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} engines: {node: '>=12'} dunder-proto@1.0.1: @@ -6858,6 +6882,10 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -6954,11 +6982,11 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-compat@6.1.0: - resolution: {integrity: sha512-xiwHz7mj6+Zj7NWOO/uaWdrQ6zP0zL5CPyKVCNlB4JaoUFeYPYwejf5toqyHGlXzhuPUdCpg31uBRiWqcgiS0A==} + eslint-plugin-compat@6.2.0: + resolution: {integrity: sha512-Ihz4zAeHKzyksDDUTObrYQxaqnV/pFlAiZoWkMuWM9XGf4O191ReQFYv516zcs9QVJ2vX+MMpqr1yEfTkXVETQ==} engines: {node: '>=18.x'} peerDependencies: - eslint: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + eslint: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 eslint-plugin-prettier@5.5.5: resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} @@ -6984,8 +7012,8 @@ packages: svelte: optional: true - eslint-plugin-unicorn@62.0.0: - resolution: {integrity: sha512-HIlIkGLkvf29YEiS/ImuDZQbP12gWyx5i3C6XrRxMvVdqMroCI9qoVYCoIl17ChN+U89pn9sVwLxhIWj5nEc7g==} + eslint-plugin-unicorn@63.0.0: + resolution: {integrity: sha512-Iqecl9118uQEXYh7adylgEmGfkn5es3/mlQTLLkd4pXkIk9CTGrAbeUux+YljSa2ohXCBmQQ0+Ej1kZaFgcfkA==} engines: {node: ^20.10.0 || >=21.0.0} peerDependencies: eslint: '>=9.38.0' @@ -6998,6 +7026,10 @@ packages: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-scope@9.1.1: + resolution: {integrity: sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7006,9 +7038,13 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.39.2: - resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.0.2: + resolution: {integrity: sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: jiti: '*' @@ -7027,13 +7063,17 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + espree@11.1.1: + resolution: {integrity: sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} esrap@2.2.3: @@ -7086,8 +7126,8 @@ packages: resolution: {integrity: sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==} engines: {node: '>=6.0.0'} - eta@4.5.0: - resolution: {integrity: sha512-qifAYjuW5AM1eEEIsFnOwB+TGqu6ynU3OKj9WbUTOtUBHFPZqL03XUW34kbp3zm19Ald+U8dEyRXaVsUck+Y1g==} + eta@4.5.1: + resolution: {integrity: sha512-EaNCGm+8XEIU7YNcc+THptWAO5NfKBHHARxt+wxZljj9bTr/+arRoOm9/MpGt4n6xn9fLnPFRSoLD0WFYGFUxQ==} engines: {node: '>=20'} etag@1.8.1: @@ -7123,17 +7163,17 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} - exiftool-vendored.exe@13.45.0: - resolution: {integrity: sha512-xa+gEnZ2Q9BAzaDr35xgADql+T6L92RqK0GjzOjzDuObwhr+sBr5RdySvZ3osHac9GJypxvk4cewNnj4OnPL3Q==} + exiftool-vendored.exe@13.51.0: + resolution: {integrity: sha512-Q49J2c4e+XSGYDJf9PYMVI/IUfUkHLRsPUeDJ2ZekEBVLuw2g7ye9x0vQGWZKwEeZTlnXol7SeBJB0wtAmzM9w==} os: [win32] - exiftool-vendored.pl@13.45.0: - resolution: {integrity: sha512-uA58bMcXqdSQAqsZbHa/SMU6XKXsmoMcJSlKJjsCmLlQKEThncuAlpg8wGVNhULNXxYmRXXnYQ1756UYQY9VIA==} + exiftool-vendored.pl@13.51.0: + resolution: {integrity: sha512-RhDM10w4kv5YNCvECj0aLXZXi0UWyzVo2OS4P/hpmyCHL+NGCkZ6N9z/Yc3ek0cEfCj4AiLhe8C96pnz/Fw9Yg==} os: ['!win32'] hasBin: true - exiftool-vendored@34.3.0: - resolution: {integrity: sha512-CpNH1FAhIQG5AlKndlTf05mNbuFxINyzG9629ZI/CKwr+39zWo8swxpnXc3GUfUvUfxkCCxumDPy2QVmi3XJkQ==} + exiftool-vendored@35.10.1: + resolution: {integrity: sha512-orD61HdNcdlegfD80wI+3JE/n+iobYPztpFqv2drLHb1rb2QEKR1QY62r+O0wZHHNIf3Bje+xjweS1hxWignQA==} engines: {node: '>=20.0.0'} expect-type@1.3.0: @@ -7494,10 +7534,6 @@ packages: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - globals@15.15.0: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} @@ -7506,6 +7542,10 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} + globals@17.3.0: + resolution: {integrity: sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==} + engines: {node: '>=18'} + globalyzer@0.1.0: resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} @@ -7553,8 +7593,8 @@ packages: engines: {node: '>=0.4.7'} hasBin: true - happy-dom@20.6.1: - resolution: {integrity: sha512-+0vhESXXhFwkdjZnJ5DlmJIfUYGgIEEjzIjB+aKJbFuqlvvKyOi+XkI1fYbgYR9QCxG5T08koxsQ6HrQfa5gCQ==} + happy-dom@20.6.3: + resolution: {integrity: sha512-QAMY7d228dHs8gb9NG4SJ3OxQo4r+NGN8pOXGZ3SGfQf/XYuuYubrtZ25QVY2WoUQdskhRXSXb4R4mcRk+hV1w==} engines: {node: '>=20.0.0'} has-flag@4.0.0: @@ -7818,8 +7858,8 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - import-in-the-middle@2.0.0: - resolution: {integrity: sha512-yNZhyQYqXpkT0AKq3F3KLasUSK4fHvebNH5hOsKQw2dhGSALvQ4U0BqUc5suziKvydO5u5hgN2hy1RJaho8U5A==} + import-in-the-middle@2.0.6: + resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} @@ -7885,6 +7925,10 @@ packages: resolution: {integrity: sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==} engines: {node: '>=12.22.0'} + ioredis@5.9.3: + resolution: {integrity: sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==} + engines: {node: '>=12.22.0'} + ip-address@10.1.0: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} @@ -8291,6 +8335,10 @@ packages: postgres: optional: true + kysely@0.28.11: + resolution: {integrity: sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg==} + engines: {node: '>=20.0.0'} + kysely@0.28.2: resolution: {integrity: sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A==} engines: {node: '>=18.0.0'} @@ -8330,78 +8378,78 @@ packages: libphonenumber-js@1.12.31: resolution: {integrity: sha512-Z3IhgVgrqO1S5xPYM3K5XwbkDasU67/Vys4heW+lfSBALcUZjeIIzI8zCLifY+OCzSq+fpDdywMDa7z+4srJPQ==} - lightningcss-android-arm64@1.30.2: - resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] - lightningcss-darwin-arm64@1.30.2: - resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.30.2: - resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.30.2: - resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.30.2: - resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.30.2: - resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] libc: [glibc] - lightningcss-linux-arm64-musl@1.30.2: - resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] libc: [musl] - lightningcss-linux-x64-gnu@1.30.2: - resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] libc: [glibc] - lightningcss-linux-x64-musl@1.30.2: - resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] libc: [musl] - lightningcss-win32-arm64-msvc@1.30.2: - resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.30.2: - resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.30.2: - resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} engines: {node: '>= 12.0.0'} lilconfig@2.1.0: @@ -8485,9 +8533,6 @@ packages: lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} @@ -8914,6 +8959,10 @@ packages: resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} engines: {node: 20 || >=22} + minimatch@10.2.2: + resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -8921,8 +8970,8 @@ packages: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + minimatch@9.0.6: + resolution: {integrity: sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==} engines: {node: '>=16 || 14 >=14.17'} minimist@1.2.8: @@ -8984,6 +9033,11 @@ packages: engines: {node: '>=10'} hasBin: true + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -9042,8 +9096,8 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nan@2.24.0: - resolution: {integrity: sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==} + nan@2.25.0: + resolution: {integrity: sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==} nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} @@ -9258,8 +9312,8 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - oidc-provider@9.6.0: - resolution: {integrity: sha512-CCRUYPOumEy/DT+L86H40WgXjXfDHlsJYZdyd4ZKGFxJh/kAd7DxMX3dwpbX0g+WjB+NWU+kla1b/yZmHNcR0Q==} + oidc-provider@9.6.1: + resolution: {integrity: sha512-8AtFXE4gEV6MLd8Re78VhqGNjBm/SUw0fUxrP2XwQc+5DZKw6GyuTuy2M4jkidpH3jRrhtkkqQpXlxD1Awi6tg==} on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} @@ -10084,8 +10138,8 @@ packages: peerDependencies: prettier: ^3.0.0 - prettier-plugin-svelte@3.4.1: - resolution: {integrity: sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg==} + prettier-plugin-svelte@3.5.0: + resolution: {integrity: sha512-2lLO/7EupnjO/95t+XZesXs8Bf3nYLIDfCo270h5QWbj/vjLqmrQ1LiRk9LPggxSDsnVYfehamZNf+rgQYApZg==} peerDependencies: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 @@ -10140,9 +10194,9 @@ packages: proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} - properties-reader@2.3.0: - resolution: {integrity: sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==} - engines: {node: '>=14'} + properties-reader@3.0.1: + resolution: {integrity: sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==} + engines: {node: '>=18'} property-information@5.6.0: resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} @@ -10594,8 +10648,8 @@ packages: sanitize-filename@1.6.3: resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} - sanitize-html@2.17.0: - resolution: {integrity: sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==} + sanitize-html@2.17.1: + resolution: {integrity: sha512-ehFCW+q1a4CSOWRAdX97BX/6/PDEkCqw7/0JXZAGQV57FQB3YOkTa/rrzHPeJ+Aghy4vZAFfWMYyfxIiB7F/gw==} sass@1.97.1: resolution: {integrity: sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==} @@ -10651,11 +10705,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -10880,8 +10929,8 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sql-formatter@15.7.0: - resolution: {integrity: sha512-o2yiy7fYXK1HvzA8P6wwj8QSuwG3e/XcpWht/jIxkQX99c0SVPw0OXdLSV9fHASPiYB09HLA0uq8hokGydi/QA==} + sql-formatter@15.7.2: + resolution: {integrity: sha512-b0BGoM81KFRVSpZFwPpIPU5gng4YD8DI/taLD96NXCFRf5af3FzSE4aSwjKmxcyTmf/MfPu91j75883nRrWDBw==} hasBin: true srcset@4.0.0: @@ -11049,8 +11098,8 @@ packages: peerDependencies: svelte: '>= 3.43.1 < 6' - svelte-check@4.3.6: - resolution: {integrity: sha512-uBkz96ElE3G4pt9E1Tw0xvBfIUQkeH794kDQZdAUk795UVMr+NJZpuFSS62vcmO/DuSalK83LyOwhgWq8YGU1Q==} + svelte-check@4.4.1: + resolution: {integrity: sha512-y1bBT0CRCMMfdjyqX1e5zCygLgEEr4KJV1qP6GSUReHl90bmcQaAWjZygHPfQ8K63f1eR8IuivuZMwmCg3zT2Q==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: @@ -11122,8 +11171,8 @@ packages: peerDependencies: svelte: ^5.30.2 - svelte@5.51.5: - resolution: {integrity: sha512-/4tR5cLsWOgH3wnNRXnFoWaJlwPGbJanZPSKSD6nHM2y01dvXeEF4Nx7jevoZ+UpJpkIHh6mY2tqDncuI4GHng==} + svelte@5.53.0: + resolution: {integrity: sha512-7dhHkSamGS2vtoBmIW2hRab+gl5Z60alEHZB4910ePqqJNxAWnDAxsofVmlZ2tREmWyHNE+A1nCKwICAquoD2A==} engines: {node: '>=18'} svg-parser@2.0.4: @@ -11197,8 +11246,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tailwindcss@4.1.18: - resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + tailwindcss@4.2.0: + resolution: {integrity: sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q==} tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} @@ -11227,6 +11276,9 @@ packages: engines: {node: '>=18'} deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + terser-webpack-plugin@5.3.16: resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} engines: {node: '>= 10.13.0'} @@ -11252,8 +11304,8 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} - testcontainers@11.11.0: - resolution: {integrity: sha512-nKTJn3n/gkyGg/3SVkOwX+isPOGSHlfI+CWMobSmvQrsj7YW01aWvl2pYIfV4LMd+C8or783yYrzKSK2JlP+Qw==} + testcontainers@11.12.0: + resolution: {integrity: sha512-VWtH+UQejVYYvb53ohEZRbx2naxyDvwO9lQ6A0VgmVE2Oh8r9EF09I+BfmrXpd9N9ntpzhao9di2yNwibSz5KA==} text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} @@ -11477,11 +11529,11 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript-eslint@8.55.0: - resolution: {integrity: sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==} + typescript-eslint@8.56.0: + resolution: {integrity: sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' typescript@5.9.3: @@ -11522,8 +11574,11 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@7.18.0: - resolution: {integrity: sha512-CfPufgPFHCYu0W4h1NiKW9+tNJ39o3kWm7Cm29ET1enSJx+AERfz7A2wAr26aY0SZbYzZlTBQtcHy15o60VZfQ==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + undici@7.22.0: + resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} engines: {node: '>=20.18.1'} unicode-canonical-property-names-ecmascript@2.0.1: @@ -11717,8 +11772,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-imagetools@9.0.2: - resolution: {integrity: sha512-FV5DXw4swU81t+g8JOLT+T7gKuBOXuVsZ0WGhi7y0R182+GfBYkcf6V9/T0Nweu/vn1X0DA2p5ePMnaGZlRl1A==} + vite-imagetools@9.0.3: + resolution: {integrity: sha512-FwjApRNZyN+RucPW9Z9kf0dyzyi3r3zlDfrTnzHXNaYpmT3pZ5w//d6QkApy1iypbDm+3fq+Gwfv+PYA4j4uYw==} engines: {node: '>=20.0.0'} vite-node@3.2.4: @@ -14531,50 +14586,38 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.0.2(jiti@2.6.1))': dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.2(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.1': + '@eslint/config-array@0.23.2': dependencies: - '@eslint/object-schema': 2.1.7 + '@eslint/object-schema': 3.0.2 debug: 4.4.3 - minimatch: 3.1.2 + minimatch: 10.2.2 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.4.2': + '@eslint/config-helpers@0.5.2': dependencies: - '@eslint/core': 0.17.0 + '@eslint/core': 1.1.0 - '@eslint/core@0.17.0': + '@eslint/core@1.1.0': dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.3': + '@eslint/js@10.0.1(eslint@10.0.2(jiti@2.6.1))': + optionalDependencies: + eslint: 10.0.2(jiti@2.6.1) + + '@eslint/object-schema@3.0.2': {} + + '@eslint/plugin-kit@0.6.0': dependencies: - ajv: 6.12.6 - debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.39.2': {} - - '@eslint/object-schema@2.1.7': {} - - '@eslint/plugin-kit@0.4.1': - dependencies: - '@eslint/core': 0.17.0 + '@eslint/core': 1.1.0 levn: 0.4.1 '@extism/extism@2.0.0-rc13': {} @@ -14664,10 +14707,10 @@ snapshots: dependencies: '@fortawesome/fontawesome-common-types': 7.1.0 - '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)': + '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)': dependencies: - '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) lodash: 4.17.23 '@grpc/grpc-js@1.14.3': @@ -14812,26 +14855,33 @@ snapshots: '@immich/justified-layout-wasm@0.4.3': {} - '@immich/svelte-markdown-preprocess@0.2.1(svelte@5.51.5)': + '@immich/sql-tools@0.2.0': + dependencies: + kysely: 0.28.11 + kysely-postgres-js: 3.0.0(kysely@0.28.11)(postgres@3.4.8) + pg-connection-string: 2.11.0 + postgres: 3.4.8 + + '@immich/svelte-markdown-preprocess@0.2.1(svelte@5.53.0)': dependencies: front-matter: 4.0.2 marked: 17.0.3 node-emoji: 2.2.0 - svelte: 5.51.5 + svelte: 5.53.0 - '@immich/ui@0.64.0(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)': + '@immich/ui@0.64.0(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)': dependencies: - '@immich/svelte-markdown-preprocess': 0.2.1(svelte@5.51.5) + '@immich/svelte-markdown-preprocess': 0.2.1(svelte@5.53.0) '@internationalized/date': 3.10.0 '@mdi/js': 7.4.47 - bits-ui: 2.16.0(@internationalized/date@3.10.0)(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5) + bits-ui: 2.16.0(@internationalized/date@3.10.0)(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0) luxon: 3.7.2 simple-icons: 16.9.0 - svelte: 5.51.5 + svelte: 5.53.0 svelte-highlight: 7.9.0 tailwind-merge: 3.4.0 - tailwind-variants: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18) - tailwindcss: 4.1.18 + tailwind-variants: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.2.0) + tailwindcss: 4.2.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -15091,7 +15141,7 @@ snapshots: dependencies: vary: 1.1.2 - '@koa/router@15.1.0(koa@3.1.1)': + '@koa/router@15.3.0(koa@3.1.1)': dependencies: debug: 4.4.3 http-errors: 2.0.1 @@ -15101,17 +15151,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@koddsson/eslint-plugin-tscompat@0.2.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@koddsson/eslint-plugin-tscompat@0.2.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@mdn/browser-compat-data': 6.1.5 - '@typescript-eslint/type-utils': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) browserslist: 4.28.1 transitivePeerDependencies: - eslint - supports-color - typescript + '@kwsites/file-exists@1.1.1': + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@leichtgewicht/ip-codec@2.0.5': {} '@lezer/common@1.5.0': {} @@ -15286,18 +15342,18 @@ snapshots: '@namnode/store@0.1.0': {} - '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)': + '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)': dependencies: - '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(bullmq@5.68.0)': + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(bullmq@5.69.3)': dependencies: - '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) - '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - bullmq: 5.68.0 + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) + bullmq: 5.69.3 tslib: 2.8.1 '@nestjs/cli@11.0.16(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@24.10.13)': @@ -15328,7 +15384,7 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: file-type: 21.3.0 iterare: 1.2.1 @@ -15343,9 +15399,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/core@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -15355,21 +15411,21 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) - '@nestjs/websockets': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-socket.io@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) + '@nestjs/websockets': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-socket.io@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.14.3 - '@nestjs/platform-express@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)': + '@nestjs/platform-express@11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)': dependencies: - '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) cors: 2.8.6 express: 5.2.1 multer: 2.0.2 @@ -15378,10 +15434,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/platform-socket.io@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.13)(rxjs@7.8.2)': + '@nestjs/platform-socket.io@11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.14)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/websockets': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-socket.io@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/websockets': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-socket.io@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) rxjs: 7.8.2 socket.io: 4.8.3 tslib: 2.8.1 @@ -15390,10 +15446,10 @@ snapshots: - supports-color - utf-8-validate - '@nestjs/schedule@6.1.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)': + '@nestjs/schedule@6.1.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)': dependencies: - '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) cron: 4.4.0 '@nestjs/schematics@11.0.9(chokidar@4.0.3)(typescript@5.9.3)': @@ -15407,12 +15463,12 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.2.6(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': + '@nestjs/swagger@11.2.6(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.16.0 - '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) js-yaml: 4.1.1 lodash: 4.17.23 path-to-regexp: 8.3.0 @@ -15422,25 +15478,25 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.3 - '@nestjs/testing@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-express@11.1.13)': + '@nestjs/testing@11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-express@11.1.14)': dependencies: - '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) + '@nestjs/platform-express': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) - '@nestjs/websockets@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-socket.io@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/websockets@11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-socket.io@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) iterare: 1.2.1 object-hash: 3.0.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-socket.io': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.13)(rxjs@7.8.2) + '@nestjs/platform-socket.io': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.14)(rxjs@7.8.2) '@noble/hashes@1.8.0': {} @@ -15476,130 +15532,131 @@ snapshots: '@oazapfts/runtime@1.2.0': {} - '@opentelemetry/api-logs@0.211.0': + '@opentelemetry/api-logs@0.212.0': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/api@1.9.0': {} - '@opentelemetry/configuration@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/configuration@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) yaml: 2.8.2 - '@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.39.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-grpc@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.212.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-http@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.211.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.212.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-proto@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.211.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-grpc@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-http@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-proto@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-prometheus@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-grpc@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-http@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-proto@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin@2.5.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-zipkin@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.39.0 '@opentelemetry/host-metrics@0.36.2(@opentelemetry/api@1.9.0)': @@ -15607,38 +15664,38 @@ snapshots: '@opentelemetry/api': 1.9.0 systeminformation: 5.23.8 - '@opentelemetry/instrumentation-http@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-http@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.39.0 forwarded-parse: 2.1.2 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-ioredis@0.59.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-ioredis@0.60.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.38.2 '@opentelemetry/semantic-conventions': 1.39.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-nestjs-core@0.57.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-nestjs-core@0.58.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.39.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-pg@0.63.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-pg@0.64.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.39.0 '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) '@types/pg': 8.15.6 @@ -15646,121 +15703,121 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.211.0 - import-in-the-middle: 2.0.0 + '@opentelemetry/api-logs': 0.212.0 + import-in-the-middle: 2.0.6 require-in-the-middle: 8.0.1 transitivePeerDependencies: - supports-color - '@opentelemetry/otlp-exporter-base@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-exporter-base@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-grpc-exporter-base@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-transformer@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.211.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) protobufjs: 8.0.0 - '@opentelemetry/propagator-b3@2.5.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/propagator-b3@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger@2.5.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/propagator-jaeger@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common@0.38.2': {} - '@opentelemetry/resources@2.5.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/resources@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.39.0 - '@opentelemetry/sdk-logs@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-logs@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.211.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics@2.5.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-metrics@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node@0.211.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-node@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.211.0 - '@opentelemetry/configuration': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/context-async-hooks': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-grpc': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-b3': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/configuration': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.5.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.39.0 transitivePeerDependencies: - supports-color - '@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.39.0 - '@opentelemetry/sdk-trace-node@2.5.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-node@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions@1.39.0': {} '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) '@paralleldrive/cuid2@2.3.1': dependencies: @@ -15855,7 +15912,7 @@ snapshots: '@photo-sphere-viewer/core': 5.14.1 three: 0.182.0 - '@photostructure/tz-lookup@11.3.0': {} + '@photostructure/tz-lookup@11.4.0': {} '@pkgjs/parseargs@0.11.0': optional: true @@ -16160,29 +16217,29 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))': dependencies: - '@sveltejs/kit': 2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/kit': 2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) - '@sveltejs/enhanced-img@0.10.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/enhanced-img@0.10.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) magic-string: 0.30.21 sharp: 0.34.5 - svelte: 5.51.5 - svelte-parse-markup: 0.1.5(svelte@5.51.5) - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vite-imagetools: 9.0.2(rollup@4.55.1) + svelte: 5.53.0 + svelte-parse-markup: 0.1.5(svelte@5.53.0) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-imagetools: 9.0.3(rollup@4.55.1) zimmerframe: 1.1.4 transitivePeerDependencies: - rollup - supports-color - '@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@types/cookie': 0.6.0 acorn: 8.16.0 cookie: 0.6.0 @@ -16193,30 +16250,30 @@ snapshots: mrmime: 2.0.1 set-cookie-parser: 3.0.1 sirv: 3.0.2 - svelte: 5.51.5 - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + svelte: 5.53.0 + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) optionalDependencies: '@opentelemetry/api': 1.9.0 typescript: 5.9.3 - '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) debug: 4.4.3 - svelte: 5.51.5 - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + svelte: 5.53.0 + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 - svelte: 5.51.5 - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vitefu: 1.1.1(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + svelte: 5.53.0 + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitefu: 1.1.1(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color @@ -16374,73 +16431,73 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tailwindcss/node@4.1.18': + '@tailwindcss/node@4.2.0': dependencies: '@jridgewell/remapping': 2.3.5 enhanced-resolve: 5.19.0 jiti: 2.6.1 - lightningcss: 1.30.2 + lightningcss: 1.31.1 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.1.18 + tailwindcss: 4.2.0 - '@tailwindcss/oxide-android-arm64@4.1.18': + '@tailwindcss/oxide-android-arm64@4.2.0': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.18': + '@tailwindcss/oxide-darwin-arm64@4.2.0': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.18': + '@tailwindcss/oxide-darwin-x64@4.2.0': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.18': + '@tailwindcss/oxide-freebsd-x64@4.2.0': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.0': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + '@tailwindcss/oxide-linux-arm64-gnu@4.2.0': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + '@tailwindcss/oxide-linux-arm64-musl@4.2.0': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + '@tailwindcss/oxide-linux-x64-gnu@4.2.0': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.18': + '@tailwindcss/oxide-linux-x64-musl@4.2.0': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.18': + '@tailwindcss/oxide-wasm32-wasi@4.2.0': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + '@tailwindcss/oxide-win32-arm64-msvc@4.2.0': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + '@tailwindcss/oxide-win32-x64-msvc@4.2.0': optional: true - '@tailwindcss/oxide@4.1.18': + '@tailwindcss/oxide@4.2.0': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.18 - '@tailwindcss/oxide-darwin-arm64': 4.1.18 - '@tailwindcss/oxide-darwin-x64': 4.1.18 - '@tailwindcss/oxide-freebsd-x64': 4.1.18 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 - '@tailwindcss/oxide-linux-x64-musl': 4.1.18 - '@tailwindcss/oxide-wasm32-wasi': 4.1.18 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + '@tailwindcss/oxide-android-arm64': 4.2.0 + '@tailwindcss/oxide-darwin-arm64': 4.2.0 + '@tailwindcss/oxide-darwin-x64': 4.2.0 + '@tailwindcss/oxide-freebsd-x64': 4.2.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.0 + '@tailwindcss/oxide-linux-x64-musl': 4.2.0 + '@tailwindcss/oxide-wasm32-wasi': 4.2.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.0 - '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@tailwindcss/vite@4.2.0(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@tailwindcss/node': 4.1.18 - '@tailwindcss/oxide': 4.1.18 - tailwindcss: 4.1.18 - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + '@tailwindcss/node': 4.2.0 + '@tailwindcss/oxide': 4.2.0 + tailwindcss: 4.2.0 + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@testing-library/dom@10.4.1': dependencies: @@ -16462,18 +16519,18 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/svelte-core@1.0.0(svelte@5.51.5)': + '@testing-library/svelte-core@1.0.0(svelte@5.53.0)': dependencies: - svelte: 5.51.5 + svelte: 5.53.0 - '@testing-library/svelte@5.3.1(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@testing-library/svelte@5.3.1(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@testing-library/dom': 10.4.1 - '@testing-library/svelte-core': 1.0.0(svelte@5.51.5) - svelte: 5.51.5 + '@testing-library/svelte-core': 1.0.0(svelte@5.53.0) + svelte: 5.53.0 optionalDependencies: - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: @@ -16719,7 +16776,7 @@ snapshots: '@types/node': 24.10.13 '@types/ssh2': 1.15.5 - '@types/dockerode@3.3.47': + '@types/dockerode@4.0.1': dependencies: '@types/docker-modem': 3.0.6 '@types/node': 24.10.13 @@ -16737,6 +16794,8 @@ snapshots: '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 + '@types/esrecurse@4.3.1': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -16901,12 +16960,12 @@ snapshots: dependencies: undici-types: 7.16.0 - '@types/node@25.2.3': + '@types/node@25.3.0': dependencies: - undici-types: 7.16.0 + undici-types: 7.18.2 optional: true - '@types/nodemailer@7.0.9': + '@types/nodemailer@7.0.10': dependencies: '@types/node': 24.10.13 @@ -17070,15 +17129,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.55.0 - '@typescript-eslint/type-utils': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.55.0 - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/parser': 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.56.0 + '@typescript-eslint/type-utils': 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.0 + eslint: 10.0.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -17086,58 +17145,58 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.55.0 - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.55.0 + '@typescript-eslint/scope-manager': 8.56.0 + '@typescript-eslint/types': 8.56.0 + '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.0 debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.55.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.56.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) - '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/tsconfig-utils': 8.56.0(typescript@5.9.3) + '@typescript-eslint/types': 8.56.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.55.0': + '@typescript-eslint/scope-manager@8.56.0': dependencies: - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/visitor-keys': 8.55.0 + '@typescript-eslint/types': 8.56.0 + '@typescript-eslint/visitor-keys': 8.56.0 - '@typescript-eslint/tsconfig-utils@8.55.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.56.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.56.0 + '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.2(jiti@2.6.1) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.55.0': {} + '@typescript-eslint/types@8.56.0': {} - '@typescript-eslint/typescript-estree@8.55.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.56.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.55.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/visitor-keys': 8.55.0 + '@typescript-eslint/project-service': 8.56.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.56.0(typescript@5.9.3) + '@typescript-eslint/types': 8.56.0 + '@typescript-eslint/visitor-keys': 8.56.0 debug: 4.4.3 - minimatch: 9.0.5 + minimatch: 9.0.6 semver: 7.7.4 tinyglobby: 0.2.15 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -17145,27 +17204,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.55.0 - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.56.0 + '@typescript-eslint/types': 8.56.0 + '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) + eslint: 10.0.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.55.0': + '@typescript-eslint/visitor-keys@8.56.0': dependencies: - '@typescript-eslint/types': 8.55.0 - eslint-visitor-keys: 4.2.1 + '@typescript-eslint/types': 8.56.0 + eslint-visitor-keys: 5.0.1 '@ungap/structured-clone@1.3.0': {} '@vercel/oidc@3.0.5': {} - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -17180,11 +17239,11 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -17199,7 +17258,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -17211,21 +17270,21 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -17337,10 +17396,10 @@ snapshots: dependencies: '@namnode/store': 0.1.0 - '@zoom-image/svelte@0.3.9(svelte@5.51.5)': + '@zoom-image/svelte@0.3.9(svelte@5.53.0)': dependencies: '@zoom-image/core': 0.42.0 - svelte: 5.51.5 + svelte: 5.53.0 abbrev@1.1.1: {} @@ -17425,6 +17484,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -17583,7 +17649,7 @@ snapshots: autoprefixer@10.4.24(postcss@8.5.6): dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001769 + caniuse-lite: 1.0.30001774 fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.6 @@ -17634,13 +17700,15 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + bare-events@2.8.2: {} - bare-fs@4.5.2: + bare-fs@4.5.4: dependencies: bare-events: 2.8.2 bare-path: 3.0.0 - bare-stream: 2.7.0(bare-events@2.8.2) + bare-stream: 2.8.0(bare-events@2.8.2) bare-url: 2.3.2 fast-fifo: 1.3.2 transitivePeerDependencies: @@ -17656,9 +17724,10 @@ snapshots: bare-os: 3.6.2 optional: true - bare-stream@2.7.0(bare-events@2.8.2): + bare-stream@2.8.0(bare-events@2.8.2): dependencies: streamx: 2.23.0 + teex: 1.0.1 optionalDependencies: bare-events: 2.8.2 transitivePeerDependencies: @@ -17677,7 +17746,7 @@ snapshots: baseline-browser-mapping@2.9.19: {} - batch-cluster@16.0.0: {} + batch-cluster@17.3.1: {} batch@0.6.1: {} @@ -17699,15 +17768,15 @@ snapshots: binary-extensions@2.3.0: {} - bits-ui@2.16.0(@internationalized/date@3.10.0)(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5): + bits-ui@2.16.0(@internationalized/date@3.10.0)(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0): dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/dom': 1.7.4 '@internationalized/date': 3.10.0 esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5) - svelte: 5.51.5 - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5) + runed: 0.35.1(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0) + svelte: 5.53.0 + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0) tabbable: 6.4.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -17787,6 +17856,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.3: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -17794,7 +17867,7 @@ snapshots: browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001769 + caniuse-lite: 1.0.30001774 electron-to-chromium: 1.5.286 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -17820,13 +17893,13 @@ snapshots: builtin-modules@5.0.0: {} - bullmq@5.68.0: + bullmq@5.69.3: dependencies: cron-parser: 4.9.0 ioredis: 5.9.2 msgpackr: 1.11.5 node-abort-controller: 3.1.1 - semver: 7.7.3 + semver: 7.7.4 tslib: 2.8.1 uuid: 11.1.0 transitivePeerDependencies: @@ -17911,16 +17984,16 @@ snapshots: caniuse-api@3.0.0: dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001769 + caniuse-lite: 1.0.30001774 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 - caniuse-lite@1.0.30001769: {} + caniuse-lite@1.0.30001774: {} canvas@2.11.2: dependencies: '@mapbox/node-pre-gyp': 1.0.11 - nan: 2.24.0 + nan: 2.25.0 simple-get: 3.1.1 transitivePeerDependencies: - encoding @@ -17930,7 +18003,7 @@ snapshots: canvas@2.11.2(encoding@0.1.13): dependencies: '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) - nan: 2.24.0 + nan: 2.25.0 simple-get: 3.1.1 transitivePeerDependencies: - encoding @@ -18035,7 +18108,7 @@ snapshots: dependencies: consola: 3.4.2 - cjs-module-lexer@1.4.3: {} + cjs-module-lexer@2.2.0: {} class-transformer@0.5.1: {} @@ -18308,7 +18381,7 @@ snapshots: cpu-features@0.0.10: dependencies: buildcheck: 0.0.7 - nan: 2.24.0 + nan: 2.25.0 optional: true crc-32@1.2.2: {} @@ -18823,7 +18896,7 @@ snapshots: dependencies: '@leichtgewicht/ip-codec': 2.0.5 - docker-compose@1.3.0: + docker-compose@1.3.1: dependencies: yaml: 2.8.2 @@ -18925,7 +18998,7 @@ snapshots: dependencies: is-obj: 2.0.0 - dotenv@17.2.4: {} + dotenv@17.3.1: {} dunder-proto@1.0.1: dependencies: @@ -19011,6 +19084,8 @@ snapshots: entities@6.0.1: {} + entities@7.0.1: {} + env-paths@2.2.1: {} err-code@2.0.3: {} @@ -19173,37 +19248,37 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)): + eslint-config-prettier@10.1.8(eslint@10.0.2(jiti@2.6.1)): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.2(jiti@2.6.1) - eslint-plugin-compat@6.1.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-compat@6.2.0(eslint@10.0.2(jiti@2.6.1)): dependencies: '@mdn/browser-compat-data': 6.1.5 ast-metadata-inferer: 0.8.1 browserslist: 4.28.1 - caniuse-lite: 1.0.30001769 - eslint: 9.39.2(jiti@2.6.1) + caniuse-lite: 1.0.30001774 + eslint: 10.0.2(jiti@2.6.1) find-up: 5.0.0 globals: 15.15.0 lodash.memoize: 4.1.2 semver: 7.7.4 - eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1): + eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1))(prettier@3.8.1): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.2(jiti@2.6.1) prettier: 3.8.1 prettier-linter-helpers: 1.0.1 synckit: 0.11.12 optionalDependencies: '@types/eslint': 9.6.1 - eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1)) + eslint-config-prettier: 10.1.8(eslint@10.0.2(jiti@2.6.1)) - eslint-plugin-svelte@3.15.0(eslint@9.39.2(jiti@2.6.1))(svelte@5.51.5): + eslint-plugin-svelte@3.15.0(eslint@10.0.2(jiti@2.6.1))(svelte@5.53.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2(jiti@2.6.1)) '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.2(jiti@2.6.1) esutils: 2.0.3 globals: 16.5.0 known-css-properties: 0.37.0 @@ -19211,23 +19286,21 @@ snapshots: postcss-load-config: 3.1.4(postcss@8.5.6) postcss-safe-parser: 7.0.1(postcss@8.5.6) semver: 7.7.4 - svelte-eslint-parser: 1.4.1(svelte@5.51.5) + svelte-eslint-parser: 1.4.1(svelte@5.53.0) optionalDependencies: - svelte: 5.51.5 + svelte: 5.53.0 transitivePeerDependencies: - ts-node - eslint-plugin-unicorn@62.0.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-unicorn@63.0.0(eslint@10.0.2(jiti@2.6.1)): dependencies: '@babel/helper-validator-identifier': 7.28.5 - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - '@eslint/plugin-kit': 0.4.1 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2(jiti@2.6.1)) change-case: 5.4.4 ci-info: 4.3.1 clean-regexp: 1.0.0 core-js-compat: 3.47.0 - eslint: 9.39.2(jiti@2.6.1) - esquery: 1.6.0 + eslint: 10.0.2(jiti@2.6.1) find-up-simple: 1.0.1 globals: 16.5.0 indent-string: 5.0.0 @@ -19249,33 +19322,39 @@ snapshots: esrecurse: 4.3.0 estraverse: 5.3.0 + eslint-scope@9.1.1: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + eslint-visitor-keys@3.4.3: {} eslint-visitor-keys@4.2.1: {} - eslint@9.39.2(jiti@2.6.1): + eslint-visitor-keys@5.0.1: {} + + eslint@10.0.2(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.3 - '@eslint/js': 9.39.2 - '@eslint/plugin-kit': 0.4.1 + '@eslint/config-array': 0.23.2 + '@eslint/config-helpers': 0.5.2 + '@eslint/core': 1.1.0 + '@eslint/plugin-kit': 0.6.0 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 - ajv: 6.12.6 - chalk: 4.1.2 + ajv: 6.14.0 cross-spawn: 7.0.6 debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.6.0 + eslint-scope: 9.1.1 + eslint-visitor-keys: 5.0.1 + espree: 11.1.1 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 8.0.0 @@ -19285,8 +19364,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 10.2.2 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -19309,9 +19387,15 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 4.2.1 + espree@11.1.1: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + esprima@4.0.1: {} - esquery@1.6.0: + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -19370,7 +19454,7 @@ snapshots: eta@2.2.0: {} - eta@4.5.0: {} + eta@4.5.1: {} etag@1.8.1: {} @@ -19410,21 +19494,21 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - exiftool-vendored.exe@13.45.0: + exiftool-vendored.exe@13.51.0: optional: true - exiftool-vendored.pl@13.45.0: {} + exiftool-vendored.pl@13.51.0: {} - exiftool-vendored@34.3.0: + exiftool-vendored@35.10.1: dependencies: - '@photostructure/tz-lookup': 11.3.0 + '@photostructure/tz-lookup': 11.4.0 '@types/luxon': 3.7.1 - batch-cluster: 16.0.0 - exiftool-vendored.pl: 13.45.0 + batch-cluster: 17.3.1 + exiftool-vendored.pl: 13.51.0 he: 1.2.0 luxon: 3.7.2 optionalDependencies: - exiftool-vendored.exe: 13.45.0 + exiftool-vendored.exe: 13.51.0 expect-type@1.3.0: {} @@ -19837,7 +19921,7 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 3.4.3 - minimatch: 9.0.5 + minimatch: 9.0.6 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 1.11.1 @@ -19876,12 +19960,12 @@ snapshots: dependencies: ini: 2.0.0 - globals@14.0.0: {} - globals@15.15.0: {} globals@16.5.0: {} + globals@17.3.0: {} + globalyzer@0.1.0: {} globby@11.1.0: @@ -19947,12 +20031,12 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - happy-dom@20.6.1: + happy-dom@20.6.3: dependencies: '@types/node': 24.10.13 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 - entities: 6.0.1 + entities: 7.0.1 whatwg-mimetype: 3.0.0 ws: 8.19.0 transitivePeerDependencies: @@ -20340,11 +20424,11 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - import-in-the-middle@2.0.0: + import-in-the-middle@2.0.6: dependencies: acorn: 8.16.0 acorn-import-attributes: 1.9.5(acorn@8.16.0) - cjs-module-lexer: 1.4.3 + cjs-module-lexer: 2.2.0 module-details-from-path: 1.0.4 import-lazy@4.0.0: {} @@ -20428,6 +20512,20 @@ snapshots: transitivePeerDependencies: - supports-color + ioredis@5.9.3: + dependencies: + '@ioredis/commands': 1.5.0 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ip-address@10.1.0: {} ipaddr.js@1.9.1: {} @@ -20822,12 +20920,20 @@ snapshots: type-is: 2.0.1 vary: 1.1.2 + kysely-postgres-js@3.0.0(kysely@0.28.11)(postgres@3.4.8): + dependencies: + kysely: 0.28.11 + optionalDependencies: + postgres: 3.4.8 + kysely-postgres-js@3.0.0(kysely@0.28.2)(postgres@3.4.8): dependencies: kysely: 0.28.2 optionalDependencies: postgres: 3.4.8 + kysely@0.28.11: {} + kysely@0.28.2: {} langium@3.3.1: @@ -20866,54 +20972,54 @@ snapshots: libphonenumber-js@1.12.31: {} - lightningcss-android-arm64@1.30.2: + lightningcss-android-arm64@1.31.1: optional: true - lightningcss-darwin-arm64@1.30.2: + lightningcss-darwin-arm64@1.31.1: optional: true - lightningcss-darwin-x64@1.30.2: + lightningcss-darwin-x64@1.31.1: optional: true - lightningcss-freebsd-x64@1.30.2: + lightningcss-freebsd-x64@1.31.1: optional: true - lightningcss-linux-arm-gnueabihf@1.30.2: + lightningcss-linux-arm-gnueabihf@1.31.1: optional: true - lightningcss-linux-arm64-gnu@1.30.2: + lightningcss-linux-arm64-gnu@1.31.1: optional: true - lightningcss-linux-arm64-musl@1.30.2: + lightningcss-linux-arm64-musl@1.31.1: optional: true - lightningcss-linux-x64-gnu@1.30.2: + lightningcss-linux-x64-gnu@1.31.1: optional: true - lightningcss-linux-x64-musl@1.30.2: + lightningcss-linux-x64-musl@1.31.1: optional: true - lightningcss-win32-arm64-msvc@1.30.2: + lightningcss-win32-arm64-msvc@1.31.1: optional: true - lightningcss-win32-x64-msvc@1.30.2: + lightningcss-win32-x64-msvc@1.31.1: optional: true - lightningcss@1.30.2: + lightningcss@1.31.1: dependencies: detect-libc: 2.1.2 optionalDependencies: - lightningcss-android-arm64: 1.30.2 - lightningcss-darwin-arm64: 1.30.2 - lightningcss-darwin-x64: 1.30.2 - lightningcss-freebsd-x64: 1.30.2 - lightningcss-linux-arm-gnueabihf: 1.30.2 - lightningcss-linux-arm64-gnu: 1.30.2 - lightningcss-linux-arm64-musl: 1.30.2 - lightningcss-linux-x64-gnu: 1.30.2 - lightningcss-linux-x64-musl: 1.30.2 - lightningcss-win32-arm64-msvc: 1.30.2 - lightningcss-win32-x64-msvc: 1.30.2 + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 lilconfig@2.1.0: {} @@ -20973,8 +21079,6 @@ snapshots: lodash.memoize@4.1.2: {} - lodash.merge@4.6.2: {} - lodash.once@4.1.1: {} lodash.uniq@4.5.0: {} @@ -21715,6 +21819,10 @@ snapshots: dependencies: '@isaacs/brace-expansion': 5.0.1 + minimatch@10.2.2: + dependencies: + brace-expansion: 5.0.3 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -21723,9 +21831,9 @@ snapshots: dependencies: brace-expansion: 2.0.2 - minimatch@9.0.5: + minimatch@9.0.6: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 5.0.3 minimist@1.2.8: {} @@ -21780,6 +21888,8 @@ snapshots: mkdirp@1.0.4: {} + mkdirp@3.0.1: {} + mlly@1.8.0: dependencies: acorn: 8.16.0 @@ -21848,7 +21958,7 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nan@2.24.0: + nan@2.25.0: optional: true nanoid@3.3.11: {} @@ -21874,12 +21984,12 @@ snapshots: neo-async@2.6.2: {} - nest-commander@3.20.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@types/inquirer@8.2.12)(@types/node@24.10.13)(typescript@5.9.3): + nest-commander@3.20.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@types/inquirer@8.2.12)(@types/node@24.10.13)(typescript@5.9.3): dependencies: '@fig/complete-commander': 3.2.0(commander@11.1.0) - '@golevelup/nestjs-discovery': 5.0.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) - '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@golevelup/nestjs-discovery': 5.0.0(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@types/inquirer': 8.2.12 commander: 11.1.0 cosmiconfig: 8.3.6(typescript@5.9.3) @@ -21888,25 +21998,25 @@ snapshots: - '@types/node' - typescript - nestjs-cls@5.4.3(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2): + nestjs-cls@5.4.3(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - nestjs-kysely@3.1.2(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(kysely@0.28.2)(reflect-metadata@0.2.2): + nestjs-kysely@3.1.2(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(kysely@0.28.2)(reflect-metadata@0.2.2): dependencies: - '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) kysely: 0.28.2 reflect-metadata: 0.2.2 tslib: 2.8.1 - nestjs-otel@7.0.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13): + nestjs-otel@7.0.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14): dependencies: - '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@opentelemetry/api': 1.9.0 '@opentelemetry/host-metrics': 0.36.2(@opentelemetry/api@1.9.0) response-time: 2.3.4 @@ -22057,12 +22167,12 @@ snapshots: obug@2.1.1: {} - oidc-provider@9.6.0: + oidc-provider@9.6.1: dependencies: '@koa/cors': 5.0.0 - '@koa/router': 15.1.0(koa@3.1.1) + '@koa/router': 15.3.0(koa@3.1.1) debug: 4.4.3 - eta: 4.5.0 + eta: 4.5.1 jose: 6.1.3 jsesc: 3.1.0 koa: 3.1.1 @@ -22920,10 +23030,10 @@ snapshots: dependencies: prettier: 3.8.1 - prettier-plugin-svelte@3.4.1(prettier@3.8.1)(svelte@5.51.5): + prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.53.0): dependencies: prettier: 3.8.1 - svelte: 5.51.5 + svelte: 5.53.0 prettier@3.8.1: {} @@ -22976,9 +23086,12 @@ snapshots: retry: 0.12.0 signal-exit: 3.0.7 - properties-reader@2.3.0: + properties-reader@3.0.1: dependencies: - mkdirp: 1.0.4 + '@kwsites/file-exists': 1.1.1 + mkdirp: 3.0.1 + transitivePeerDependencies: + - supports-color property-information@5.6.0: dependencies: @@ -23540,14 +23653,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 - runed@0.35.1(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5): + runed@0.35.1(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 - svelte: 5.51.5 + svelte: 5.53.0 optionalDependencies: - '@sveltejs/kit': 2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/kit': 2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) rw@1.3.3: {} @@ -23573,7 +23686,7 @@ snapshots: dependencies: truncate-utf8-bytes: 1.0.2 - sanitize-html@2.17.0: + sanitize-html@2.17.1: dependencies: deepmerge: 4.3.1 escape-string-regexp: 4.0.0 @@ -23642,8 +23755,6 @@ snapshots: semver@6.3.1: {} - semver@7.7.3: {} - semver@7.7.4: {} send@0.19.2: @@ -23991,7 +24102,7 @@ snapshots: sprintf-js@1.0.3: {} - sql-formatter@15.7.0: + sql-formatter@15.7.2: dependencies: argparse: 2.0.1 nearley: 2.20.1 @@ -24009,7 +24120,7 @@ snapshots: bcrypt-pbkdf: 1.0.2 optionalDependencies: cpu-features: 0.0.10 - nan: 2.24.0 + nan: 2.25.0 ssri@13.0.1: dependencies: @@ -24173,23 +24284,23 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-awesome@3.3.5(svelte@5.51.5): + svelte-awesome@3.3.5(svelte@5.53.0): dependencies: - svelte: 5.51.5 + svelte: 5.53.0 - svelte-check@4.3.6(picomatch@4.0.3)(svelte@5.51.5)(typescript@5.9.3): + svelte-check@4.4.1(picomatch@4.0.3)(svelte@5.53.0)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.3) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.51.5 + svelte: 5.53.0 typescript: 5.9.3 transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.4.1(svelte@5.51.5): + svelte-eslint-parser@1.4.1(svelte@5.53.0): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -24198,7 +24309,7 @@ snapshots: postcss-scss: 4.0.9(postcss@8.5.6) postcss-selector-parser: 7.1.1 optionalDependencies: - svelte: 5.51.5 + svelte: 5.53.0 svelte-floating-ui@1.5.8: dependencies: @@ -24211,7 +24322,7 @@ snapshots: dependencies: highlight.js: 11.11.1 - svelte-i18n@4.0.1(svelte@5.51.5): + svelte-i18n@4.0.1(svelte@5.53.0): dependencies: cli-color: 2.0.4 deepmerge: 4.3.1 @@ -24219,10 +24330,10 @@ snapshots: estree-walker: 2.0.2 intl-messageformat: 10.7.18 sade: 1.8.1 - svelte: 5.51.5 + svelte: 5.53.0 tiny-glob: 0.2.9 - svelte-jsoneditor@3.11.0(svelte@5.51.5): + svelte-jsoneditor@3.11.0(svelte@5.53.0): dependencies: '@codemirror/autocomplete': 6.20.0 '@codemirror/commands': 6.10.1 @@ -24249,42 +24360,42 @@ snapshots: memoize-one: 6.0.0 natural-compare-lite: 1.4.0 sass: 1.97.1 - svelte: 5.51.5 - svelte-awesome: 3.3.5(svelte@5.51.5) + svelte: 5.53.0 + svelte-awesome: 3.3.5(svelte@5.53.0) svelte-select: 5.8.3 vanilla-picker: 2.12.3 - svelte-maplibre@1.2.6(svelte@5.51.5): + svelte-maplibre@1.2.6(svelte@5.53.0): dependencies: d3-geo: 3.1.1 dequal: 2.0.3 just-compare: 2.3.0 maplibre-gl: 5.18.0 pmtiles: 3.2.1 - svelte: 5.51.5 + svelte: 5.53.0 - svelte-parse-markup@0.1.5(svelte@5.51.5): + svelte-parse-markup@0.1.5(svelte@5.53.0): dependencies: - svelte: 5.51.5 + svelte: 5.53.0 - svelte-persisted-store@0.12.0(svelte@5.51.5): + svelte-persisted-store@0.12.0(svelte@5.53.0): dependencies: - svelte: 5.51.5 + svelte: 5.53.0 svelte-select@5.8.3: dependencies: svelte-floating-ui: 1.5.8 - svelte-toolbelt@0.10.6(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5): + svelte-toolbelt@0.10.6(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.51.5)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.51.5) + runed: 0.35.1(@sveltejs/kit@2.52.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.0) style-to-object: 1.0.14 - svelte: 5.51.5 + svelte: 5.53.0 transitivePeerDependencies: - '@sveltejs/kit' - svelte@5.51.5: + svelte@5.53.0: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 @@ -24340,9 +24451,9 @@ snapshots: tailwind-merge@3.4.0: {} - tailwind-variants@3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18): + tailwind-variants@3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.2.0): dependencies: - tailwindcss: 4.1.18 + tailwindcss: 4.2.0 optionalDependencies: tailwind-merge: 3.4.0 @@ -24388,7 +24499,7 @@ snapshots: - tsx - yaml - tailwindcss@4.1.18: {} + tailwindcss@4.2.0: {} tapable@2.3.0: {} @@ -24404,7 +24515,7 @@ snapshots: pump: 3.0.3 tar-stream: 3.1.7 optionalDependencies: - bare-fs: 4.5.2 + bare-fs: 4.5.4 bare-path: 3.0.0 transitivePeerDependencies: - bare-abort-controller @@ -24445,6 +24556,14 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + teex@1.0.1: + dependencies: + streamx: 2.23.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + terser-webpack-plugin@5.3.16(@swc/core@1.15.11(@swc/helpers@0.5.17))(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -24476,25 +24595,25 @@ snapshots: dependencies: '@istanbuljs/schema': 0.1.3 glob: 10.5.0 - minimatch: 9.0.5 + minimatch: 9.0.6 - testcontainers@11.11.0: + testcontainers@11.12.0: dependencies: '@balena/dockerignore': 1.0.2 - '@types/dockerode': 3.3.47 + '@types/dockerode': 4.0.1 archiver: 7.0.1 async-lock: 1.4.1 byline: 5.0.0 debug: 4.4.3 - docker-compose: 1.3.0 + docker-compose: 1.3.1 dockerode: 4.0.9 get-port: 7.1.0 proper-lockfile: 4.1.2 - properties-reader: 2.3.0 + properties-reader: 3.0.1 ssh-remote-port-forward: 1.0.4 tar-fs: 3.1.1 tmp: 0.2.5 - undici: 7.18.0 + undici: 7.22.0 transitivePeerDependencies: - bare-abort-controller - bare-buffer @@ -24690,13 +24809,13 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.0.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -24728,7 +24847,10 @@ snapshots: undici-types@7.16.0: {} - undici@7.18.0: {} + undici-types@7.18.2: + optional: true + + undici@7.22.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -24955,7 +25077,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-imagetools@9.0.2(rollup@4.55.1): + vite-imagetools@9.0.3(rollup@4.55.1): dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.55.1) imagetools-core: 9.1.0 @@ -24964,13 +25086,13 @@ snapshots: - rollup - supports-color - vite-node@3.2.4(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vite-node@3.2.4(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -24985,13 +25107,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vite-node@3.2.4(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -25006,17 +25128,17 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) - vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - typescript - vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -25028,13 +25150,13 @@ snapshots: '@types/node': 24.10.13 fsevents: 2.3.3 jiti: 2.6.1 - lightningcss: 1.30.2 + lightningcss: 1.31.1 sass: 1.97.1 terser: 5.44.1 tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -25043,28 +25165,28 @@ snapshots: rollup: 4.55.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.2.3 + '@types/node': 25.3.0 fsevents: 2.3.3 jiti: 2.6.1 - lightningcss: 1.30.2 + lightningcss: 1.31.1 sass: 1.97.1 terser: 5.44.1 tsx: 4.21.0 yaml: 2.8.2 - vitefu@1.1.1(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + vitefu@1.1.1(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): optionalDependencies: - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -25082,13 +25204,13 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.10.13 - happy-dom: 20.6.1 + happy-dom: 20.6.3 jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13)) transitivePeerDependencies: - jiti @@ -25104,11 +25226,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -25126,13 +25248,13 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.10.13 - happy-dom: 20.6.1 + happy-dom: 20.6.3 jsdom: 26.1.0(canvas@2.11.2) transitivePeerDependencies: - jiti @@ -25148,11 +25270,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -25170,13 +25292,13 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 25.2.3 - happy-dom: 20.6.1 + '@types/node': 25.3.0 + happy-dom: 20.6.3 jsdom: 26.1.0(canvas@2.11.2) transitivePeerDependencies: - jiti diff --git a/server/package.json b/server/package.json index fa10f8bd1a..9b1acc91fb 100644 --- a/server/package.json +++ b/server/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@extism/extism": "2.0.0-rc13", + "@immich/sql-tools": "^0.2.0", "@nestjs/bullmq": "^11.0.1", "@nestjs/common": "^11.0.4", "@nestjs/core": "^11.0.4", @@ -45,14 +46,14 @@ "@nestjs/websockets": "^11.0.4", "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.0.0", - "@opentelemetry/exporter-prometheus": "^0.211.0", - "@opentelemetry/instrumentation-http": "^0.211.0", - "@opentelemetry/instrumentation-ioredis": "^0.59.0", - "@opentelemetry/instrumentation-nestjs-core": "^0.57.0", - "@opentelemetry/instrumentation-pg": "^0.63.0", + "@opentelemetry/exporter-prometheus": "^0.212.0", + "@opentelemetry/instrumentation-http": "^0.212.0", + "@opentelemetry/instrumentation-ioredis": "^0.60.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.58.0", + "@opentelemetry/instrumentation-pg": "^0.64.0", "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1", - "@opentelemetry/sdk-node": "^0.211.0", + "@opentelemetry/sdk-node": "^0.212.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@react-email/components": "^0.5.0", "@react-email/render": "^1.1.2", @@ -70,7 +71,7 @@ "cookie": "^1.0.2", "cookie-parser": "^1.4.7", "cron": "4.4.0", - "exiftool-vendored": "^34.3.0", + "exiftool-vendored": "^35.0.0", "express": "^5.1.0", "fast-glob": "^3.3.2", "fluent-ffmpeg": "^2.1.2", @@ -116,7 +117,7 @@ "validator": "^13.12.0" }, "devDependencies": { - "@eslint/js": "^9.8.0", + "@eslint/js": "^10.0.0", "@nestjs/cli": "^11.0.2", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.4", @@ -146,11 +147,11 @@ "@types/ua-parser-js": "^0.7.36", "@types/validator": "^13.15.2", "@vitest/coverage-v8": "^3.0.0", - "eslint": "^9.14.0", + "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^62.0.0", - "globals": "^16.0.0", + "eslint-plugin-unicorn": "^63.0.0", + "globals": "^17.0.0", "mock-fs": "^5.2.0", "node-gyp": "^12.0.0", "pngjs": "^7.0.0", diff --git a/server/src/bin/migrations.ts b/server/src/bin/migrations.ts index 588f358023..bfa0f1733c 100644 --- a/server/src/bin/migrations.ts +++ b/server/src/bin/migrations.ts @@ -1,16 +1,15 @@ #!/usr/bin/env node process.env.DB_URL = process.env.DB_URL || 'postgres://postgres:postgres@localhost:5432/immich'; +import { schemaDiff, schemaFromCode, schemaFromDatabase } from '@immich/sql-tools'; import { Kysely, sql } from 'kysely'; import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'; import { basename, dirname, extname, join } from 'node:path'; -import postgres from 'postgres'; import { ConfigRepository } from 'src/repositories/config.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import 'src/schema'; -import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools'; -import { asPostgresConnectionConfig, getKyselyConfig } from 'src/utils/database'; +import { getKyselyConfig } from 'src/utils/database'; const main = async () => { const command = process.argv[2]; @@ -130,10 +129,9 @@ const create = (path: string, up: string[], down: string[]) => { const compare = async () => { const configRepository = new ConfigRepository(); const { database } = configRepository.getEnv(); - const db = postgres(asPostgresConnectionConfig(database.config)); const source = schemaFromCode({ overrides: true, namingStrategy: 'default' }); - const target = await schemaFromDatabase(db, {}); + const target = await schemaFromDatabase({ connection: database.config }); console.log(source.warnings.join('\n')); diff --git a/server/src/commands/schema-check.ts b/server/src/commands/schema-check.ts index c6e90fd9ca..e0ccae8469 100644 --- a/server/src/commands/schema-check.ts +++ b/server/src/commands/schema-check.ts @@ -1,7 +1,7 @@ +import { asHuman } from '@immich/sql-tools'; import { Command, CommandRunner } from 'nest-commander'; import { ErrorMessages } from 'src/constants'; import { CliService } from 'src/services/cli.service'; -import { asHuman } from 'src/sql-tools/schema-diff'; @Command({ name: 'schema-check', diff --git a/server/src/controllers/asset.controller.spec.ts b/server/src/controllers/asset.controller.spec.ts index 197e06d02d..2893a27539 100644 --- a/server/src/controllers/asset.controller.spec.ts +++ b/server/src/controllers/asset.controller.spec.ts @@ -369,6 +369,31 @@ describe(AssetController.name, () => { expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['id must be a UUID']))); }); + it('should check the action and parameters discriminator', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/assets/${factory.uuid()}/edits`) + .send({ + edits: [ + { + action: 'rotate', + parameters: { + x: 0, + y: 0, + width: 100, + height: 100, + }, + }, + ], + }); + + expect(status).toBe(400); + expect(body).toEqual( + factory.responses.badRequest( + expect.arrayContaining([expect.stringContaining('parameters.angle must be one of the following values')]), + ), + ); + }); + it('should require at least one edit', async () => { const { status, body } = await request(ctx.getHttpServer()) .put(`/assets/${factory.uuid()}/edits`) diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index 8eb3a5ce44..2024760975 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -20,7 +20,7 @@ import { UpdateAssetDto, } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetEditActionListDto, AssetEditsDto } from 'src/dtos/editing.dto'; +import { AssetEditsCreateDto, AssetEditsResponseDto } from 'src/dtos/editing.dto'; import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; import { ApiTag, Permission, RouteKey } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; @@ -235,7 +235,7 @@ export class AssetController { description: 'Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset.', history: new HistoryBuilder().added('v2.5.0').beta('v2.5.0'), }) - getAssetEdits(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + getAssetEdits(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getAssetEdits(auth, id); } @@ -249,8 +249,8 @@ export class AssetController { editAsset( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, - @Body() dto: AssetEditActionListDto, - ): Promise { + @Body() dto: AssetEditsCreateDto, + ): Promise { return this.service.editAsset(auth, id, dto); } diff --git a/server/src/controllers/memory.controller.spec.ts b/server/src/controllers/memory.controller.spec.ts index 8629b6c799..820819ee6e 100644 --- a/server/src/controllers/memory.controller.spec.ts +++ b/server/src/controllers/memory.controller.spec.ts @@ -51,6 +51,20 @@ describe(MemoryController.name, () => { errorDto.badRequest(['data.year must be a positive number', 'data.year must be an integer number']), ); }); + + it('should accept showAt and hideAt', async () => { + const { status } = await request(ctx.getHttpServer()) + .post('/memories') + .send({ + type: 'on_this_day', + data: { year: 2020 }, + memoryAt: new Date(2021).toISOString(), + showAt: new Date(2022).toISOString(), + hideAt: new Date(2023).toISOString(), + }); + + expect(status).toBe(201); + }); }); describe('GET /memories/statistics', () => { diff --git a/server/src/database.ts b/server/src/database.ts index dd979fdea6..5d29cbb043 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -352,6 +352,7 @@ export const columns = { 'asset_file.type', 'asset_file.isEdited', 'asset_file.isProgressive', + 'asset_file.isTransparent', ], authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'], authApiKey: ['api_key.id', 'api_key.permissions'], diff --git a/server/src/decorators.ts b/server/src/decorators.ts index 87a3900a7f..695adb4a36 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -1,10 +1,10 @@ +import { BeforeUpdateTrigger, Column, ColumnOptions } from '@immich/sql-tools'; import { SetMetadata, applyDecorators } from '@nestjs/common'; import { ApiOperation, ApiOperationOptions, ApiProperty, ApiPropertyOptions, ApiTags } from '@nestjs/swagger'; import _ from 'lodash'; import { ApiCustomExtension, ApiTag, ImmichWorker, JobName, MetadataKey, QueueName } from 'src/enum'; import { EmitEvent } from 'src/repositories/event.repository'; import { immich_uuid_v7, updated_at } from 'src/schema/functions'; -import { BeforeUpdateTrigger, Column, ColumnOptions } from 'src/sql-tools'; import { setUnion } from 'src/utils/set'; const GeneratedUuidV7Column = (options: Omit = {}) => diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index df02a0cdea..a76df4abaa 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -25,7 +25,10 @@ export class SanitizedAssetResponseDto { id!: string; @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', description: 'Asset type' }) type!: AssetType; - @ApiProperty({ description: 'Thumbhash for thumbnail generation' }) + @ApiProperty({ + description: + 'Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting.', + }) thumbhash!: string | null; @ApiPropertyOptional({ description: 'Original MIME type' }) originalMimeType?: string; diff --git a/server/src/dtos/editing.dto.ts b/server/src/dtos/editing.dto.ts index 3c4c063b10..fcdfdcad5f 100644 --- a/server/src/dtos/editing.dto.ts +++ b/server/src/dtos/editing.dto.ts @@ -1,7 +1,8 @@ -import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger'; -import { ClassConstructor, plainToInstance, Transform, Type } from 'class-transformer'; +import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; import { ArrayMinSize, IsEnum, IsInt, Min, ValidateNested } from 'class-validator'; -import { IsAxisAlignedRotation, IsUniqueEditActions, ValidateUUID } from 'src/validation'; +import { ExtraModel } from 'src/dtos/sync.dto'; +import { IsAxisAlignedRotation, IsUniqueEditActions, ValidateEnum, ValidateUUID } from 'src/validation'; export enum AssetEditAction { Crop = 'crop', @@ -14,6 +15,7 @@ export enum MirrorAxis { Vertical = 'vertical', } +@ExtraModel() export class CropParameters { @IsInt() @Min(0) @@ -36,48 +38,21 @@ export class CropParameters { height!: number; } +@ExtraModel() export class RotateParameters { @IsAxisAlignedRotation() @ApiProperty({ description: 'Rotation angle in degrees' }) angle!: number; } +@ExtraModel() export class MirrorParameters { @IsEnum(MirrorAxis) @ApiProperty({ enum: MirrorAxis, enumName: 'MirrorAxis', description: 'Axis to mirror along' }) axis!: MirrorAxis; } -class AssetEditActionBase { - @IsEnum(AssetEditAction) - @ApiProperty({ enum: AssetEditAction, enumName: 'AssetEditAction', description: 'Type of edit action to perform' }) - action!: AssetEditAction; -} - -export class AssetEditActionCrop extends AssetEditActionBase { - @ValidateNested() - @Type(() => CropParameters) - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - parameters!: CropParameters; -} - -export class AssetEditActionRotate extends AssetEditActionBase { - @ValidateNested() - @Type(() => RotateParameters) - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - parameters!: RotateParameters; -} - -export class AssetEditActionMirror extends AssetEditActionBase { - @ValidateNested() - @Type(() => MirrorParameters) - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - parameters!: MirrorParameters; -} - +export type AssetEditParameters = CropParameters | RotateParameters | MirrorParameters; export type AssetEditActionItem = | { action: AssetEditAction.Crop; @@ -92,47 +67,48 @@ export type AssetEditActionItem = parameters: MirrorParameters; }; -export type AssetEditActionParameter = { - [AssetEditAction.Crop]: CropParameters; - [AssetEditAction.Rotate]: RotateParameters; - [AssetEditAction.Mirror]: MirrorParameters; +export class AssetEditActionItemDto { + @ValidateEnum({ name: 'AssetEditAction', enum: AssetEditAction, description: 'Type of edit action to perform' }) + action!: AssetEditAction; + + @ApiProperty({ + description: 'List of edit actions to apply (crop, rotate, or mirror)', + anyOf: [CropParameters, RotateParameters, MirrorParameters].map((type) => ({ + $ref: getSchemaPath(type), + })), + }) + @ValidateNested() + @Type((options) => actionParameterMap[options?.object.action as keyof AssetEditActionParameter]) + parameters!: AssetEditActionItem['parameters']; +} + +export class AssetEditActionItemResponseDto extends AssetEditActionItemDto { + @ValidateUUID() + id!: string; +} + +export type AssetEditActionParameter = typeof actionParameterMap; +const actionParameterMap = { + [AssetEditAction.Crop]: CropParameters, + [AssetEditAction.Rotate]: RotateParameters, + [AssetEditAction.Mirror]: MirrorParameters, }; -type AssetEditActions = AssetEditActionCrop | AssetEditActionRotate | AssetEditActionMirror; -const actionToClass: Record> = { - [AssetEditAction.Crop]: AssetEditActionCrop, - [AssetEditAction.Rotate]: AssetEditActionRotate, - [AssetEditAction.Mirror]: AssetEditActionMirror, -} as const; - -const getActionClass = (item: { action: AssetEditAction }): ClassConstructor => - actionToClass[item.action]; - -@ApiExtraModels(AssetEditActionRotate, AssetEditActionMirror, AssetEditActionCrop) -export class AssetEditActionListDto { - /** list of edits */ +export class AssetEditsCreateDto { @ArrayMinSize(1) @IsUniqueEditActions() @ValidateNested({ each: true }) - @Transform(({ value: edits }) => - Array.isArray(edits) ? edits.map((item) => plainToInstance(getActionClass(item), item)) : edits, - ) - @ApiProperty({ - items: { - anyOf: Object.values(actionToClass).map((type) => ({ $ref: getSchemaPath(type) })), - discriminator: { - propertyName: 'action', - mapping: Object.fromEntries( - Object.entries(actionToClass).map(([action, type]) => [action, getSchemaPath(type)]), - ), - }, - }, - description: 'List of edit actions to apply (crop, rotate, or mirror)', - }) - edits!: AssetEditActionItem[]; + @Type(() => AssetEditActionItemDto) + @ApiProperty({ description: 'List of edit actions to apply (crop, rotate, or mirror)' }) + edits!: AssetEditActionItemDto[]; } -export class AssetEditsDto extends AssetEditActionListDto { - @ValidateUUID({ description: 'Asset ID to apply edits to' }) +export class AssetEditsResponseDto { + @ValidateUUID({ description: 'Asset ID these edits belong to' }) assetId!: string; + + @ApiProperty({ + description: 'List of edit actions applied to the asset', + }) + edits!: AssetEditActionItemResponseDto[]; } diff --git a/server/src/dtos/env.dto.ts b/server/src/dtos/env.dto.ts index e088a33413..b04366c273 100644 --- a/server/src/dtos/env.dto.ts +++ b/server/src/dtos/env.dto.ts @@ -1,8 +1,17 @@ import { Transform, Type } from 'class-transformer'; import { IsEnum, IsInt, IsString, Matches } from 'class-validator'; -import { DatabaseSslMode, ImmichEnvironment, LogFormat, LogLevel } from 'src/enum'; +import { ImmichEnvironment, LogFormat, LogLevel } from 'src/enum'; import { IsIPRange, Optional, ValidateBoolean } from 'src/validation'; +// TODO import from sql-tools once the swagger plugin supports external enums +enum DatabaseSslMode { + Disable = 'disable', + Allow = 'allow', + Prefer = 'prefer', + Require = 'require', + VerifyFull = 'verify-full', +} + export class EnvDto { @IsInt() @Optional() diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index 0d73c19b20..edf65ef583 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -2,6 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; import { Memory } from 'src/database'; +import { HistoryBuilder } from 'src/decorators'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetOrderWithRandom, MemoryType } from 'src/enum'; @@ -77,6 +78,20 @@ export class MemoryCreateDto extends MemoryBaseDto { @ValidateDate({ description: 'Memory date' }) memoryAt!: Date; + @ValidateDate({ + optional: true, + description: 'Date when memory should be shown', + history: new HistoryBuilder().added('v2.6.0').stable('v2.6.0'), + }) + showAt?: Date; + + @ValidateDate({ + optional: true, + description: 'Date when memory should be hidden', + history: new HistoryBuilder().added('v2.6.0').stable('v2.6.0'), + }) + hideAt?: Date; + @ValidateUUID({ optional: true, each: true, description: 'Asset IDs to associate with memory' }) assetIds?: string[]; } diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 59d7d373f0..c1b85c0430 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -422,6 +422,20 @@ export class SyncAssetFaceV1 { sourceType!: string; } +@ExtraModel() +export class SyncAssetFaceV2 extends SyncAssetFaceV1 { + @ApiProperty({ description: 'Face deleted at' }) + deletedAt!: Date | null; + @ApiProperty({ description: 'Is the face visible in the asset' }) + isVisible!: boolean; +} + +export function syncAssetFaceV2ToV1(faceV2: SyncAssetFaceV2): SyncAssetFaceV1 { + const { deletedAt: _, isVisible: __, ...faceV1 } = faceV2; + + return faceV1; +} + @ExtraModel() export class SyncAssetFaceDeleteV1 { @ApiProperty({ description: 'Asset face ID' }) @@ -497,6 +511,7 @@ export type SyncItem = { [SyncEntityType.PersonV1]: SyncPersonV1; [SyncEntityType.PersonDeleteV1]: SyncPersonDeleteV1; [SyncEntityType.AssetFaceV1]: SyncAssetFaceV1; + [SyncEntityType.AssetFaceV2]: SyncAssetFaceV2; [SyncEntityType.AssetFaceDeleteV1]: SyncAssetFaceDeleteV1; [SyncEntityType.UserMetadataV1]: SyncUserMetadataV1; [SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1; diff --git a/server/src/enum.ts b/server/src/enum.ts index 59ae509333..e7c72d6728 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -762,6 +762,7 @@ export enum SyncRequestType { UsersV1 = 'UsersV1', PeopleV1 = 'PeopleV1', AssetFacesV1 = 'AssetFacesV1', + AssetFacesV2 = 'AssetFacesV2', UserMetadataV1 = 'UserMetadataV1', } @@ -820,6 +821,7 @@ export enum SyncEntityType { PersonDeleteV1 = 'PersonDeleteV1', AssetFaceV1 = 'AssetFaceV1', + AssetFaceV2 = 'AssetFaceV2', AssetFaceDeleteV1 = 'AssetFaceDeleteV1', UserMetadataV1 = 'UserMetadataV1', @@ -851,14 +853,6 @@ export enum OAuthTokenEndpointAuthMethod { ClientSecretBasic = 'client_secret_basic', } -export enum DatabaseSslMode { - Disable = 'disable', - Allow = 'allow', - Prefer = 'prefer', - Require = 'require', - VerifyFull = 'verify-full', -} - export enum AssetVisibility { Archive = 'archive', Timeline = 'timeline', diff --git a/server/src/main.ts b/server/src/main.ts index a8e3178a43..f2491f07bc 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -52,9 +52,9 @@ class Workers { try { const value = await systemMetadataRepository.get(SystemMetadataKey.MaintenanceMode); return value?.isMaintenanceMode || false; - } catch (error) { + } catch (error: Error | any) { // Table doesn't exist (migrations haven't run yet) - if (error instanceof PostgresError && error.code === '42P01') { + if ((error as PostgresError).code === '42P01') { return false; } diff --git a/server/src/queries/asset.edit.repository.sql b/server/src/queries/asset.edit.repository.sql index 0cf62882db..9330305973 100644 --- a/server/src/queries/asset.edit.repository.sql +++ b/server/src/queries/asset.edit.repository.sql @@ -9,6 +9,7 @@ rollback -- AssetEditRepository.getAll select + "id", "action", "parameters" from diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index d990e0a304..54b3c92dd4 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -216,7 +216,8 @@ select "asset_file"."path", "asset_file"."type", "asset_file"."isEdited", - "asset_file"."isProgressive" + "asset_file"."isProgressive", + "asset_file"."isTransparent" from "asset_file" where diff --git a/server/src/queries/sync.repository.sql b/server/src/queries/sync.repository.sql index f817ad57b3..68a85e4c0f 100644 --- a/server/src/queries/sync.repository.sql +++ b/server/src/queries/sync.repository.sql @@ -540,6 +540,8 @@ select "boundingBoxX2", "boundingBoxY2", "sourceType", + "isVisible", + "asset_face"."deletedAt", "asset_face"."updateId" from "asset_face" as "asset_face" diff --git a/server/src/repositories/api-key.repository.ts b/server/src/repositories/api-key.repository.ts index 28307d7c83..1e0d13f16c 100644 --- a/server/src/repositories/api-key.repository.ts +++ b/server/src/repositories/api-key.repository.ts @@ -31,7 +31,7 @@ export class ApiKeyRepository { } @GenerateSql({ params: [DummyValue.STRING] }) - getKey(hashedToken: string) { + getKey(hashedToken: Buffer) { return this.db .selectFrom('api_key') .select((eb) => [ diff --git a/server/src/repositories/asset-edit.repository.ts b/server/src/repositories/asset-edit.repository.ts index 088cb1ccff..23c7c3a0b9 100644 --- a/server/src/repositories/asset-edit.repository.ts +++ b/server/src/repositories/asset-edit.repository.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { Kysely } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { AssetEditActionItem } from 'src/dtos/editing.dto'; +import { AssetEditActionItem, AssetEditActionItemResponseDto } from 'src/dtos/editing.dto'; import { DB } from 'src/schema'; @Injectable() @@ -12,7 +12,7 @@ export class AssetEditRepository { @GenerateSql({ params: [DummyValue.UUID], }) - replaceAll(assetId: string, edits: AssetEditActionItem[]): Promise { + replaceAll(assetId: string, edits: AssetEditActionItem[]): Promise { return this.db.transaction().execute(async (trx) => { await trx.deleteFrom('asset_edit').where('assetId', '=', assetId).execute(); @@ -20,8 +20,8 @@ export class AssetEditRepository { return trx .insertInto('asset_edit') .values(edits.map((edit, i) => ({ assetId, sequence: i, ...edit }))) - .returning(['action', 'parameters']) - .execute() as Promise; + .returning(['id', 'action', 'parameters']) + .execute(); } return []; @@ -31,12 +31,12 @@ export class AssetEditRepository { @GenerateSql({ params: [DummyValue.UUID], }) - getAll(assetId: string): Promise { + getAll(assetId: string): Promise { return this.db .selectFrom('asset_edit') - .select(['action', 'parameters']) + .select(['id', 'action', 'parameters']) .where('assetId', '=', assetId) .orderBy('sequence', 'asc') - .execute() as Promise; + .execute(); } } diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index e424d4e0b8..b58d852707 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -903,7 +903,10 @@ export class AssetRepository { } async upsertFile( - file: Pick, 'assetId' | 'path' | 'type' | 'isEdited' | 'isProgressive'>, + file: Pick< + Insertable, + 'assetId' | 'path' | 'type' | 'isEdited' | 'isProgressive' | 'isTransparent' + >, ): Promise { await this.db .insertInto('asset_file') @@ -917,7 +920,10 @@ export class AssetRepository { } async upsertFiles( - files: Pick, 'assetId' | 'path' | 'type' | 'isEdited' | 'isProgressive'>[], + files: Pick< + Insertable, + 'assetId' | 'path' | 'type' | 'isEdited' | 'isProgressive' | 'isTransparent' + >[], ): Promise { if (files.length === 0) { return; @@ -930,6 +936,7 @@ export class AssetRepository { oc.columns(['assetId', 'type', 'isEdited']).doUpdateSet((eb) => ({ path: eb.ref('excluded.path'), isProgressive: eb.ref('excluded.isProgressive'), + isTransparent: eb.ref('excluded.isTransparent'), })), ) .execute(); diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index 54a5d1987f..7e8082a582 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -1,3 +1,4 @@ +import { DatabaseConnectionParams } from '@immich/sql-tools'; import { RegisterQueueOptions } from '@nestjs/bullmq'; import { Inject, Injectable, Optional } from '@nestjs/common'; import { QueueOptions } from 'bullmq'; @@ -21,7 +22,7 @@ import { LogLevel, QueueName, } from 'src/enum'; -import { DatabaseConnectionParams, VectorExtension } from 'src/types'; +import { VectorExtension } from 'src/types'; import { setDifference } from 'src/utils/set'; export interface EnvData { @@ -184,7 +185,7 @@ const getEnv = (): EnvData => { try { redisConfig = JSON.parse(Buffer.from(redisUrl.slice(10), 'base64').toString()); } catch (error) { - throw new Error(`Failed to decode redis options: ${error}`); + throw new Error('Failed to decode redis options', { cause: error }); } } diff --git a/server/src/repositories/crypto.repository.ts b/server/src/repositories/crypto.repository.ts index bcd791ade2..9b093f6d79 100644 --- a/server/src/repositories/crypto.repository.ts +++ b/server/src/repositories/crypto.repository.ts @@ -23,7 +23,7 @@ export class CryptoRepository { } hashSha256(value: string) { - return createHash('sha256').update(value).digest('base64'); + return createHash('sha256').update(value).digest(); } verifySha256(value: string, encryptedValue: string, publicKey: string) { diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index 650820b18e..06bdef5abf 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -1,3 +1,4 @@ +import { schemaDiff, schemaFromCode, schemaFromDatabase } from '@immich/sql-tools'; import { Injectable } from '@nestjs/common'; import AsyncLock from 'async-lock'; import { FileMigrationProvider, Kysely, Migrator, sql, Transaction } from 'kysely'; @@ -21,7 +22,6 @@ import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import 'src/schema'; // make sure all schema definitions are imported for schemaFromCode import { DB } from 'src/schema'; -import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools'; import { ExtensionVersion, VectorExtension, VectorUpdateResult } from 'src/types'; import { vectorIndexQuery } from 'src/utils/database'; import { isValidInteger } from 'src/validation'; @@ -289,7 +289,8 @@ export class DatabaseRepository { async getSchemaDrift() { const source = schemaFromCode({ overrides: true, namingStrategy: 'default' }); - const target = await schemaFromDatabase(this.db, {}); + const { database } = this.configRepository.getEnv(); + const target = await schemaFromDatabase({ connection: database.config }); const drift = schemaDiff(source, target, { tables: { ignoreExtra: true }, diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index 33025e73cf..e3e78b3238 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -309,9 +309,9 @@ export class MediaRepository { }); } - async getImageDimensions(input: string | Buffer): Promise { - const { width = 0, height = 0 } = await sharp(input).metadata(); - return { width, height }; + async getImageMetadata(input: string | Buffer): Promise { + const { width = 0, height = 0, hasAlpha = false } = await sharp(input).metadata(); + return { width, height, isTransparent: hasAlpha }; } private configureFfmpegCall(input: string, output: string | Writable, options: TranscodeCommand) { diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts index 4500094899..934706d5e1 100644 --- a/server/src/repositories/server-info.repository.ts +++ b/server/src/repositories/server-info.repository.ts @@ -69,7 +69,7 @@ export class ServerInfoRepository { return response.json(); } catch (error) { - throw new Error(`Failed to fetch GitHub release: ${error}`); + throw new Error('Failed to fetch GitHub release', { cause: error }); } } diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts index 52292b8e4a..e008943f21 100644 --- a/server/src/repositories/session.repository.ts +++ b/server/src/repositories/session.repository.ts @@ -48,7 +48,7 @@ export class SessionRepository { } @GenerateSql({ params: [DummyValue.STRING] }) - getByToken(token: string) { + getByToken(token: Buffer) { return this.db .selectFrom('session') .select((eb) => [ diff --git a/server/src/repositories/sync.repository.ts b/server/src/repositories/sync.repository.ts index 511d7b589f..f851038dc6 100644 --- a/server/src/repositories/sync.repository.ts +++ b/server/src/repositories/sync.repository.ts @@ -479,6 +479,8 @@ class AssetFaceSync extends BaseSync { 'boundingBoxX2', 'boundingBoxY2', 'sourceType', + 'isVisible', + 'asset_face.deletedAt', 'asset_face.updateId', ]) .leftJoin('asset', 'asset.id', 'asset_face.assetId') diff --git a/server/src/schema/enums.ts b/server/src/schema/enums.ts index a1134df6bc..c68f152779 100644 --- a/server/src/schema/enums.ts +++ b/server/src/schema/enums.ts @@ -1,5 +1,5 @@ +import { registerEnum } from '@immich/sql-tools'; import { AssetStatus, AssetVisibility, SourceType } from 'src/enum'; -import { registerEnum } from 'src/sql-tools'; export const assets_status_enum = registerEnum({ name: 'assets_status_enum', diff --git a/server/src/schema/functions.ts b/server/src/schema/functions.ts index d7dabfef4c..6acfc45750 100644 --- a/server/src/schema/functions.ts +++ b/server/src/schema/functions.ts @@ -1,4 +1,4 @@ -import { registerFunction } from 'src/sql-tools'; +import { registerFunction } from '@immich/sql-tools'; export const immich_uuid_v7 = registerFunction({ name: 'immich_uuid_v7', diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index f9abcae097..fa80161143 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -1,3 +1,4 @@ +import { Database, Extensions, Generated, Int8 } from '@immich/sql-tools'; import { asset_face_source_type, asset_visibility_enum, assets_status_enum } from 'src/schema/enums'; import { album_delete_audit, @@ -73,7 +74,6 @@ import { UserMetadataTable } from 'src/schema/tables/user-metadata.table'; import { UserTable } from 'src/schema/tables/user.table'; import { VersionHistoryTable } from 'src/schema/tables/version-history.table'; import { WorkflowActionTable, WorkflowFilterTable, WorkflowTable } from 'src/schema/tables/workflow.table'; -import { Database, Extensions, Generated, Int8 } from 'src/sql-tools'; @Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql']) @Database({ name: 'immich' }) diff --git a/server/src/schema/migrations/1771639515206-AddIsTransparentColumn.ts b/server/src/schema/migrations/1771639515206-AddIsTransparentColumn.ts new file mode 100644 index 0000000000..a19d102edb --- /dev/null +++ b/server/src/schema/migrations/1771639515206-AddIsTransparentColumn.ts @@ -0,0 +1,9 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "asset_file" ADD "isTransparent" boolean NOT NULL DEFAULT false;`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "asset_file" DROP COLUMN "isTransparent";`.execute(db); +} diff --git a/server/src/schema/migrations/1771873044511-ChangesTokensToBuffers.ts b/server/src/schema/migrations/1771873044511-ChangesTokensToBuffers.ts new file mode 100644 index 0000000000..b0ed28a55c --- /dev/null +++ b/server/src/schema/migrations/1771873044511-ChangesTokensToBuffers.ts @@ -0,0 +1,15 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "api_key" ALTER COLUMN "key" TYPE bytea USING decode("key", 'base64');`.execute(db); + await sql`ALTER TABLE "session" ALTER COLUMN "token" TYPE bytea USING decode("token", 'base64');`.execute(db); + await sql`CREATE INDEX "api_key_key_idx" ON "api_key" ("key");`.execute(db); + await sql`CREATE INDEX "session_token_idx" ON "session" ("token");`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP INDEX "api_key_key_idx";`.execute(db); + await sql`DROP INDEX "session_token_idx";`.execute(db); + await sql`ALTER TABLE "api_key" ALTER COLUMN "key" TYPE character varying USING encode("key", 'base64');`.execute(db); + await sql`ALTER TABLE "session" ALTER COLUMN "token" TYPE character varying USING encode("token", 'base64');`.execute(db); +} diff --git a/server/src/schema/tables/activity.table.ts b/server/src/schema/tables/activity.table.ts index dfa7c98e42..4a3cc196ee 100644 --- a/server/src/schema/tables/activity.table.ts +++ b/server/src/schema/tables/activity.table.ts @@ -1,8 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { AlbumAssetTable } from 'src/schema/tables/album-asset.table'; -import { AlbumTable } from 'src/schema/tables/album.table'; -import { AssetTable } from 'src/schema/tables/asset.table'; -import { UserTable } from 'src/schema/tables/user.table'; import { Check, Column, @@ -15,7 +10,12 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { AlbumAssetTable } from 'src/schema/tables/album-asset.table'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('activity') @UpdatedAtTrigger('activity_updatedAt') diff --git a/server/src/schema/tables/album-asset-audit.table.ts b/server/src/schema/tables/album-asset-audit.table.ts index ab8fd9ae89..176d32575a 100644 --- a/server/src/schema/tables/album-asset-audit.table.ts +++ b/server/src/schema/tables/album-asset-audit.table.ts @@ -1,6 +1,6 @@ +import { Column, CreateDateColumn, ForeignKeyColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { AlbumTable } from 'src/schema/tables/album.table'; -import { Column, CreateDateColumn, ForeignKeyColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('album_asset_audit') export class AlbumAssetAuditTable { diff --git a/server/src/schema/tables/album-asset.table.ts b/server/src/schema/tables/album-asset.table.ts index dea271239b..5853e846f1 100644 --- a/server/src/schema/tables/album-asset.table.ts +++ b/server/src/schema/tables/album-asset.table.ts @@ -1,7 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { album_asset_delete_audit } from 'src/schema/functions'; -import { AlbumTable } from 'src/schema/tables/album.table'; -import { AssetTable } from 'src/schema/tables/asset.table'; import { AfterDeleteTrigger, CreateDateColumn, @@ -10,7 +6,11 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { album_asset_delete_audit } from 'src/schema/functions'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { AssetTable } from 'src/schema/tables/asset.table'; @Table({ name: 'album_asset' }) @UpdatedAtTrigger('album_asset_updatedAt') diff --git a/server/src/schema/tables/album-audit.table.ts b/server/src/schema/tables/album-audit.table.ts index 432c51c36a..7865f6bfa8 100644 --- a/server/src/schema/tables/album-audit.table.ts +++ b/server/src/schema/tables/album-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('album_audit') export class AlbumAuditTable { diff --git a/server/src/schema/tables/album-user-audit.table.ts b/server/src/schema/tables/album-user-audit.table.ts index 2259511bdd..d4798761e0 100644 --- a/server/src/schema/tables/album-user-audit.table.ts +++ b/server/src/schema/tables/album-user-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('album_user_audit') export class AlbumUserAuditTable { diff --git a/server/src/schema/tables/album-user.table.ts b/server/src/schema/tables/album-user.table.ts index 761aabc1af..2e38041daf 100644 --- a/server/src/schema/tables/album-user.table.ts +++ b/server/src/schema/tables/album-user.table.ts @@ -1,8 +1,3 @@ -import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { AlbumUserRole } from 'src/enum'; -import { album_user_after_insert, album_user_delete_audit } from 'src/schema/functions'; -import { AlbumTable } from 'src/schema/tables/album.table'; -import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, AfterInsertTrigger, @@ -13,7 +8,12 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { AlbumUserRole } from 'src/enum'; +import { album_user_after_insert, album_user_delete_audit } from 'src/schema/functions'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { UserTable } from 'src/schema/tables/user.table'; @Table({ name: 'album_user' }) // Pre-existing indices from original album <--> user ManyToMany mapping diff --git a/server/src/schema/tables/album.table.ts b/server/src/schema/tables/album.table.ts index 5628db3d03..81b846c0f4 100644 --- a/server/src/schema/tables/album.table.ts +++ b/server/src/schema/tables/album.table.ts @@ -1,8 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { AssetOrder } from 'src/enum'; -import { album_delete_audit } from 'src/schema/functions'; -import { AssetTable } from 'src/schema/tables/asset.table'; -import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, Column, @@ -14,7 +9,12 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { AssetOrder } from 'src/enum'; +import { album_delete_audit } from 'src/schema/functions'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { UserTable } from 'src/schema/tables/user.table'; @Table({ name: 'album' }) @UpdatedAtTrigger('album_updatedAt') diff --git a/server/src/schema/tables/api-key.table.ts b/server/src/schema/tables/api-key.table.ts index efbf18afaa..f0e33a9c71 100644 --- a/server/src/schema/tables/api-key.table.ts +++ b/server/src/schema/tables/api-key.table.ts @@ -1,6 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { Permission } from 'src/enum'; -import { UserTable } from 'src/schema/tables/user.table'; import { Column, CreateDateColumn, @@ -10,7 +7,10 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { Permission } from 'src/enum'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('api_key') @UpdatedAtTrigger('api_key_updatedAt') @@ -21,8 +21,8 @@ export class ApiKeyTable { @Column() name!: string; - @Column() - key!: string; + @Column({ type: 'bytea', index: true }) + key!: Buffer; @ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) userId!: string; diff --git a/server/src/schema/tables/asset-audit.table.ts b/server/src/schema/tables/asset-audit.table.ts index 86c3f6f28b..fee6dde59a 100644 --- a/server/src/schema/tables/asset-audit.table.ts +++ b/server/src/schema/tables/asset-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('asset_audit') export class AssetAuditTable { diff --git a/server/src/schema/tables/asset-edit.table.ts b/server/src/schema/tables/asset-edit.table.ts index 886b62dc0b..1ec6bf081c 100644 --- a/server/src/schema/tables/asset-edit.table.ts +++ b/server/src/schema/tables/asset-edit.table.ts @@ -1,6 +1,3 @@ -import { AssetEditAction, AssetEditActionParameter } from 'src/dtos/editing.dto'; -import { asset_edit_delete, asset_edit_insert } from 'src/schema/functions'; -import { AssetTable } from 'src/schema/tables/asset.table'; import { AfterDeleteTrigger, AfterInsertTrigger, @@ -10,7 +7,10 @@ import { PrimaryGeneratedColumn, Table, Unique, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { AssetEditAction, AssetEditParameters } from 'src/dtos/editing.dto'; +import { asset_edit_delete, asset_edit_insert } from 'src/schema/functions'; +import { AssetTable } from 'src/schema/tables/asset.table'; @Table('asset_edit') @AfterInsertTrigger({ scope: 'statement', function: asset_edit_insert, referencingNewTableAs: 'inserted_edit' }) @@ -21,7 +21,7 @@ import { when: 'pg_trigger_depth() = 0', }) @Unique({ columns: ['assetId', 'sequence'] }) -export class AssetEditTable { +export class AssetEditTable { @PrimaryGeneratedColumn() id!: Generated; @@ -29,10 +29,10 @@ export class AssetEditTable { assetId!: string; @Column() - action!: T; + action!: AssetEditAction; @Column({ type: 'jsonb' }) - parameters!: AssetEditActionParameter[T]; + parameters!: AssetEditParameters; @Column({ type: 'integer' }) sequence!: number; diff --git a/server/src/schema/tables/asset-exif.table.ts b/server/src/schema/tables/asset-exif.table.ts index 9dacb547cf..1ae8f731a9 100644 --- a/server/src/schema/tables/asset-exif.table.ts +++ b/server/src/schema/tables/asset-exif.table.ts @@ -1,7 +1,7 @@ +import { Column, ForeignKeyColumn, Generated, Int8, Table, Timestamp, UpdateDateColumn } from '@immich/sql-tools'; import { LockableProperty } from 'src/database'; import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { Column, ForeignKeyColumn, Generated, Int8, Table, Timestamp, UpdateDateColumn } from 'src/sql-tools'; @Table('asset_exif') @UpdatedAtTrigger('asset_exif_updatedAt') diff --git a/server/src/schema/tables/asset-face-audit.table.ts b/server/src/schema/tables/asset-face-audit.table.ts index 4f03c22aa0..2e61904800 100644 --- a/server/src/schema/tables/asset-face-audit.table.ts +++ b/server/src/schema/tables/asset-face-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('asset_face_audit') export class AssetFaceAuditTable { diff --git a/server/src/schema/tables/asset-face.table.ts b/server/src/schema/tables/asset-face.table.ts index 8a3b3ac611..b67e5e5dac 100644 --- a/server/src/schema/tables/asset-face.table.ts +++ b/server/src/schema/tables/asset-face.table.ts @@ -1,9 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { SourceType } from 'src/enum'; -import { asset_face_source_type } from 'src/schema/enums'; -import { asset_face_audit } from 'src/schema/functions'; -import { AssetTable } from 'src/schema/tables/asset.table'; -import { PersonTable } from 'src/schema/tables/person.table'; import { AfterDeleteTrigger, Column, @@ -15,7 +9,13 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { SourceType } from 'src/enum'; +import { asset_face_source_type } from 'src/schema/enums'; +import { asset_face_audit } from 'src/schema/functions'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { PersonTable } from 'src/schema/tables/person.table'; @Table({ name: 'asset_face' }) @UpdatedAtTrigger('asset_face_updatedAt') diff --git a/server/src/schema/tables/asset-file.table.ts b/server/src/schema/tables/asset-file.table.ts index 73b5171a47..6285e4d653 100644 --- a/server/src/schema/tables/asset-file.table.ts +++ b/server/src/schema/tables/asset-file.table.ts @@ -1,6 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { AssetFileType } from 'src/enum'; -import { AssetTable } from 'src/schema/tables/asset.table'; import { Column, CreateDateColumn, @@ -11,7 +8,10 @@ import { Timestamp, Unique, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { AssetFileType } from 'src/enum'; +import { AssetTable } from 'src/schema/tables/asset.table'; @Table('asset_file') @Unique({ columns: ['assetId', 'type', 'isEdited'] }) @@ -43,4 +43,7 @@ export class AssetFileTable { @Column({ type: 'boolean', default: false }) isProgressive!: Generated; + + @Column({ type: 'boolean', default: false }) + isTransparent!: Generated; } diff --git a/server/src/schema/tables/asset-job-status.table.ts b/server/src/schema/tables/asset-job-status.table.ts index 62194825e5..4d889ade46 100644 --- a/server/src/schema/tables/asset-job-status.table.ts +++ b/server/src/schema/tables/asset-job-status.table.ts @@ -1,5 +1,5 @@ +import { Column, ForeignKeyColumn, Table, Timestamp } from '@immich/sql-tools'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { Column, ForeignKeyColumn, Table, Timestamp } from 'src/sql-tools'; @Table('asset_job_status') export class AssetJobStatusTable { diff --git a/server/src/schema/tables/asset-metadata-audit.table.ts b/server/src/schema/tables/asset-metadata-audit.table.ts index 16272eacf7..15c0b47edc 100644 --- a/server/src/schema/tables/asset-metadata-audit.table.ts +++ b/server/src/schema/tables/asset-metadata-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('asset_metadata_audit') export class AssetMetadataAuditTable { diff --git a/server/src/schema/tables/asset-metadata.table.ts b/server/src/schema/tables/asset-metadata.table.ts index 8a7af1360f..53e3121a41 100644 --- a/server/src/schema/tables/asset-metadata.table.ts +++ b/server/src/schema/tables/asset-metadata.table.ts @@ -1,7 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { AssetMetadataKey } from 'src/enum'; -import { asset_metadata_audit } from 'src/schema/functions'; -import { AssetTable } from 'src/schema/tables/asset.table'; import { AfterDeleteTrigger, Column, @@ -11,7 +7,11 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { AssetMetadataKey } from 'src/enum'; +import { asset_metadata_audit } from 'src/schema/functions'; +import { AssetTable } from 'src/schema/tables/asset.table'; @UpdatedAtTrigger('asset_metadata_updated_at') @Table('asset_metadata') diff --git a/server/src/schema/tables/asset-ocr.table.ts b/server/src/schema/tables/asset-ocr.table.ts index b9b0838cbe..b58224a247 100644 --- a/server/src/schema/tables/asset-ocr.table.ts +++ b/server/src/schema/tables/asset-ocr.table.ts @@ -1,5 +1,5 @@ +import { Column, ForeignKeyColumn, Generated, PrimaryGeneratedColumn, Table } from '@immich/sql-tools'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { Column, ForeignKeyColumn, Generated, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; @Table('asset_ocr') export class AssetOcrTable { diff --git a/server/src/schema/tables/asset.table.ts b/server/src/schema/tables/asset.table.ts index ffa6a7547f..7c3e9392f2 100644 --- a/server/src/schema/tables/asset.table.ts +++ b/server/src/schema/tables/asset.table.ts @@ -1,10 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { AssetStatus, AssetType, AssetVisibility } from 'src/enum'; -import { asset_visibility_enum, assets_status_enum } from 'src/schema/enums'; -import { asset_delete_audit } from 'src/schema/functions'; -import { LibraryTable } from 'src/schema/tables/library.table'; -import { StackTable } from 'src/schema/tables/stack.table'; -import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, Column, @@ -17,7 +10,14 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { AssetStatus, AssetType, AssetVisibility } from 'src/enum'; +import { asset_visibility_enum, assets_status_enum } from 'src/schema/enums'; +import { asset_delete_audit } from 'src/schema/functions'; +import { LibraryTable } from 'src/schema/tables/library.table'; +import { StackTable } from 'src/schema/tables/stack.table'; +import { UserTable } from 'src/schema/tables/user.table'; import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database'; @Table('asset') diff --git a/server/src/schema/tables/audit.table.ts b/server/src/schema/tables/audit.table.ts index 15b4990814..78c9a57c09 100644 --- a/server/src/schema/tables/audit.table.ts +++ b/server/src/schema/tables/audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Index, PrimaryColumn, Table, Timestamp } from '@immich/sql-tools'; import { DatabaseAction, EntityType } from 'src/enum'; -import { Column, CreateDateColumn, Generated, Index, PrimaryColumn, Table, Timestamp } from 'src/sql-tools'; @Table('audit') @Index({ columns: ['ownerId', 'createdAt'] }) diff --git a/server/src/schema/tables/face-search.table.ts b/server/src/schema/tables/face-search.table.ts index ff63879404..7c585437c8 100644 --- a/server/src/schema/tables/face-search.table.ts +++ b/server/src/schema/tables/face-search.table.ts @@ -1,5 +1,5 @@ +import { Column, ForeignKeyColumn, Index, Table } from '@immich/sql-tools'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; -import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; @Table({ name: 'face_search' }) @Index({ diff --git a/server/src/schema/tables/geodata-places.table.ts b/server/src/schema/tables/geodata-places.table.ts index eec2b240d0..101ddb759f 100644 --- a/server/src/schema/tables/geodata-places.table.ts +++ b/server/src/schema/tables/geodata-places.table.ts @@ -1,4 +1,4 @@ -import { Column, Index, PrimaryColumn, Table, Timestamp } from 'src/sql-tools'; +import { Column, Index, PrimaryColumn, Table, Timestamp } from '@immich/sql-tools'; @Table({ name: 'geodata_places', primaryConstraintName: 'geodata_places_pkey' }) @Index({ diff --git a/server/src/schema/tables/library.table.ts b/server/src/schema/tables/library.table.ts index 57ad144c8e..2f79a3e78d 100644 --- a/server/src/schema/tables/library.table.ts +++ b/server/src/schema/tables/library.table.ts @@ -1,5 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { UserTable } from 'src/schema/tables/user.table'; import { Column, CreateDateColumn, @@ -10,7 +8,9 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('library') @UpdatedAtTrigger('library_updatedAt') diff --git a/server/src/schema/tables/memory-asset-audit.table.ts b/server/src/schema/tables/memory-asset-audit.table.ts index 218c2f19ff..67c434c45a 100644 --- a/server/src/schema/tables/memory-asset-audit.table.ts +++ b/server/src/schema/tables/memory-asset-audit.table.ts @@ -1,6 +1,6 @@ +import { Column, CreateDateColumn, ForeignKeyColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { MemoryTable } from 'src/schema/tables/memory.table'; -import { Column, CreateDateColumn, ForeignKeyColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('memory_asset_audit') export class MemoryAssetAuditTable { diff --git a/server/src/schema/tables/memory-asset.table.ts b/server/src/schema/tables/memory-asset.table.ts index b162000ca0..b44c78c3b9 100644 --- a/server/src/schema/tables/memory-asset.table.ts +++ b/server/src/schema/tables/memory-asset.table.ts @@ -1,7 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { memory_asset_delete_audit } from 'src/schema/functions'; -import { AssetTable } from 'src/schema/tables/asset.table'; -import { MemoryTable } from 'src/schema/tables/memory.table'; import { AfterDeleteTrigger, CreateDateColumn, @@ -10,7 +6,11 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { memory_asset_delete_audit } from 'src/schema/functions'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { MemoryTable } from 'src/schema/tables/memory.table'; @Table('memory_asset') @UpdatedAtTrigger('memory_asset_updatedAt') diff --git a/server/src/schema/tables/memory-audit.table.ts b/server/src/schema/tables/memory-audit.table.ts index 167caf8e6e..6d278676b7 100644 --- a/server/src/schema/tables/memory-audit.table.ts +++ b/server/src/schema/tables/memory-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('memory_audit') export class MemoryAuditTable { diff --git a/server/src/schema/tables/memory.table.ts b/server/src/schema/tables/memory.table.ts index 408f7bca19..8b9867b4cc 100644 --- a/server/src/schema/tables/memory.table.ts +++ b/server/src/schema/tables/memory.table.ts @@ -1,7 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { MemoryType } from 'src/enum'; -import { memory_delete_audit } from 'src/schema/functions'; -import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, Column, @@ -13,7 +9,11 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { MemoryType } from 'src/enum'; +import { memory_delete_audit } from 'src/schema/functions'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('memory') @UpdatedAtTrigger('memory_updatedAt') diff --git a/server/src/schema/tables/move.table.ts b/server/src/schema/tables/move.table.ts index 1afda2767a..c7229431f7 100644 --- a/server/src/schema/tables/move.table.ts +++ b/server/src/schema/tables/move.table.ts @@ -1,5 +1,5 @@ +import { Column, Generated, PrimaryGeneratedColumn, Table, Unique } from '@immich/sql-tools'; import { PathType } from 'src/enum'; -import { Column, Generated, PrimaryGeneratedColumn, Table, Unique } from 'src/sql-tools'; @Table('move_history') // path lock (per entity) diff --git a/server/src/schema/tables/natural-earth-countries.table.ts b/server/src/schema/tables/natural-earth-countries.table.ts index c59d15fc21..06f189264e 100644 --- a/server/src/schema/tables/natural-earth-countries.table.ts +++ b/server/src/schema/tables/natural-earth-countries.table.ts @@ -1,4 +1,4 @@ -import { Column, Generated, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; +import { Column, Generated, PrimaryGeneratedColumn, Table } from '@immich/sql-tools'; @Table({ name: 'naturalearth_countries', primaryConstraintName: 'naturalearth_countries_pkey' }) export class NaturalEarthCountriesTable { diff --git a/server/src/schema/tables/notification.table.ts b/server/src/schema/tables/notification.table.ts index 01a93a73e5..6bf65808f1 100644 --- a/server/src/schema/tables/notification.table.ts +++ b/server/src/schema/tables/notification.table.ts @@ -1,6 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { NotificationLevel, NotificationType } from 'src/enum'; -import { UserTable } from 'src/schema/tables/user.table'; import { Column, CreateDateColumn, @@ -11,7 +8,10 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { NotificationLevel, NotificationType } from 'src/enum'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('notification') @UpdatedAtTrigger('notification_updatedAt') diff --git a/server/src/schema/tables/ocr-search.table.ts b/server/src/schema/tables/ocr-search.table.ts index 3449725adb..74aefb333b 100644 --- a/server/src/schema/tables/ocr-search.table.ts +++ b/server/src/schema/tables/ocr-search.table.ts @@ -1,5 +1,5 @@ +import { Column, ForeignKeyColumn, Index, Table } from '@immich/sql-tools'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; @Table('ocr_search') @Index({ diff --git a/server/src/schema/tables/partner-audit.table.ts b/server/src/schema/tables/partner-audit.table.ts index fa2f0c27cc..3cfd1854e1 100644 --- a/server/src/schema/tables/partner-audit.table.ts +++ b/server/src/schema/tables/partner-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('partner_audit') export class PartnerAuditTable { diff --git a/server/src/schema/tables/partner.table.ts b/server/src/schema/tables/partner.table.ts index 8fc332cb12..408cac650f 100644 --- a/server/src/schema/tables/partner.table.ts +++ b/server/src/schema/tables/partner.table.ts @@ -1,6 +1,3 @@ -import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { partner_delete_audit } from 'src/schema/functions'; -import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, Column, @@ -10,7 +7,10 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { partner_delete_audit } from 'src/schema/functions'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('partner') @UpdatedAtTrigger('partner_updatedAt') diff --git a/server/src/schema/tables/person-audit.table.ts b/server/src/schema/tables/person-audit.table.ts index 8a899a1808..4fb55f1744 100644 --- a/server/src/schema/tables/person-audit.table.ts +++ b/server/src/schema/tables/person-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('person_audit') export class PersonAuditTable { diff --git a/server/src/schema/tables/person.table.ts b/server/src/schema/tables/person.table.ts index 3b523a39d2..02fb85b757 100644 --- a/server/src/schema/tables/person.table.ts +++ b/server/src/schema/tables/person.table.ts @@ -1,7 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { person_delete_audit } from 'src/schema/functions'; -import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; -import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, Check, @@ -13,7 +9,11 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { person_delete_audit } from 'src/schema/functions'; +import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('person') @UpdatedAtTrigger('person_updatedAt') diff --git a/server/src/schema/tables/plugin.table.ts b/server/src/schema/tables/plugin.table.ts index 3de7ca63c9..5f82807f23 100644 --- a/server/src/schema/tables/plugin.table.ts +++ b/server/src/schema/tables/plugin.table.ts @@ -1,4 +1,3 @@ -import { PluginContext } from 'src/enum'; import { Column, CreateDateColumn, @@ -9,7 +8,8 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { PluginContext } from 'src/enum'; import type { JSONSchema } from 'src/types/plugin-schema.types'; @Table('plugin') diff --git a/server/src/schema/tables/session.table.ts b/server/src/schema/tables/session.table.ts index 466152d35d..e57628d6da 100644 --- a/server/src/schema/tables/session.table.ts +++ b/server/src/schema/tables/session.table.ts @@ -1,5 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { UserTable } from 'src/schema/tables/user.table'; import { Column, CreateDateColumn, @@ -9,7 +7,9 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { UserTable } from 'src/schema/tables/user.table'; @Table({ name: 'session' }) @UpdatedAtTrigger('session_updatedAt') @@ -17,9 +17,8 @@ export class SessionTable { @PrimaryGeneratedColumn() id!: Generated; - // TODO convert to byte[] - @Column() - token!: string; + @Column({ type: 'bytea', index: true }) + token!: Buffer; @CreateDateColumn() createdAt!: Generated; diff --git a/server/src/schema/tables/shared-link-asset.table.ts b/server/src/schema/tables/shared-link-asset.table.ts index 37e6a3d9f0..ff96f69980 100644 --- a/server/src/schema/tables/shared-link-asset.table.ts +++ b/server/src/schema/tables/shared-link-asset.table.ts @@ -1,6 +1,6 @@ +import { ForeignKeyColumn, Table } from '@immich/sql-tools'; import { AssetTable } from 'src/schema/tables/asset.table'; import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; -import { ForeignKeyColumn, Table } from 'src/sql-tools'; @Table('shared_link_asset') export class SharedLinkAssetTable { diff --git a/server/src/schema/tables/shared-link.table.ts b/server/src/schema/tables/shared-link.table.ts index 80e2d7cdf4..d99520388a 100644 --- a/server/src/schema/tables/shared-link.table.ts +++ b/server/src/schema/tables/shared-link.table.ts @@ -1,6 +1,3 @@ -import { SharedLinkType } from 'src/enum'; -import { AlbumTable } from 'src/schema/tables/album.table'; -import { UserTable } from 'src/schema/tables/user.table'; import { Column, CreateDateColumn, @@ -9,7 +6,10 @@ import { PrimaryGeneratedColumn, Table, Timestamp, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { SharedLinkType } from 'src/enum'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('shared_link') export class SharedLinkTable { diff --git a/server/src/schema/tables/smart-search.table.ts b/server/src/schema/tables/smart-search.table.ts index dc140efb2f..31071e6134 100644 --- a/server/src/schema/tables/smart-search.table.ts +++ b/server/src/schema/tables/smart-search.table.ts @@ -1,5 +1,5 @@ +import { Column, ForeignKeyColumn, Index, Table } from '@immich/sql-tools'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; @Table({ name: 'smart_search' }) @Index({ diff --git a/server/src/schema/tables/stack-audit.table.ts b/server/src/schema/tables/stack-audit.table.ts index d46ff95e57..3a62545cd2 100644 --- a/server/src/schema/tables/stack-audit.table.ts +++ b/server/src/schema/tables/stack-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('stack_audit') export class StackAuditTable { diff --git a/server/src/schema/tables/stack.table.ts b/server/src/schema/tables/stack.table.ts index 9c9eb81373..3f903e065a 100644 --- a/server/src/schema/tables/stack.table.ts +++ b/server/src/schema/tables/stack.table.ts @@ -1,7 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { stack_delete_audit } from 'src/schema/functions'; -import { AssetTable } from 'src/schema/tables/asset.table'; -import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, CreateDateColumn, @@ -11,7 +7,11 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { stack_delete_audit } from 'src/schema/functions'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('stack') @UpdatedAtTrigger('stack_updatedAt') diff --git a/server/src/schema/tables/sync-checkpoint.table.ts b/server/src/schema/tables/sync-checkpoint.table.ts index 6ad4c54a86..d9ada5aed0 100644 --- a/server/src/schema/tables/sync-checkpoint.table.ts +++ b/server/src/schema/tables/sync-checkpoint.table.ts @@ -1,6 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { SyncEntityType } from 'src/enum'; -import { SessionTable } from 'src/schema/tables/session.table'; import { Column, CreateDateColumn, @@ -10,7 +7,10 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { SyncEntityType } from 'src/enum'; +import { SessionTable } from 'src/schema/tables/session.table'; @Table('session_sync_checkpoint') @UpdatedAtTrigger('session_sync_checkpoint_updatedAt') diff --git a/server/src/schema/tables/system-metadata.table.ts b/server/src/schema/tables/system-metadata.table.ts index 8657768db6..9f21172505 100644 --- a/server/src/schema/tables/system-metadata.table.ts +++ b/server/src/schema/tables/system-metadata.table.ts @@ -1,5 +1,5 @@ +import { Column, PrimaryColumn, Table } from '@immich/sql-tools'; import { SystemMetadataKey } from 'src/enum'; -import { Column, PrimaryColumn, Table } from 'src/sql-tools'; import { SystemMetadata } from 'src/types'; @Table('system_metadata') diff --git a/server/src/schema/tables/tag-asset.table.ts b/server/src/schema/tables/tag-asset.table.ts index 3ea2361b4f..9d7ea026c6 100644 --- a/server/src/schema/tables/tag-asset.table.ts +++ b/server/src/schema/tables/tag-asset.table.ts @@ -1,6 +1,6 @@ +import { ForeignKeyColumn, Index, Table } from '@immich/sql-tools'; import { AssetTable } from 'src/schema/tables/asset.table'; import { TagTable } from 'src/schema/tables/tag.table'; -import { ForeignKeyColumn, Index, Table } from 'src/sql-tools'; @Index({ columns: ['assetId', 'tagId'] }) @Table('tag_asset') diff --git a/server/src/schema/tables/tag-closure.table.ts b/server/src/schema/tables/tag-closure.table.ts index aeb8c8cf11..2e1c83a20f 100644 --- a/server/src/schema/tables/tag-closure.table.ts +++ b/server/src/schema/tables/tag-closure.table.ts @@ -1,5 +1,5 @@ +import { ForeignKeyColumn, Table } from '@immich/sql-tools'; import { TagTable } from 'src/schema/tables/tag.table'; -import { ForeignKeyColumn, Table } from 'src/sql-tools'; @Table('tag_closure') export class TagClosureTable { diff --git a/server/src/schema/tables/tag.table.ts b/server/src/schema/tables/tag.table.ts index dc1fa2947b..2a07239d84 100644 --- a/server/src/schema/tables/tag.table.ts +++ b/server/src/schema/tables/tag.table.ts @@ -1,5 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { UserTable } from 'src/schema/tables/user.table'; import { Column, CreateDateColumn, @@ -10,7 +8,9 @@ import { Timestamp, Unique, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('tag') @UpdatedAtTrigger('tag_updatedAt') diff --git a/server/src/schema/tables/user-audit.table.ts b/server/src/schema/tables/user-audit.table.ts index 084b42fb65..36f89dfa7d 100644 --- a/server/src/schema/tables/user-audit.table.ts +++ b/server/src/schema/tables/user-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('user_audit') export class UserAuditTable { diff --git a/server/src/schema/tables/user-metadata-audit.table.ts b/server/src/schema/tables/user-metadata-audit.table.ts index 63f503ab85..17dee673b4 100644 --- a/server/src/schema/tables/user-metadata-audit.table.ts +++ b/server/src/schema/tables/user-metadata-audit.table.ts @@ -1,6 +1,6 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { UserMetadataKey } from 'src/enum'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('user_metadata_audit') export class UserMetadataAuditTable { diff --git a/server/src/schema/tables/user-metadata.table.ts b/server/src/schema/tables/user-metadata.table.ts index a453ec6677..6983ed3dda 100644 --- a/server/src/schema/tables/user-metadata.table.ts +++ b/server/src/schema/tables/user-metadata.table.ts @@ -1,7 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { UserMetadataKey } from 'src/enum'; -import { user_metadata_audit } from 'src/schema/functions'; -import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, Column, @@ -11,7 +7,11 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { UserMetadataKey } from 'src/enum'; +import { user_metadata_audit } from 'src/schema/functions'; +import { UserTable } from 'src/schema/tables/user.table'; import { UserMetadata, UserMetadataItem } from 'src/types'; @UpdatedAtTrigger('user_metadata_updated_at') diff --git a/server/src/schema/tables/user.table.ts b/server/src/schema/tables/user.table.ts index 46d6656382..3a340d976b 100644 --- a/server/src/schema/tables/user.table.ts +++ b/server/src/schema/tables/user.table.ts @@ -1,7 +1,3 @@ -import { ColumnType } from 'kysely'; -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { UserAvatarColor, UserStatus } from 'src/enum'; -import { user_delete_audit } from 'src/schema/functions'; import { AfterDeleteTrigger, Column, @@ -13,7 +9,11 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { ColumnType } from 'kysely'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { UserAvatarColor, UserStatus } from 'src/enum'; +import { user_delete_audit } from 'src/schema/functions'; @Table('user') @UpdatedAtTrigger('user_updatedAt') diff --git a/server/src/schema/tables/version-history.table.ts b/server/src/schema/tables/version-history.table.ts index 143852c527..12eab7fd69 100644 --- a/server/src/schema/tables/version-history.table.ts +++ b/server/src/schema/tables/version-history.table.ts @@ -1,4 +1,4 @@ -import { Column, CreateDateColumn, Generated, PrimaryGeneratedColumn, Table, Timestamp } from 'src/sql-tools'; +import { Column, CreateDateColumn, Generated, PrimaryGeneratedColumn, Table, Timestamp } from '@immich/sql-tools'; @Table('version_history') export class VersionHistoryTable { diff --git a/server/src/schema/tables/workflow.table.ts b/server/src/schema/tables/workflow.table.ts index 62a5531d8e..163518e039 100644 --- a/server/src/schema/tables/workflow.table.ts +++ b/server/src/schema/tables/workflow.table.ts @@ -1,6 +1,3 @@ -import { PluginTriggerType } from 'src/enum'; -import { PluginActionTable, PluginFilterTable } from 'src/schema/tables/plugin.table'; -import { UserTable } from 'src/schema/tables/user.table'; import { Column, CreateDateColumn, @@ -10,7 +7,10 @@ import { PrimaryGeneratedColumn, Table, Timestamp, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { PluginTriggerType } from 'src/enum'; +import { PluginActionTable, PluginFilterTable } from 'src/schema/tables/plugin.table'; +import { UserTable } from 'src/schema/tables/user.table'; import type { ActionConfig, FilterConfig } from 'src/types/plugin-schema.types'; @Table('workflow') diff --git a/server/src/services/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts index 14544f454f..3a31dbbea1 100644 --- a/server/src/services/api-key.service.spec.ts +++ b/server/src/services/api-key.service.spec.ts @@ -24,7 +24,7 @@ describe(ApiKeyService.name, () => { await sut.create(auth, { name: apiKey.name, permissions: apiKey.permissions }); expect(mocks.apiKey.create).toHaveBeenCalledWith({ - key: 'super-secret (hashed)', + key: Buffer.from('super-secret (hashed)'), name: apiKey.name, permissions: apiKey.permissions, userId: apiKey.userId, @@ -44,7 +44,7 @@ describe(ApiKeyService.name, () => { await sut.create(auth, { permissions: [Permission.All] }); expect(mocks.apiKey.create).toHaveBeenCalledWith({ - key: 'super-secret (hashed)', + key: Buffer.from('super-secret (hashed)'), name: 'API Key', permissions: [Permission.All], userId: auth.user.id, diff --git a/server/src/services/api-key.service.ts b/server/src/services/api-key.service.ts index 492ee9c0fd..534de69107 100644 --- a/server/src/services/api-key.service.ts +++ b/server/src/services/api-key.service.ts @@ -10,14 +10,14 @@ import { isGranted } from 'src/utils/access'; export class ApiKeyService extends BaseService { async create(auth: AuthDto, dto: APIKeyCreateDto): Promise { const token = this.cryptoRepository.randomBytesAsText(32); - const tokenHashed = this.cryptoRepository.hashSha256(token); + const hashed = this.cryptoRepository.hashSha256(token); if (auth.apiKey && !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })) { throw new BadRequestException('Cannot grant permissions you do not have'); } const entity = await this.apiKeyRepository.create({ - key: tokenHashed, + key: hashed, name: dto.name || 'API Key', userId: auth.user.id, permissions: dto.permissions, diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index f6098248ed..32225545bb 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -21,7 +21,7 @@ import { mapStats, } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetEditAction, AssetEditActionCrop, AssetEditActionListDto, AssetEditsDto } from 'src/dtos/editing.dto'; +import { AssetEditAction, AssetEditActionItem, AssetEditsCreateDto, AssetEditsResponseDto } from 'src/dtos/editing.dto'; import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; import { AssetFileType, @@ -543,7 +543,7 @@ export class AssetService extends BaseService { } } - async getAssetEdits(auth: AuthDto, id: string): Promise { + async getAssetEdits(auth: AuthDto, id: string): Promise { await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] }); const edits = await this.assetEditRepository.getAll(id); return { @@ -552,7 +552,7 @@ export class AssetService extends BaseService { }; } - async editAsset(auth: AuthDto, id: string, dto: AssetEditActionListDto): Promise { + async editAsset(auth: AuthDto, id: string, dto: AssetEditsCreateDto): Promise { await this.requireAccess({ auth, permission: Permission.AssetEditCreate, ids: [id] }); const asset = await this.assetRepository.getForEdit(id); @@ -587,12 +587,13 @@ export class AssetService extends BaseService { throw new BadRequestException('Asset dimensions are not available for editing'); } - const cropIndex = dto.edits.findIndex((e) => e.action === AssetEditAction.Crop); - if (cropIndex > 0) { - throw new BadRequestException('Crop action must be the first edit action'); - } - const crop = cropIndex === -1 ? null : (dto.edits[cropIndex] as AssetEditActionCrop); + const edits = dto.edits as AssetEditActionItem[]; + const crop = edits.find((e) => e.action === AssetEditAction.Crop); if (crop) { + if (edits[0].action !== AssetEditAction.Crop) { + throw new BadRequestException('Crop action must be the first edit action'); + } + // check that crop parameters will not go out of bounds const { width: assetWidth, height: assetHeight } = getDimensions(asset); @@ -606,7 +607,7 @@ export class AssetService extends BaseService { } } - const newEdits = await this.assetEditRepository.replaceAll(id, dto.edits); + const newEdits = await this.assetEditRepository.replaceAll(id, edits); await this.jobRepository.queue({ name: JobName.AssetEditThumbnailGeneration, data: { id } }); // Return the asset and its applied edits diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index a34efedfb0..81f601da0a 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -513,7 +513,7 @@ describe(AuthService.name, () => { metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, }), ).rejects.toBeInstanceOf(UnauthorizedException); - expect(mocks.apiKey.getKey).toHaveBeenCalledWith('auth_token (hashed)'); + expect(mocks.apiKey.getKey).toHaveBeenCalledWith(Buffer.from('auth_token (hashed)')); }); it('should throw an error if api key has insufficient permissions', async () => { @@ -574,7 +574,7 @@ describe(AuthService.name, () => { metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, }), ).resolves.toEqual({ user: authUser, apiKey: expect.objectContaining(authApiKey) }); - expect(mocks.apiKey.getKey).toHaveBeenCalledWith('auth_token (hashed)'); + expect(mocks.apiKey.getKey).toHaveBeenCalledWith(Buffer.from('auth_token (hashed)')); }); }); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index a6580f89dd..b8e1b78107 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -456,8 +456,8 @@ export class AuthService extends BaseService { } private async validateApiKey(key: string): Promise { - const hashedKey = this.cryptoRepository.hashSha256(key); - const apiKey = await this.apiKeyRepository.getKey(hashedKey); + const hashed = this.cryptoRepository.hashSha256(key); + const apiKey = await this.apiKeyRepository.getKey(hashed); if (apiKey?.user) { return { user: apiKey.user, @@ -476,9 +476,9 @@ export class AuthService extends BaseService { return this.cryptoRepository.compareBcrypt(inputSecret, existingHash); } - private async validateSession(tokenValue: string, headers: IncomingHttpHeaders): Promise { - const hashedToken = this.cryptoRepository.hashSha256(tokenValue); - const session = await this.sessionRepository.getByToken(hashedToken); + private async validateSession(token: string, headers: IncomingHttpHeaders): Promise { + const hashed = this.cryptoRepository.hashSha256(token); + const session = await this.sessionRepository.getByToken(hashed); if (session?.user) { const { appVersion, deviceOS, deviceType } = getUserAgentDetails(headers); const now = DateTime.now(); @@ -543,10 +543,10 @@ export class AuthService extends BaseService { private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) { const token = this.cryptoRepository.randomBytesAsText(32); - const tokenHashed = this.cryptoRepository.hashSha256(token); + const hashed = this.cryptoRepository.hashSha256(token); await this.sessionRepository.create({ - token: tokenHashed, + token: hashed, deviceOS: loginDetails.deviceOS, deviceType: loginDetails.deviceType, appVersion: loginDetails.appVersion, diff --git a/server/src/services/cli.service.ts b/server/src/services/cli.service.ts index 479fd130a6..22f06e2ed9 100644 --- a/server/src/services/cli.service.ts +++ b/server/src/services/cli.service.ts @@ -1,3 +1,4 @@ +import { schemaDiff } from '@immich/sql-tools'; import { Injectable } from '@nestjs/common'; import { isAbsolute, join } from 'node:path'; import { SALT_ROUNDS } from 'src/constants'; @@ -5,7 +6,6 @@ import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { MaintenanceAction, SystemMetadataKey } from 'src/enum'; import { BaseService } from 'src/services/base.service'; -import { schemaDiff } from 'src/sql-tools'; import { createMaintenanceLoginUrl, generateMaintenanceSecret } from 'src/utils/maintenance'; import { getExternalDomain } from 'src/utils/misc'; diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 399eb5d6a0..5317989739 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -348,6 +348,7 @@ describe(MediaService.name, () => { : { data: fullsizeBuffer, info: rawInfo as OutputInfo }, // buffer implies embedded image extracted ), ); + mocks.media.getImageMetadata.mockResolvedValue({ width: 100, height: 100, isTransparent: false }); }); it('should skip thumbnail generation if asset not found', async () => { @@ -467,6 +468,7 @@ describe(MediaService.name, () => { path: expect.any(String), isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: asset.id, @@ -474,6 +476,7 @@ describe(MediaService.name, () => { path: expect.any(String), isEdited: false, isProgressive: false, + isTransparent: false, }, ]); expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, thumbhash: thumbhashBuffer }); @@ -508,6 +511,7 @@ describe(MediaService.name, () => { path: expect.any(String), isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: asset.id, @@ -515,6 +519,7 @@ describe(MediaService.name, () => { path: expect.any(String), isEdited: false, isProgressive: false, + isTransparent: false, }, ]); }); @@ -548,6 +553,7 @@ describe(MediaService.name, () => { path: expect.any(String), isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: asset.id, @@ -555,6 +561,7 @@ describe(MediaService.name, () => { path: expect.any(String), isEdited: false, isProgressive: false, + isTransparent: false, }, ]); }); @@ -770,10 +777,12 @@ describe(MediaService.name, () => { expect.objectContaining({ type: AssetFileType.Preview, isProgressive: true, + isTransparent: false, }), expect.objectContaining({ type: AssetFileType.Thumbnail, isProgressive: false, + isTransparent: false, }), ]); }); @@ -807,10 +816,12 @@ describe(MediaService.name, () => { expect.objectContaining({ type: AssetFileType.Preview, isProgressive: false, + isTransparent: false, }), expect.objectContaining({ type: AssetFileType.Thumbnail, isProgressive: true, + isTransparent: false, }), ]); }); @@ -829,10 +840,12 @@ describe(MediaService.name, () => { expect.objectContaining({ type: AssetFileType.Preview, isProgressive: false, + isTransparent: false, }), expect.objectContaining({ type: AssetFileType.Thumbnail, isProgressive: false, + isTransparent: false, }), ]); }); @@ -857,7 +870,7 @@ describe(MediaService.name, () => { .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) .build(); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); @@ -871,12 +884,39 @@ describe(MediaService.name, () => { }); }); + it('should not check transparency metadata for raw files without extracted images', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); + mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: false } }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + + await sut.handleGenerateThumbnails({ id: asset.id }); + + expect(mocks.media.getImageMetadata).not.toHaveBeenCalled(); + }); + + it('should not check transparency metadata for raw files with extracted images', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); + mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); + mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + + await sut.handleGenerateThumbnails({ id: asset.id }); + + expect(mocks.media.getImageMetadata).toHaveBeenCalledOnce(); + expect(mocks.media.getImageMetadata).toHaveBeenCalledWith(extractedBuffer); + }); + it('should resize original image if embedded image is too small', async () => { const asset = AssetFactory.from({ originalFileName: 'file.dng' }) .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) .build(); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 1000, height: 1000, isTransparent: false }); mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); @@ -970,7 +1010,7 @@ describe(MediaService.name, () => { image: { fullsize: { enabled: true, format: ImageFormat.Webp }, extractEmbedded: true }, }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); await sut.handleGenerateThumbnails({ id: asset.id }); @@ -1008,7 +1048,7 @@ describe(MediaService.name, () => { image: { fullsize: { enabled: true, format: ImageFormat.Webp }, extractEmbedded: true }, }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jxl }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); await sut.handleGenerateThumbnails({ id: asset.id }); @@ -1056,7 +1096,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: false } }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); await sut.handleGenerateThumbnails({ id: asset.id }); @@ -1100,7 +1140,7 @@ describe(MediaService.name, () => { it('should generate full-size preview from non-web-friendly images', async () => { mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); // HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari. const asset = AssetFactory.from({ originalFileName: 'image.hif' }) .exif({ @@ -1139,7 +1179,7 @@ describe(MediaService.name, () => { const asset = AssetFactory.from().exif().build(); mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); await sut.handleGenerateThumbnails({ id: asset.id }); @@ -1162,7 +1202,7 @@ describe(MediaService.name, () => { it('should always generate full-size preview from non-web-friendly panoramas', async () => { mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: false } } }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); mocks.media.copyTagGroup.mockResolvedValue(true); const asset = AssetFactory.from({ originalFileName: 'panorama.tif' }) @@ -1208,7 +1248,7 @@ describe(MediaService.name, () => { image: { fullsize: { enabled: true, format: ImageFormat.Webp, quality: 90 } }, }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); // HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari. const asset = AssetFactory.from({ originalFileName: 'image.hif' }) .exif({ @@ -1248,7 +1288,7 @@ describe(MediaService.name, () => { image: { fullsize: { enabled: true, format: ImageFormat.Jpeg, progressive: true } }, }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); const asset = AssetFactory.from({ originalFileName: 'image.hif' }) .exif({ fileSizeInByte: 5000, @@ -1286,6 +1326,7 @@ describe(MediaService.name, () => { : { data: fullsizeBuffer, info: rawInfo as OutputInfo }, // buffer implies embedded image extracted ), ); + mocks.media.getImageMetadata.mockResolvedValue({ width: 100, height: 100, isTransparent: false }); }); it('should skip videos', async () => { @@ -1719,7 +1760,7 @@ describe(MediaService.name, () => { const info = { width: 2160, height: 3840 } as OutputInfo; mocks.media.extract.mockResolvedValue({ buffer: extracted, format: RawExtractedFormat.Jpeg }); mocks.media.decodeImage.mockResolvedValue({ data, info }); - mocks.media.getImageDimensions.mockResolvedValue(info); + mocks.media.getImageMetadata.mockResolvedValue({ width: 2160, height: 3840, isTransparent: false }); await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( JobStatus.Success, @@ -1802,7 +1843,7 @@ describe(MediaService.name, () => { const info = { width: 1000, height: 1000 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); mocks.media.extract.mockResolvedValue({ buffer: extracted, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue(info); + mocks.media.getImageMetadata.mockResolvedValue({ width: 1000, height: 1000, isTransparent: false }); await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( JobStatus.Success, @@ -3554,6 +3595,7 @@ describe(MediaService.name, () => { path: '/new/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: asset.id, @@ -3561,6 +3603,7 @@ describe(MediaService.name, () => { path: '/new/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ]); @@ -3571,6 +3614,7 @@ describe(MediaService.name, () => { type: AssetFileType.Preview, isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: 'asset-id', @@ -3578,6 +3622,7 @@ describe(MediaService.name, () => { type: AssetFileType.Thumbnail, isEdited: false, isProgressive: false, + isTransparent: false, }, ]); expect(mocks.asset.deleteFiles).not.toHaveBeenCalled(); @@ -3595,6 +3640,7 @@ describe(MediaService.name, () => { path: '/old/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { id: 'file-2', @@ -3603,6 +3649,7 @@ describe(MediaService.name, () => { path: '/old/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ], }; @@ -3614,6 +3661,7 @@ describe(MediaService.name, () => { path: '/new/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: asset.id, @@ -3621,6 +3669,7 @@ describe(MediaService.name, () => { path: '/new/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ]); @@ -3631,6 +3680,7 @@ describe(MediaService.name, () => { type: AssetFileType.Preview, isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: 'asset-id', @@ -3638,6 +3688,7 @@ describe(MediaService.name, () => { type: AssetFileType.Thumbnail, isEdited: false, isProgressive: false, + isTransparent: false, }, ]); expect(mocks.asset.deleteFiles).not.toHaveBeenCalled(); @@ -3658,6 +3709,7 @@ describe(MediaService.name, () => { path: '/old/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { id: 'file-2', @@ -3666,6 +3718,7 @@ describe(MediaService.name, () => { path: '/old/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ], }; @@ -3681,6 +3734,7 @@ describe(MediaService.name, () => { path: '/old/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { id: 'file-2', @@ -3689,6 +3743,7 @@ describe(MediaService.name, () => { path: '/old/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ]); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -3708,6 +3763,7 @@ describe(MediaService.name, () => { path: '/same/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { id: 'file-2', @@ -3716,6 +3772,7 @@ describe(MediaService.name, () => { path: '/same/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ], }; @@ -3727,6 +3784,7 @@ describe(MediaService.name, () => { path: '/same/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: asset.id, @@ -3734,6 +3792,7 @@ describe(MediaService.name, () => { path: '/same/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ]); @@ -3753,6 +3812,7 @@ describe(MediaService.name, () => { path: '/old/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { id: 'file-2', @@ -3761,6 +3821,7 @@ describe(MediaService.name, () => { path: '/old/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ], }; @@ -3772,6 +3833,7 @@ describe(MediaService.name, () => { path: '/new/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, // replace { assetId: asset.id, @@ -3779,6 +3841,7 @@ describe(MediaService.name, () => { path: '/new/fullsize.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, // new ]); @@ -3789,6 +3852,7 @@ describe(MediaService.name, () => { type: AssetFileType.Preview, isEdited: false, isProgressive: false, + isTransparent: false, }, { assetId: 'asset-id', @@ -3796,6 +3860,7 @@ describe(MediaService.name, () => { type: AssetFileType.FullSize, isEdited: false, isProgressive: false, + isTransparent: false, }, ]); expect(mocks.asset.deleteFiles).toHaveBeenCalledWith([ @@ -3806,6 +3871,7 @@ describe(MediaService.name, () => { path: '/old/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ]); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -3838,6 +3904,7 @@ describe(MediaService.name, () => { path: '/old/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ], }; @@ -3853,6 +3920,7 @@ describe(MediaService.name, () => { path: '/old/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ]); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -3872,6 +3940,7 @@ describe(MediaService.name, () => { path: '/old/preview.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, { id: 'file-2', @@ -3880,6 +3949,7 @@ describe(MediaService.name, () => { path: '/old/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ], }; @@ -3891,6 +3961,7 @@ describe(MediaService.name, () => { path: '/old/preview.jpg', isEdited: false, isProgressive: true, + isTransparent: false, }, { assetId: asset.id, @@ -3898,6 +3969,7 @@ describe(MediaService.name, () => { path: '/old/thumbnail.jpg', isEdited: false, isProgressive: false, + isTransparent: false, }, ]); @@ -3908,6 +3980,7 @@ describe(MediaService.name, () => { type: AssetFileType.Preview, isEdited: false, isProgressive: true, + isTransparent: false, }, ]); expect(mocks.asset.deleteFiles).not.toHaveBeenCalled(); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 5fa72cf117..3555d7d108 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -52,6 +52,7 @@ interface UpsertFileOptions { path: string; isEdited: boolean; isProgressive: boolean; + isTransparent: boolean; } type ThumbnailAsset = NonNullable>>; @@ -280,14 +281,20 @@ export class MediaService extends BaseService { useEdits; const convertFullsize = generateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`)); + const thumbSource = extracted ? extracted.buffer : asset.originalPath; const { data, info, colorspace } = await this.decodeImage( - extracted ? extracted.buffer : asset.originalPath, + thumbSource, // only specify orientation to extracted images which don't have EXIF orientation data // or it can double rotate the image extracted ? asset.exifInfo : { ...asset.exifInfo, orientation: null }, convertFullsize ? undefined : image.preview.size, ); + let isTransparent = false; + if (!extracted && mimeTypes.canBeTransparent(asset.originalPath)) { + ({ isTransparent } = await this.mediaRepository.getImageMetadata(asset.originalPath)); + } + return { extracted, data, @@ -295,50 +302,64 @@ export class MediaService extends BaseService { colorspace, convertFullsize, generateFullsize, + isTransparent, }; } private async generateImageThumbnails(asset: ThumbnailAsset, { image }: SystemConfig, useEdits: boolean = false) { + // Handle embedded preview extraction for RAW files + const extractedImage = await this.extractOriginalImage(asset, image, useEdits); + const { info, data, colorspace, generateFullsize, convertFullsize, extracted, isTransparent } = extractedImage; + + const previewFormat = image.preview.format; + this.warnOnTransparencyLoss(isTransparent, previewFormat, asset.id); + + const thumbnailFormat = image.thumbnail.format; + this.warnOnTransparencyLoss(isTransparent, thumbnailFormat, asset.id); + const previewFile = this.getImageFile(asset, { fileType: AssetFileType.Preview, - format: image.preview.format, + format: previewFormat, isEdited: useEdits, - isProgressive: !!image.preview.progressive && image.preview.format !== ImageFormat.Webp, + isProgressive: !!image.preview.progressive && previewFormat !== ImageFormat.Webp, + isTransparent, }); const thumbnailFile = this.getImageFile(asset, { fileType: AssetFileType.Thumbnail, - format: image.thumbnail.format, + format: thumbnailFormat, isEdited: useEdits, - isProgressive: !!image.thumbnail.progressive && image.thumbnail.format !== ImageFormat.Webp, + isProgressive: !!image.thumbnail.progressive && thumbnailFormat !== ImageFormat.Webp, + isTransparent, }); this.storageCore.ensureFolders(previewFile.path); - // Handle embedded preview extraction for RAW files - const extractedImage = await this.extractOriginalImage(asset, image, useEdits); - const { info, data, colorspace, generateFullsize, convertFullsize, extracted } = extractedImage; - // generate final images - const thumbnailOptions = { colorspace, processInvalidImages: false, raw: info, edits: useEdits ? asset.edits : [] }; + const baseOptions = { colorspace, processInvalidImages: false, raw: info, edits: useEdits ? asset.edits : [] }; + const thumbnailOptions = { ...image.thumbnail, ...baseOptions, format: thumbnailFormat }; + const previewOptions = { ...image.preview, ...baseOptions, format: previewFormat }; const promises = [ - this.mediaRepository.generateThumbhash(data, thumbnailOptions), - this.mediaRepository.generateThumbnail(data, { ...image.thumbnail, ...thumbnailOptions }, thumbnailFile.path), - this.mediaRepository.generateThumbnail(data, { ...image.preview, ...thumbnailOptions }, previewFile.path), + this.mediaRepository.generateThumbhash(data, baseOptions), + this.mediaRepository.generateThumbnail(data, thumbnailOptions, thumbnailFile.path), + this.mediaRepository.generateThumbnail(data, previewOptions, previewFile.path), ]; let fullsizeFile: UpsertFileOptions | undefined; if (convertFullsize) { + const fullsizeFormat = image.fullsize.format; + this.warnOnTransparencyLoss(isTransparent, fullsizeFormat, asset.id); // convert a new fullsize image from the same source as the thumbnail fullsizeFile = this.getImageFile(asset, { fileType: AssetFileType.FullSize, - format: image.fullsize.format, + format: fullsizeFormat, isEdited: useEdits, - isProgressive: !!image.fullsize.progressive && image.fullsize.format !== ImageFormat.Webp, + isProgressive: !!image.fullsize.progressive && fullsizeFormat !== ImageFormat.Webp, + isTransparent, }); const fullsizeOptions = { - format: image.fullsize.format, + ...baseOptions, + format: fullsizeFormat, quality: image.fullsize.quality, progressive: image.fullsize.progressive, - ...thumbnailOptions, }; promises.push(this.mediaRepository.generateThumbnail(data, fullsizeOptions, fullsizeFile.path)); } else if (generateFullsize && extracted && extracted.format === RawExtractedFormat.Jpeg) { @@ -347,6 +368,7 @@ export class MediaService extends BaseService { format: extracted.format, isEdited: false, isProgressive: !!image.fullsize.progressive && image.fullsize.format !== ImageFormat.Webp, + isTransparent, }); this.storageCore.ensureFolders(fullsizeFile.path); @@ -493,12 +515,14 @@ export class MediaService extends BaseService { format: image.preview.format, isEdited: false, isProgressive: false, + isTransparent: false, }); const thumbnailFile = this.getImageFile(asset, { fileType: AssetFileType.Thumbnail, format: image.thumbnail.format, isEdited: false, isProgressive: false, + isTransparent: false, }); this.storageCore.ensureFolders(previewFile.path); @@ -758,7 +782,7 @@ export class MediaService extends BaseService { } private async shouldUseExtractedImage(extractedPathOrBuffer: string | Buffer, targetSize: number) { - const { width, height } = await this.mediaRepository.getImageDimensions(extractedPathOrBuffer); + const { width, height } = await this.mediaRepository.getImageMetadata(extractedPathOrBuffer); const extractedSize = Math.min(width, height); return extractedSize >= targetSize; } @@ -785,7 +809,10 @@ export class MediaService extends BaseService { } } - private async syncFiles(oldFiles: (AssetFile & { isProgressive: boolean })[], newFiles: UpsertFileOptions[]) { + private async syncFiles( + oldFiles: (AssetFile & { isProgressive: boolean; isTransparent: boolean })[], + newFiles: UpsertFileOptions[], + ) { const toUpsert: UpsertFileOptions[] = []; const pathsToDelete: string[] = []; const toDelete = new Set(oldFiles); @@ -797,7 +824,11 @@ export class MediaService extends BaseService { } // upsert new file path - if (existingFile?.path !== newFile.path || existingFile.isProgressive !== newFile.isProgressive) { + if ( + existingFile?.path !== newFile.path || + existingFile.isProgressive !== newFile.isProgressive || + existingFile.isTransparent !== newFile.isTransparent + ) { toUpsert.push(newFile); // delete old file from disk @@ -857,7 +888,18 @@ export class MediaService extends BaseService { return generated; } - private getImageFile(asset: ThumbnailPathEntity, options: ImagePathOptions & { isProgressive: boolean }) { + private warnOnTransparencyLoss(isTransparent: boolean, format: ImageFormat, assetId: string) { + if (isTransparent && format === ImageFormat.Jpeg) { + this.logger.warn( + `Asset ${assetId} has transparency but the configured format is ${format} which does not support it, consider using a format that does, such as ${ImageFormat.Webp}`, + ); + } + } + + private getImageFile( + asset: ThumbnailPathEntity, + options: ImagePathOptions & { isProgressive: boolean; isTransparent: boolean }, + ) { const path = StorageCore.getImagePath(asset, options); return { assetId: asset.id, @@ -865,6 +907,7 @@ export class MediaService extends BaseService { path, isEdited: options.isEdited, isProgressive: options.isProgressive, + isTransparent: options.isTransparent, }; } } diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index db682b6393..2378d594e1 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -100,6 +100,8 @@ export class MemoryService extends BaseService { data: dto.data, isSaved: dto.isSaved, memoryAt: dto.memoryAt, + showAt: dto.showAt, + hideAt: dto.hideAt, seenAt: dto.seenAt, }, allowedAssetIds, diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 983d905aad..c5d7278d56 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -169,7 +169,7 @@ export class MetadataService extends BaseService { this.logger.log(`Initialized local reverse geocoder`); } catch (error: Error | any) { this.logger.error(`Unable to initialize reverse geocoding: ${error}`, error?.stack); - throw new Error(`Metadata service init failed`); + throw new Error('Metadata service init failed', { cause: error }); } } @@ -286,7 +286,7 @@ export class MetadataService extends BaseService { orientation: validate(exifTags.Orientation)?.toString() ?? null, projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null, bitsPerSample: this.getBitsPerSample(exifTags), - colorspace: exifTags.ColorSpace ?? null, + colorspace: exifTags.ColorSpace === undefined ? null : String(exifTags.ColorSpace), // camera make: exifTags.Make ?? exifTags.Device?.Manufacturer ?? exifTags.AndroidMake ?? null, diff --git a/server/src/services/notification-admin.service.ts b/server/src/services/notification-admin.service.ts index bf0d2bba41..2fc4584dca 100644 --- a/server/src/services/notification-admin.service.ts +++ b/server/src/services/notification-admin.service.ts @@ -59,7 +59,7 @@ export class NotificationAdminService extends BaseService { async getTemplate(name: EmailTemplate, customTemplate: string) { const { server, templates } = await this.getConfig({ withCache: false }); - let templateResponse = ''; + let templateResponse: string; switch (name) { case EmailTemplate.WELCOME: { diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index ee87fcf775..9f11d19af7 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -134,7 +134,7 @@ export class NotificationService extends BaseService { } } catch (error: Error | any) { this.logger.error(`Failed to validate SMTP configuration: ${error}`, error?.stack); - throw new Error(`Invalid SMTP configuration: ${error}`); + throw new Error('Invalid SMTP configuration', { cause: error }); } } diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts index 2f477c0d6a..8b5bd13928 100644 --- a/server/src/services/session.service.ts +++ b/server/src/services/session.service.ts @@ -33,14 +33,14 @@ export class SessionService extends BaseService { } const token = this.cryptoRepository.randomBytesAsText(32); - const tokenHashed = this.cryptoRepository.hashSha256(token); + const hashed = this.cryptoRepository.hashSha256(token); const session = await this.sessionRepository.create({ parentId: auth.session.id, userId: auth.user.id, expiresAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null, deviceType: dto.deviceType, deviceOS: dto.deviceOS, - token: tokenHashed, + token: hashed, }); return { ...mapSession(session), token }; diff --git a/server/src/services/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts index 5ad145af2b..07f31db4da 100644 --- a/server/src/services/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -69,8 +69,9 @@ describe(SharedLinkService.name, () => { it('should accept a valid shared link auth token', async () => { mocks.sharedLink.get.mockResolvedValue({ ...sharedLinkStub.individual, password: '123' }); - mocks.crypto.hashSha256.mockReturnValue('hashed-auth-token'); - await expect(sut.getMine(authStub.adminSharedLink, ['hashed-auth-token'])).resolves.toBeDefined(); + const secret = Buffer.from('auth-token-123'); + mocks.crypto.hashSha256.mockReturnValue(secret); + await expect(sut.getMine(authStub.adminSharedLink, [secret.toString('base64')])).resolves.toBeDefined(); expect(mocks.sharedLink.get).toHaveBeenCalledWith( authStub.adminSharedLink.user.id, authStub.adminSharedLink.sharedLink?.id, diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index e321e4990d..b942c32326 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -236,6 +236,6 @@ export class SharedLinkService extends BaseService { } private asToken(sharedLink: { id: string; password: string }) { - return this.cryptoRepository.hashSha256(`${sharedLink.id}-${sharedLink.password}`); + return this.cryptoRepository.hashSha256(`${sharedLink.id}-${sharedLink.password}`).toString('base64'); } } diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index d5020a9c5e..a8f4e6a185 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -125,7 +125,7 @@ export class StorageTemplateService extends BaseService { }); } catch (error) { this.logger.warn(`Storage template validation failed: ${JSON.stringify(error)}`); - throw new Error(`Invalid storage template: ${error}`); + throw new Error('Invalid storage template', { cause: error }); } } diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index f354a71791..76fd129f50 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -12,6 +12,7 @@ import { AssetFullSyncDto, SyncAckDeleteDto, SyncAckSetDto, + syncAssetFaceV2ToV1, SyncAssetV1, SyncItem, SyncStreamDto, @@ -85,6 +86,7 @@ export const SYNC_TYPES_ORDER = [ SyncRequestType.MemoryToAssetsV1, SyncRequestType.PeopleV1, SyncRequestType.AssetFacesV1, + SyncRequestType.AssetFacesV2, SyncRequestType.UserMetadataV1, SyncRequestType.AssetMetadataV1, ]; @@ -189,6 +191,7 @@ export class SyncService extends BaseService { [SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(options, response, checkpointMap, session.id), [SyncRequestType.PeopleV1]: () => this.syncPeopleV1(options, response, checkpointMap), [SyncRequestType.AssetFacesV1]: async () => this.syncAssetFacesV1(options, response, checkpointMap), + [SyncRequestType.AssetFacesV2]: async () => this.syncAssetFacesV2(options, response, checkpointMap), [SyncRequestType.UserMetadataV1]: () => this.syncUserMetadataV1(options, response, checkpointMap), }; @@ -789,6 +792,21 @@ export class SyncService extends BaseService { const upsertType = SyncEntityType.AssetFaceV1; const upserts = this.syncRepository.assetFace.getUpserts({ ...options, ack: checkpointMap[upsertType] }); + for await (const { updateId, ...data } of upserts) { + const v1 = syncAssetFaceV2ToV1(data); + send(response, { type: upsertType, ids: [updateId], data: v1 }); + } + } + + private async syncAssetFacesV2(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) { + const deleteType = SyncEntityType.AssetFaceDeleteV1; + const deletes = this.syncRepository.assetFace.getDeletes({ ...options, ack: checkpointMap[deleteType] }); + for await (const { id, ...data } of deletes) { + send(response, { type: deleteType, ids: [id], data }); + } + + const upsertType = SyncEntityType.AssetFaceV2; + const upserts = this.syncRepository.assetFace.getUpserts({ ...options, ack: checkpointMap[upsertType] }); for await (const { updateId, ...data } of upserts) { send(response, { type: upsertType, ids: [updateId], data }); } diff --git a/server/src/sql-tools/comparers/column.comparer.spec.ts b/server/src/sql-tools/comparers/column.comparer.spec.ts deleted file mode 100644 index ef2afb348a..0000000000 --- a/server/src/sql-tools/comparers/column.comparer.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { compareColumns } from 'src/sql-tools/comparers/column.comparer'; -import { DatabaseColumn, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testColumn: DatabaseColumn = { - name: 'test', - tableName: 'table1', - primary: false, - nullable: false, - isArray: false, - type: 'character varying', - synchronize: true, -}; - -describe('compareColumns', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareColumns().onExtra(testColumn)).toEqual([ - { - tableName: 'table1', - columnName: 'test', - type: 'ColumnDrop', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareColumns().onMissing(testColumn)).toEqual([ - { - type: 'ColumnAdd', - column: testColumn, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareColumns().onCompare(testColumn, testColumn)).toEqual([]); - }); - - it('should detect a change in type', () => { - const source: DatabaseColumn = { ...testColumn }; - const target: DatabaseColumn = { ...testColumn, type: 'text' }; - const reason = 'column type is different (character varying vs text)'; - expect(compareColumns().onCompare(source, target)).toEqual([ - { - columnName: 'test', - tableName: 'table1', - type: 'ColumnDrop', - reason, - }, - { - type: 'ColumnAdd', - column: source, - reason, - }, - ]); - }); - - it('should detect a change in default', () => { - const source: DatabaseColumn = { ...testColumn, nullable: true }; - const target: DatabaseColumn = { ...testColumn, nullable: true, default: "''" }; - const reason = `default is different (null vs '')`; - expect(compareColumns().onCompare(source, target)).toEqual([ - { - columnName: 'test', - tableName: 'table1', - type: 'ColumnAlter', - changes: { - default: 'NULL', - }, - reason, - }, - ]); - }); - - it('should detect a comment change', () => { - const source: DatabaseColumn = { ...testColumn, comment: 'new comment' }; - const target: DatabaseColumn = { ...testColumn, comment: 'old comment' }; - const reason = 'comment is different (new comment vs old comment)'; - expect(compareColumns().onCompare(source, target)).toEqual([ - { - columnName: 'test', - tableName: 'table1', - type: 'ColumnAlter', - changes: { - comment: 'new comment', - }, - reason, - }, - ]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/column.comparer.ts b/server/src/sql-tools/comparers/column.comparer.ts deleted file mode 100644 index 54ffb34ffa..0000000000 --- a/server/src/sql-tools/comparers/column.comparer.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { asRenameKey, getColumnType, isDefaultEqual } from 'src/sql-tools/helpers'; -import { Comparer, DatabaseColumn, Reason, SchemaDiff } from 'src/sql-tools/types'; - -export const compareColumns = () => - ({ - getRenameKey: (column) => { - return asRenameKey([ - column.tableName, - column.type, - column.nullable, - column.default, - column.storage, - column.primary, - column.isArray, - column.length, - column.identity, - column.enumName, - column.numericPrecision, - column.numericScale, - ]); - }, - onRename: (source, target) => [ - { - type: 'ColumnRename', - tableName: source.tableName, - oldName: target.name, - newName: source.name, - reason: Reason.Rename, - }, - ], - onMissing: (source) => [ - { - type: 'ColumnAdd', - column: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'ColumnDrop', - tableName: target.tableName, - columnName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => { - const sourceType = getColumnType(source); - const targetType = getColumnType(target); - - const isTypeChanged = sourceType !== targetType; - - if (isTypeChanged) { - // TODO: convert between types via UPDATE when possible - return dropAndRecreateColumn(source, target, `column type is different (${sourceType} vs ${targetType})`); - } - - const items: SchemaDiff[] = []; - if (source.nullable !== target.nullable) { - items.push({ - type: 'ColumnAlter', - tableName: source.tableName, - columnName: source.name, - changes: { - nullable: source.nullable, - }, - reason: `nullable is different (${source.nullable} vs ${target.nullable})`, - }); - } - - if (!isDefaultEqual(source, target)) { - items.push({ - type: 'ColumnAlter', - tableName: source.tableName, - columnName: source.name, - changes: { - default: String(source.default ?? 'NULL'), - }, - reason: `default is different (${source.default ?? 'null'} vs ${target.default})`, - }); - } - - if (source.comment !== target.comment) { - items.push({ - type: 'ColumnAlter', - tableName: source.tableName, - columnName: source.name, - changes: { - comment: String(source.comment), - }, - reason: `comment is different (${source.comment} vs ${target.comment})`, - }); - } - - return items; - }, - }) satisfies Comparer; - -const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => { - return [ - { - type: 'ColumnDrop', - tableName: target.tableName, - columnName: target.name, - reason, - }, - { type: 'ColumnAdd', column: source, reason }, - ]; -}; diff --git a/server/src/sql-tools/comparers/constraint.comparer.spec.ts b/server/src/sql-tools/comparers/constraint.comparer.spec.ts deleted file mode 100644 index 216728f8c4..0000000000 --- a/server/src/sql-tools/comparers/constraint.comparer.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { compareConstraints } from 'src/sql-tools/comparers/constraint.comparer'; -import { ConstraintType, DatabaseConstraint, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testConstraint: DatabaseConstraint = { - type: ConstraintType.PRIMARY_KEY, - name: 'test', - tableName: 'table1', - columnNames: ['column1'], - synchronize: true, -}; - -describe('compareConstraints', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareConstraints().onExtra(testConstraint)).toEqual([ - { - type: 'ConstraintDrop', - constraintName: 'test', - tableName: 'table1', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareConstraints().onMissing(testConstraint)).toEqual([ - { - type: 'ConstraintAdd', - constraint: testConstraint, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareConstraints().onCompare(testConstraint, testConstraint)).toEqual([]); - }); - - it('should detect a change in type', () => { - const source: DatabaseConstraint = { ...testConstraint }; - const target: DatabaseConstraint = { ...testConstraint, columnNames: ['column1', 'column2'] }; - const reason = 'Primary key columns are different: (column1 vs column1,column2)'; - expect(compareConstraints().onCompare(source, target)).toEqual([ - { - constraintName: 'test', - tableName: 'table1', - type: 'ConstraintDrop', - reason, - }, - { - type: 'ConstraintAdd', - constraint: source, - reason, - }, - ]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/constraint.comparer.ts b/server/src/sql-tools/comparers/constraint.comparer.ts deleted file mode 100644 index 03128878d5..0000000000 --- a/server/src/sql-tools/comparers/constraint.comparer.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { asRenameKey, haveEqualColumns } from 'src/sql-tools/helpers'; -import { - CompareFunction, - Comparer, - ConstraintType, - DatabaseCheckConstraint, - DatabaseConstraint, - DatabaseForeignKeyConstraint, - DatabasePrimaryKeyConstraint, - DatabaseUniqueConstraint, - Reason, - SchemaDiff, -} from 'src/sql-tools/types'; - -export const compareConstraints = (): Comparer => ({ - getRenameKey: (constraint) => { - switch (constraint.type) { - case ConstraintType.PRIMARY_KEY: - case ConstraintType.UNIQUE: { - return asRenameKey([constraint.type, constraint.tableName, ...constraint.columnNames.toSorted()]); - } - - case ConstraintType.FOREIGN_KEY: { - return asRenameKey([ - constraint.type, - constraint.tableName, - ...constraint.columnNames.toSorted(), - constraint.referenceTableName, - ...constraint.referenceColumnNames.toSorted(), - ]); - } - - case ConstraintType.CHECK: { - const expression = constraint.expression.replaceAll('(', '').replaceAll(')', ''); - return asRenameKey([constraint.type, constraint.tableName, expression]); - } - } - }, - onRename: (source, target) => [ - { - type: 'ConstraintRename', - tableName: target.tableName, - oldName: target.name, - newName: source.name, - reason: Reason.Rename, - }, - ], - onMissing: (source) => [ - { - type: 'ConstraintAdd', - constraint: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'ConstraintDrop', - tableName: target.tableName, - constraintName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => { - switch (source.type) { - case ConstraintType.PRIMARY_KEY: { - return comparePrimaryKeyConstraint(source, target as DatabasePrimaryKeyConstraint); - } - - case ConstraintType.FOREIGN_KEY: { - return compareForeignKeyConstraint(source, target as DatabaseForeignKeyConstraint); - } - - case ConstraintType.UNIQUE: { - return compareUniqueConstraint(source, target as DatabaseUniqueConstraint); - } - - case ConstraintType.CHECK: { - return compareCheckConstraint(source, target as DatabaseCheckConstraint); - } - - default: { - return []; - } - } - }, -}); - -const comparePrimaryKeyConstraint: CompareFunction = (source, target) => { - if (!haveEqualColumns(source.columnNames, target.columnNames)) { - return dropAndRecreateConstraint( - source, - target, - `Primary key columns are different: (${source.columnNames} vs ${target.columnNames})`, - ); - } - - return []; -}; - -const compareForeignKeyConstraint: CompareFunction = (source, target) => { - let reason = ''; - - const sourceDeleteAction = source.onDelete ?? 'NO ACTION'; - const targetDeleteAction = target.onDelete ?? 'NO ACTION'; - - const sourceUpdateAction = source.onUpdate ?? 'NO ACTION'; - const targetUpdateAction = target.onUpdate ?? 'NO ACTION'; - - if (!haveEqualColumns(source.columnNames, target.columnNames)) { - reason = `columns are different (${source.columnNames} vs ${target.columnNames})`; - } else if (!haveEqualColumns(source.referenceColumnNames, target.referenceColumnNames)) { - reason = `reference columns are different (${source.referenceColumnNames} vs ${target.referenceColumnNames})`; - } else if (source.referenceTableName !== target.referenceTableName) { - reason = `reference table is different (${source.referenceTableName} vs ${target.referenceTableName})`; - } else if (sourceDeleteAction !== targetDeleteAction) { - reason = `ON DELETE action is different (${sourceDeleteAction} vs ${targetDeleteAction})`; - } else if (sourceUpdateAction !== targetUpdateAction) { - reason = `ON UPDATE action is different (${sourceUpdateAction} vs ${targetUpdateAction})`; - } - - if (reason) { - return dropAndRecreateConstraint(source, target, reason); - } - - return []; -}; - -const compareUniqueConstraint: CompareFunction = (source, target) => { - let reason = ''; - - if (!haveEqualColumns(source.columnNames, target.columnNames)) { - reason = `columns are different (${source.columnNames} vs ${target.columnNames})`; - } - - if (reason) { - return dropAndRecreateConstraint(source, target, reason); - } - - return []; -}; - -const compareCheckConstraint: CompareFunction = (source, target) => { - if (source.expression !== target.expression) { - // comparing expressions is hard because postgres reconstructs it with different formatting - // for now if the constraint exists with the same name, we will just skip it - } - - return []; -}; - -const dropAndRecreateConstraint = ( - source: DatabaseConstraint, - target: DatabaseConstraint, - reason: string, -): SchemaDiff[] => { - return [ - { - type: 'ConstraintDrop', - tableName: target.tableName, - constraintName: target.name, - reason, - }, - { type: 'ConstraintAdd', constraint: source, reason }, - ]; -}; diff --git a/server/src/sql-tools/comparers/enum.comparer.spec.ts b/server/src/sql-tools/comparers/enum.comparer.spec.ts deleted file mode 100644 index d788c7cd71..0000000000 --- a/server/src/sql-tools/comparers/enum.comparer.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { compareEnums } from 'src/sql-tools/comparers/enum.comparer'; -import { DatabaseEnum, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testEnum: DatabaseEnum = { name: 'test', values: ['foo', 'bar'], synchronize: true }; - -describe('compareEnums', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareEnums().onExtra(testEnum)).toEqual([ - { - enumName: 'test', - type: 'EnumDrop', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareEnums().onMissing(testEnum)).toEqual([ - { - type: 'EnumCreate', - enum: testEnum, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareEnums().onCompare(testEnum, testEnum)).toEqual([]); - }); - - it('should drop and recreate when values list is different', () => { - const source = { name: 'test', values: ['foo', 'bar'], synchronize: true }; - const target = { name: 'test', values: ['foo', 'bar', 'world'], synchronize: true }; - expect(compareEnums().onCompare(source, target)).toEqual([ - { - enumName: 'test', - type: 'EnumDrop', - reason: 'enum values has changed (foo,bar vs foo,bar,world)', - }, - { - type: 'EnumCreate', - enum: source, - reason: 'enum values has changed (foo,bar vs foo,bar,world)', - }, - ]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/enum.comparer.ts b/server/src/sql-tools/comparers/enum.comparer.ts deleted file mode 100644 index efc08ae727..0000000000 --- a/server/src/sql-tools/comparers/enum.comparer.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Comparer, DatabaseEnum, Reason } from 'src/sql-tools/types'; - -export const compareEnums = (): Comparer => ({ - onMissing: (source) => [ - { - type: 'EnumCreate', - enum: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'EnumDrop', - enumName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => { - if (source.values.toString() !== target.values.toString()) { - // TODO add or remove values if the lists are different or the order has changed - const reason = `enum values has changed (${source.values} vs ${target.values})`; - return [ - { - type: 'EnumDrop', - enumName: source.name, - reason, - }, - { - type: 'EnumCreate', - enum: source, - reason, - }, - ]; - } - - return []; - }, -}); diff --git a/server/src/sql-tools/comparers/extension.comparer.spec.ts b/server/src/sql-tools/comparers/extension.comparer.spec.ts deleted file mode 100644 index df70ccc761..0000000000 --- a/server/src/sql-tools/comparers/extension.comparer.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { compareExtensions } from 'src/sql-tools/comparers/extension.comparer'; -import { Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testExtension = { name: 'test', synchronize: true }; - -describe('compareExtensions', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareExtensions().onExtra(testExtension)).toEqual([ - { - extensionName: 'test', - type: 'ExtensionDrop', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareExtensions().onMissing(testExtension)).toEqual([ - { - type: 'ExtensionCreate', - extension: testExtension, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareExtensions().onCompare(testExtension, testExtension)).toEqual([]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/extension.comparer.ts b/server/src/sql-tools/comparers/extension.comparer.ts deleted file mode 100644 index 3cb70dadc4..0000000000 --- a/server/src/sql-tools/comparers/extension.comparer.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Comparer, DatabaseExtension, Reason } from 'src/sql-tools/types'; - -export const compareExtensions = (): Comparer => ({ - onMissing: (source) => [ - { - type: 'ExtensionCreate', - extension: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'ExtensionDrop', - extensionName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: () => { - // if the name matches they are the same - return []; - }, -}); diff --git a/server/src/sql-tools/comparers/function.comparer.spec.ts b/server/src/sql-tools/comparers/function.comparer.spec.ts deleted file mode 100644 index 3d18aaf50a..0000000000 --- a/server/src/sql-tools/comparers/function.comparer.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { compareFunctions } from 'src/sql-tools/comparers/function.comparer'; -import { DatabaseFunction, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testFunction: DatabaseFunction = { - name: 'test', - expression: 'CREATE FUNCTION something something something', - synchronize: true, -}; - -describe('compareFunctions', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareFunctions().onExtra(testFunction)).toEqual([ - { - functionName: 'test', - type: 'FunctionDrop', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareFunctions().onMissing(testFunction)).toEqual([ - { - type: 'FunctionCreate', - function: testFunction, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should ignore functions with the same hash', () => { - expect(compareFunctions().onCompare(testFunction, testFunction)).toEqual([]); - }); - - it('should report differences if functions have different hashes', () => { - const source: DatabaseFunction = { ...testFunction, expression: 'SELECT 1' }; - const target: DatabaseFunction = { ...testFunction, expression: 'SELECT 2' }; - expect(compareFunctions().onCompare(source, target)).toEqual([ - { - type: 'FunctionCreate', - reason: 'function expression has changed (SELECT 1 vs SELECT 2)', - function: source, - }, - ]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/function.comparer.ts b/server/src/sql-tools/comparers/function.comparer.ts deleted file mode 100644 index c6217ee708..0000000000 --- a/server/src/sql-tools/comparers/function.comparer.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Comparer, DatabaseFunction, Reason } from 'src/sql-tools/types'; - -export const compareFunctions = (): Comparer => ({ - onMissing: (source) => [ - { - type: 'FunctionCreate', - function: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'FunctionDrop', - functionName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => { - if (source.expression !== target.expression) { - const reason = `function expression has changed (${source.expression} vs ${target.expression})`; - return [ - { - type: 'FunctionCreate', - function: source, - reason, - }, - ]; - } - - return []; - }, -}); diff --git a/server/src/sql-tools/comparers/index.comparer.spec.ts b/server/src/sql-tools/comparers/index.comparer.spec.ts deleted file mode 100644 index 9ae7f34f04..0000000000 --- a/server/src/sql-tools/comparers/index.comparer.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { compareIndexes } from 'src/sql-tools/comparers/index.comparer'; -import { DatabaseIndex, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testIndex: DatabaseIndex = { - name: 'test', - tableName: 'table1', - columnNames: ['column1', 'column2'], - unique: false, - synchronize: true, -}; - -describe('compareIndexes', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareIndexes().onExtra(testIndex)).toEqual([ - { - type: 'IndexDrop', - indexName: 'test', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareIndexes().onMissing(testIndex)).toEqual([ - { - type: 'IndexCreate', - index: testIndex, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareIndexes().onCompare(testIndex, testIndex)).toEqual([]); - }); - - it('should drop and recreate when column list is different', () => { - const source = { - name: 'test', - tableName: 'table1', - columnNames: ['column1'], - unique: true, - synchronize: true, - }; - const target = { - name: 'test', - tableName: 'table1', - columnNames: ['column1', 'column2'], - unique: true, - synchronize: true, - }; - expect(compareIndexes().onCompare(source, target)).toEqual([ - { - indexName: 'test', - type: 'IndexDrop', - reason: 'columns are different (column1 vs column1,column2)', - }, - { - type: 'IndexCreate', - index: source, - reason: 'columns are different (column1 vs column1,column2)', - }, - ]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/index.comparer.ts b/server/src/sql-tools/comparers/index.comparer.ts deleted file mode 100644 index e474302c6e..0000000000 --- a/server/src/sql-tools/comparers/index.comparer.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { asRenameKey, haveEqualColumns } from 'src/sql-tools/helpers'; -import { Comparer, DatabaseIndex, Reason } from 'src/sql-tools/types'; - -export const compareIndexes = (): Comparer => ({ - getRenameKey: (index) => { - if (index.override) { - return index.override.value.sql.replace(index.name, 'INDEX_NAME'); - } - - return asRenameKey([index.tableName, ...(index.columnNames || []), index.unique]); - }, - onRename: (source, target) => [ - { - type: 'IndexRename', - tableName: source.tableName, - oldName: target.name, - newName: source.name, - reason: Reason.Rename, - }, - ], - onMissing: (source) => [ - { - type: 'IndexCreate', - index: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'IndexDrop', - indexName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => { - const sourceUsing = source.using ?? 'btree'; - const targetUsing = target.using ?? 'btree'; - - let reason = ''; - - if (!haveEqualColumns(source.columnNames, target.columnNames)) { - reason = `columns are different (${source.columnNames} vs ${target.columnNames})`; - } else if (source.unique !== target.unique) { - reason = `uniqueness is different (${source.unique} vs ${target.unique})`; - } else if (sourceUsing !== targetUsing) { - reason = `using method is different (${source.using} vs ${target.using})`; - } else if (source.where !== target.where) { - reason = `where clause is different (${source.where} vs ${target.where})`; - } else if (source.expression !== target.expression) { - reason = `expression is different (${source.expression} vs ${target.expression})`; - } - - if (reason) { - return [ - { type: 'IndexDrop', indexName: target.name, reason }, - { type: 'IndexCreate', index: source, reason }, - ]; - } - - return []; - }, -}); diff --git a/server/src/sql-tools/comparers/override.comparer.spec.ts b/server/src/sql-tools/comparers/override.comparer.spec.ts deleted file mode 100644 index dfa6fa4455..0000000000 --- a/server/src/sql-tools/comparers/override.comparer.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { compareOverrides } from 'src/sql-tools/comparers/override.comparer'; -import { DatabaseOverride, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testOverride: DatabaseOverride = { - name: 'test', - value: { type: 'function', name: 'test_func', sql: 'func implementation' }, - synchronize: true, -}; - -describe('compareOverrides', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareOverrides().onExtra(testOverride)).toEqual([ - { - type: 'OverrideDrop', - overrideName: 'test', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareOverrides().onMissing(testOverride)).toEqual([ - { - type: 'OverrideCreate', - override: testOverride, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareOverrides().onCompare(testOverride, testOverride)).toEqual([]); - }); - - it('should drop and recreate when the value changes', () => { - const source: DatabaseOverride = { - name: 'test', - value: { - type: 'function', - name: 'test_func', - sql: 'func implementation', - }, - synchronize: true, - }; - const target: DatabaseOverride = { - name: 'test', - value: { - type: 'function', - name: 'test_func', - sql: 'func implementation2', - }, - synchronize: true, - }; - expect(compareOverrides().onCompare(source, target)).toEqual([ - { - override: source, - type: 'OverrideUpdate', - reason: expect.stringContaining('value is different'), - }, - ]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/override.comparer.ts b/server/src/sql-tools/comparers/override.comparer.ts deleted file mode 100644 index 999770bf69..0000000000 --- a/server/src/sql-tools/comparers/override.comparer.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Comparer, DatabaseOverride, Reason } from 'src/sql-tools/types'; - -export const compareOverrides = (): Comparer => ({ - onMissing: (source) => [ - { - type: 'OverrideCreate', - override: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'OverrideDrop', - overrideName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => { - if (source.value.name !== target.value.name || source.value.sql !== target.value.sql) { - const sourceValue = JSON.stringify(source.value); - const targetValue = JSON.stringify(target.value); - return [ - { type: 'OverrideUpdate', override: source, reason: `value is different (${sourceValue} vs ${targetValue})` }, - ]; - } - - return []; - }, -}); diff --git a/server/src/sql-tools/comparers/parameter.comparer.spec.ts b/server/src/sql-tools/comparers/parameter.comparer.spec.ts deleted file mode 100644 index 23e6c78118..0000000000 --- a/server/src/sql-tools/comparers/parameter.comparer.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { compareParameters } from 'src/sql-tools/comparers/parameter.comparer'; -import { DatabaseParameter, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testParameter: DatabaseParameter = { - name: 'test', - databaseName: 'immich', - value: 'on', - scope: 'database', - synchronize: true, -}; - -describe('compareParameters', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareParameters().onExtra(testParameter)).toEqual([ - { - type: 'ParameterReset', - databaseName: 'immich', - parameterName: 'test', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareParameters().onMissing(testParameter)).toEqual([ - { - type: 'ParameterSet', - parameter: testParameter, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareParameters().onCompare(testParameter, testParameter)).toEqual([]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/parameter.comparer.ts b/server/src/sql-tools/comparers/parameter.comparer.ts deleted file mode 100644 index 41d0508d70..0000000000 --- a/server/src/sql-tools/comparers/parameter.comparer.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Comparer, DatabaseParameter, Reason } from 'src/sql-tools/types'; - -export const compareParameters = (): Comparer => ({ - onMissing: (source) => [ - { - type: 'ParameterSet', - parameter: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'ParameterReset', - databaseName: target.databaseName, - parameterName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: () => { - // TODO - return []; - }, -}); diff --git a/server/src/sql-tools/comparers/table.comparer.spec.ts b/server/src/sql-tools/comparers/table.comparer.spec.ts deleted file mode 100644 index 909db26ea9..0000000000 --- a/server/src/sql-tools/comparers/table.comparer.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { compareTables } from 'src/sql-tools/comparers/table.comparer'; -import { DatabaseTable, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testTable: DatabaseTable = { - name: 'test', - columns: [], - constraints: [], - indexes: [], - triggers: [], - synchronize: true, -}; - -describe('compareParameters', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareTables({}).onExtra(testTable)).toEqual([ - { - type: 'TableDrop', - tableName: 'test', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareTables({}).onMissing(testTable)).toEqual([ - { - type: 'TableCreate', - table: testTable, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareTables({}).onCompare(testTable, testTable)).toEqual([]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/table.comparer.ts b/server/src/sql-tools/comparers/table.comparer.ts deleted file mode 100644 index 6576dce1b1..0000000000 --- a/server/src/sql-tools/comparers/table.comparer.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { compareColumns } from 'src/sql-tools/comparers/column.comparer'; -import { compareConstraints } from 'src/sql-tools/comparers/constraint.comparer'; -import { compareIndexes } from 'src/sql-tools/comparers/index.comparer'; -import { compareTriggers } from 'src/sql-tools/comparers/trigger.comparer'; -import { compare } from 'src/sql-tools/helpers'; -import { Comparer, DatabaseTable, Reason, SchemaDiffOptions } from 'src/sql-tools/types'; - -export const compareTables = (options: SchemaDiffOptions): Comparer => ({ - onMissing: (source) => [ - { - type: 'TableCreate', - table: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'TableDrop', - tableName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => { - return [ - ...compare(source.columns, target.columns, options.columns, compareColumns()), - ...compare(source.indexes, target.indexes, options.indexes, compareIndexes()), - ...compare(source.constraints, target.constraints, options.constraints, compareConstraints()), - ...compare(source.triggers, target.triggers, options.triggers, compareTriggers()), - ]; - }, -}); diff --git a/server/src/sql-tools/comparers/trigger.comparer.spec.ts b/server/src/sql-tools/comparers/trigger.comparer.spec.ts deleted file mode 100644 index c80b0d2273..0000000000 --- a/server/src/sql-tools/comparers/trigger.comparer.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { compareTriggers } from 'src/sql-tools/comparers/trigger.comparer'; -import { DatabaseTrigger, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testTrigger: DatabaseTrigger = { - name: 'test', - tableName: 'table1', - timing: 'before', - actions: ['delete'], - scope: 'row', - functionName: 'my_trigger_function', - synchronize: true, -}; - -describe('compareTriggers', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareTriggers().onExtra(testTrigger)).toEqual([ - { - type: 'TriggerDrop', - tableName: 'table1', - triggerName: 'test', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareTriggers().onMissing(testTrigger)).toEqual([ - { - type: 'TriggerCreate', - trigger: testTrigger, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareTriggers().onCompare(testTrigger, testTrigger)).toEqual([]); - }); - - it('should detect a change in function name', () => { - const source: DatabaseTrigger = { ...testTrigger, functionName: 'my_new_name' }; - const target: DatabaseTrigger = { ...testTrigger, functionName: 'my_old_name' }; - const reason = `function is different (my_new_name vs my_old_name)`; - expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); - }); - - it('should detect a change in actions', () => { - const source: DatabaseTrigger = { ...testTrigger, actions: ['delete'] }; - const target: DatabaseTrigger = { ...testTrigger, actions: ['delete', 'insert'] }; - const reason = `action is different (delete vs delete,insert)`; - expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); - }); - - it('should detect a change in timing', () => { - const source: DatabaseTrigger = { ...testTrigger, timing: 'before' }; - const target: DatabaseTrigger = { ...testTrigger, timing: 'after' }; - const reason = `timing method is different (before vs after)`; - expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); - }); - - it('should detect a change in scope', () => { - const source: DatabaseTrigger = { ...testTrigger, scope: 'row' }; - const target: DatabaseTrigger = { ...testTrigger, scope: 'statement' }; - const reason = `scope is different (row vs statement)`; - expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); - }); - - it('should detect a change in new table reference', () => { - const source: DatabaseTrigger = { ...testTrigger, referencingNewTableAs: 'new_table' }; - const target: DatabaseTrigger = { ...testTrigger, referencingNewTableAs: undefined }; - const reason = `new table reference is different (new_table vs undefined)`; - expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); - }); - - it('should detect a change in old table reference', () => { - const source: DatabaseTrigger = { ...testTrigger, referencingOldTableAs: 'old_table' }; - const target: DatabaseTrigger = { ...testTrigger, referencingOldTableAs: undefined }; - const reason = `old table reference is different (old_table vs undefined)`; - expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/trigger.comparer.ts b/server/src/sql-tools/comparers/trigger.comparer.ts deleted file mode 100644 index 4ba2d5dba3..0000000000 --- a/server/src/sql-tools/comparers/trigger.comparer.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Comparer, DatabaseTrigger, Reason } from 'src/sql-tools/types'; - -export const compareTriggers = (): Comparer => ({ - onMissing: (source) => [ - { - type: 'TriggerCreate', - trigger: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'TriggerDrop', - tableName: target.tableName, - triggerName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => { - let reason = ''; - if (source.functionName !== target.functionName) { - reason = `function is different (${source.functionName} vs ${target.functionName})`; - } else if (source.actions.join(' OR ') !== target.actions.join(' OR ')) { - reason = `action is different (${source.actions} vs ${target.actions})`; - } else if (source.timing !== target.timing) { - reason = `timing method is different (${source.timing} vs ${target.timing})`; - } else if (source.scope !== target.scope) { - reason = `scope is different (${source.scope} vs ${target.scope})`; - } else if (source.referencingNewTableAs !== target.referencingNewTableAs) { - reason = `new table reference is different (${source.referencingNewTableAs} vs ${target.referencingNewTableAs})`; - } else if (source.referencingOldTableAs !== target.referencingOldTableAs) { - reason = `old table reference is different (${source.referencingOldTableAs} vs ${target.referencingOldTableAs})`; - } - - if (reason) { - return [{ type: 'TriggerCreate', trigger: source, reason }]; - } - - return []; - }, -}); diff --git a/server/src/sql-tools/contexts/base-context.ts b/server/src/sql-tools/contexts/base-context.ts deleted file mode 100644 index 0fa7230a00..0000000000 --- a/server/src/sql-tools/contexts/base-context.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { DefaultNamingStrategy } from 'src/sql-tools/naming/default.naming'; -import { HashNamingStrategy } from 'src/sql-tools/naming/hash.naming'; -import { NamingInterface, NamingItem } from 'src/sql-tools/naming/naming.interface'; -import { - BaseContextOptions, - DatabaseEnum, - DatabaseExtension, - DatabaseFunction, - DatabaseOverride, - DatabaseParameter, - DatabaseSchema, - DatabaseTable, -} from 'src/sql-tools/types'; - -const asOverrideKey = (type: string, name: string) => `${type}:${name}`; - -const isNamingInterface = (strategy: any): strategy is NamingInterface => { - return typeof strategy === 'object' && typeof strategy.getName === 'function'; -}; - -const asNamingStrategy = (strategy: 'hash' | 'default' | NamingInterface): NamingInterface => { - if (isNamingInterface(strategy)) { - return strategy; - } - - switch (strategy) { - case 'hash': { - return new HashNamingStrategy(); - } - - default: { - return new DefaultNamingStrategy(); - } - } -}; - -export class BaseContext { - databaseName: string; - schemaName: string; - overrideTableName: string; - - tables: DatabaseTable[] = []; - functions: DatabaseFunction[] = []; - enums: DatabaseEnum[] = []; - extensions: DatabaseExtension[] = []; - parameters: DatabaseParameter[] = []; - overrides: DatabaseOverride[] = []; - warnings: string[] = []; - - private namingStrategy: NamingInterface; - - constructor(options: BaseContextOptions) { - this.databaseName = options.databaseName ?? 'postgres'; - this.schemaName = options.schemaName ?? 'public'; - this.overrideTableName = options.overrideTableName ?? 'migration_overrides'; - this.namingStrategy = asNamingStrategy(options.namingStrategy ?? 'hash'); - } - - getNameFor(item: NamingItem) { - return this.namingStrategy.getName(item); - } - - getTableByName(name: string) { - return this.tables.find((table) => table.name === name); - } - - warn(context: string, message: string) { - this.warnings.push(`[${context}] ${message}`); - } - - build(): DatabaseSchema { - const overrideMap = new Map(); - for (const override of this.overrides) { - const { type, name } = override.value; - overrideMap.set(asOverrideKey(type, name), override); - } - - for (const func of this.functions) { - func.override = overrideMap.get(asOverrideKey('function', func.name)); - } - - for (const { indexes, triggers } of this.tables) { - for (const index of indexes) { - index.override = overrideMap.get(asOverrideKey('index', index.name)); - } - - for (const trigger of triggers) { - trigger.override = overrideMap.get(asOverrideKey('trigger', trigger.name)); - } - } - - return { - databaseName: this.databaseName, - schemaName: this.schemaName, - tables: this.tables, - functions: this.functions, - enums: this.enums, - extensions: this.extensions, - parameters: this.parameters, - overrides: this.overrides, - warnings: this.warnings, - }; - } -} diff --git a/server/src/sql-tools/contexts/processor-context.ts b/server/src/sql-tools/contexts/processor-context.ts deleted file mode 100644 index 3ab196b0af..0000000000 --- a/server/src/sql-tools/contexts/processor-context.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-function-type */ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; -import { TableOptions } from 'src/sql-tools/decorators/table.decorator'; -import { DatabaseColumn, DatabaseTable, SchemaFromCodeOptions } from 'src/sql-tools/types'; - -type TableMetadata = { options: TableOptions; object: Function; methodToColumn: Map }; - -export class ProcessorContext extends BaseContext { - constructor(public options: SchemaFromCodeOptions) { - options.createForeignKeyIndexes = options.createForeignKeyIndexes ?? true; - options.overrides = options.overrides ?? false; - super(options); - } - - classToTable: WeakMap = new WeakMap(); - tableToMetadata: WeakMap = new WeakMap(); - - getTableByObject(object: Function) { - return this.classToTable.get(object); - } - - getTableMetadata(table: DatabaseTable) { - const metadata = this.tableToMetadata.get(table); - if (!metadata) { - throw new Error(`Table metadata not found for table: ${table.name}`); - } - return metadata; - } - - addTable(table: DatabaseTable, options: TableOptions, object: Function) { - this.tables.push(table); - this.classToTable.set(object, table); - this.tableToMetadata.set(table, { options, object, methodToColumn: new Map() }); - } - - getColumnByObjectAndPropertyName( - object: object, - propertyName: string | symbol, - ): { table?: DatabaseTable; column?: DatabaseColumn } { - const table = this.getTableByObject(object.constructor); - if (!table) { - return {}; - } - - const tableMetadata = this.tableToMetadata.get(table); - if (!tableMetadata) { - return {}; - } - - const column = tableMetadata.methodToColumn.get(propertyName); - - return { table, column }; - } - - addColumn(table: DatabaseTable, column: DatabaseColumn, options: ColumnOptions, propertyName: string | symbol) { - table.columns.push(column); - const tableMetadata = this.getTableMetadata(table); - tableMetadata.methodToColumn.set(propertyName, column); - } - - warnMissingTable(context: string, object: object, propertyName?: symbol | string) { - const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : ''); - this.warn(context, `Unable to find table (${label})`); - } - - warnMissingColumn(context: string, object: object, propertyName?: symbol | string) { - const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : ''); - this.warn(context, `Unable to find column (${label})`); - } -} diff --git a/server/src/sql-tools/contexts/reader-context.ts b/server/src/sql-tools/contexts/reader-context.ts deleted file mode 100644 index 94f5c82fc1..0000000000 --- a/server/src/sql-tools/contexts/reader-context.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { SchemaFromDatabaseOptions } from 'src/sql-tools/types'; - -export class ReaderContext extends BaseContext { - constructor(public options: SchemaFromDatabaseOptions) { - super(options); - } -} diff --git a/server/src/sql-tools/decorators/after-delete.decorator.ts b/server/src/sql-tools/decorators/after-delete.decorator.ts deleted file mode 100644 index 181bfab6c8..0000000000 --- a/server/src/sql-tools/decorators/after-delete.decorator.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/decorators/trigger-function.decorator'; - -export const AfterDeleteTrigger = (options: Omit) => - TriggerFunction({ - timing: 'after', - actions: ['delete'], - ...options, - }); diff --git a/server/src/sql-tools/decorators/after-insert.decorator.ts b/server/src/sql-tools/decorators/after-insert.decorator.ts deleted file mode 100644 index c302a5cebe..0000000000 --- a/server/src/sql-tools/decorators/after-insert.decorator.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/decorators/trigger-function.decorator'; - -export const AfterInsertTrigger = (options: Omit) => - TriggerFunction({ - timing: 'after', - actions: ['insert'], - ...options, - }); diff --git a/server/src/sql-tools/decorators/before-update.decorator.ts b/server/src/sql-tools/decorators/before-update.decorator.ts deleted file mode 100644 index 2119e29c9b..0000000000 --- a/server/src/sql-tools/decorators/before-update.decorator.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/decorators/trigger-function.decorator'; - -export const BeforeUpdateTrigger = (options: Omit) => - TriggerFunction({ - timing: 'before', - actions: ['update'], - ...options, - }); diff --git a/server/src/sql-tools/decorators/check.decorator.ts b/server/src/sql-tools/decorators/check.decorator.ts deleted file mode 100644 index 56fe1ecc3f..0000000000 --- a/server/src/sql-tools/decorators/check.decorator.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { register } from 'src/sql-tools/register'; - -export type CheckOptions = { - name?: string; - expression: string; - synchronize?: boolean; -}; -export const Check = (options: CheckOptions): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => void register({ type: 'checkConstraint', item: { object, options } }); -}; diff --git a/server/src/sql-tools/decorators/column.decorator.ts b/server/src/sql-tools/decorators/column.decorator.ts deleted file mode 100644 index e5a0eb52f8..0000000000 --- a/server/src/sql-tools/decorators/column.decorator.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { asOptions } from 'src/sql-tools/helpers'; -import { register } from 'src/sql-tools/register'; -import { ColumnStorage, ColumnType, DatabaseEnum } from 'src/sql-tools/types'; - -export type ColumnValue = null | boolean | string | number | Array | object | Date | (() => string); - -export type ColumnBaseOptions = { - name?: string; - primary?: boolean; - type?: ColumnType; - nullable?: boolean; - length?: number; - default?: ColumnValue; - comment?: string; - synchronize?: boolean; - storage?: ColumnStorage; - identity?: boolean; - index?: boolean; - indexName?: string; - unique?: boolean; - uniqueConstraintName?: string; -}; - -export type ColumnOptions = ColumnBaseOptions & { - enum?: DatabaseEnum; - array?: boolean; -}; - -export const Column = (options: string | ColumnOptions = {}): PropertyDecorator => { - return (object: object, propertyName: string | symbol) => - void register({ type: 'column', item: { object, propertyName, options: asOptions(options) } }); -}; diff --git a/server/src/sql-tools/decorators/configuration-parameter.decorator.ts b/server/src/sql-tools/decorators/configuration-parameter.decorator.ts deleted file mode 100644 index 953027d25c..0000000000 --- a/server/src/sql-tools/decorators/configuration-parameter.decorator.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ColumnValue } from 'src/sql-tools/decorators/column.decorator'; -import { register } from 'src/sql-tools/register'; -import { ParameterScope } from 'src/sql-tools/types'; - -export type ConfigurationParameterOptions = { - name: string; - value: ColumnValue; - scope: ParameterScope; - synchronize?: boolean; -}; -export const ConfigurationParameter = (options: ConfigurationParameterOptions): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => void register({ type: 'configurationParameter', item: { object, options } }); -}; diff --git a/server/src/sql-tools/decorators/create-date-column.decorator.ts b/server/src/sql-tools/decorators/create-date-column.decorator.ts deleted file mode 100644 index 1a3362a614..0000000000 --- a/server/src/sql-tools/decorators/create-date-column.decorator.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; - -export const CreateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => { - return Column({ - type: 'timestamp with time zone', - default: () => 'now()', - ...options, - }); -}; diff --git a/server/src/sql-tools/decorators/database.decorator.ts b/server/src/sql-tools/decorators/database.decorator.ts deleted file mode 100644 index 17b2460df6..0000000000 --- a/server/src/sql-tools/decorators/database.decorator.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { register } from 'src/sql-tools/register'; - -export type DatabaseOptions = { - name?: string; - synchronize?: boolean; -}; -export const Database = (options: DatabaseOptions): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => void register({ type: 'database', item: { object, options } }); -}; diff --git a/server/src/sql-tools/decorators/delete-date-column.decorator.ts b/server/src/sql-tools/decorators/delete-date-column.decorator.ts deleted file mode 100644 index ca5427c27f..0000000000 --- a/server/src/sql-tools/decorators/delete-date-column.decorator.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; - -export const DeleteDateColumn = (options: ColumnOptions = {}): PropertyDecorator => { - return Column({ - type: 'timestamp with time zone', - nullable: true, - ...options, - }); -}; diff --git a/server/src/sql-tools/decorators/extension.decorator.ts b/server/src/sql-tools/decorators/extension.decorator.ts deleted file mode 100644 index d431cbfd02..0000000000 --- a/server/src/sql-tools/decorators/extension.decorator.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { asOptions } from 'src/sql-tools/helpers'; -import { register } from 'src/sql-tools/register'; - -export type ExtensionOptions = { - name: string; - synchronize?: boolean; -}; -export const Extension = (options: string | ExtensionOptions): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => void register({ type: 'extension', item: { object, options: asOptions(options) } }); -}; diff --git a/server/src/sql-tools/decorators/extensions.decorator.ts b/server/src/sql-tools/decorators/extensions.decorator.ts deleted file mode 100644 index 724446c5fa..0000000000 --- a/server/src/sql-tools/decorators/extensions.decorator.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { asOptions } from 'src/sql-tools/helpers'; -import { register } from 'src/sql-tools/register'; - -export type ExtensionsOptions = { - name: string; - synchronize?: boolean; -}; -export const Extensions = (options: Array): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => { - for (const option of options) { - register({ type: 'extension', item: { object, options: asOptions(option) } }); - } - }; -}; diff --git a/server/src/sql-tools/decorators/foreign-key-column.decorator.ts b/server/src/sql-tools/decorators/foreign-key-column.decorator.ts deleted file mode 100644 index c9c83f010d..0000000000 --- a/server/src/sql-tools/decorators/foreign-key-column.decorator.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-function-type */ -import { ForeignKeyAction } from 'src/sql-tools//decorators/foreign-key-constraint.decorator'; -import { ColumnBaseOptions } from 'src/sql-tools/decorators/column.decorator'; -import { register } from 'src/sql-tools/register'; - -export type ForeignKeyColumnOptions = ColumnBaseOptions & { - onUpdate?: ForeignKeyAction; - onDelete?: ForeignKeyAction; - constraintName?: string; -}; - -export const ForeignKeyColumn = (target: () => Function, options: ForeignKeyColumnOptions): PropertyDecorator => { - return (object: object, propertyName: string | symbol) => { - register({ type: 'foreignKeyColumn', item: { object, propertyName, options, target } }); - }; -}; diff --git a/server/src/sql-tools/decorators/foreign-key-constraint.decorator.ts b/server/src/sql-tools/decorators/foreign-key-constraint.decorator.ts deleted file mode 100644 index e5d2f513dc..0000000000 --- a/server/src/sql-tools/decorators/foreign-key-constraint.decorator.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { register } from 'src/sql-tools/register'; - -export type ForeignKeyAction = 'CASCADE' | 'SET NULL' | 'SET DEFAULT' | 'RESTRICT' | 'NO ACTION'; - -export type ForeignKeyConstraintOptions = { - name?: string; - index?: boolean; - indexName?: string; - columns: string[]; - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - referenceTable: () => Function; - referenceColumns?: string[]; - onUpdate?: ForeignKeyAction; - onDelete?: ForeignKeyAction; - synchronize?: boolean; -}; - -export const ForeignKeyConstraint = (options: ForeignKeyConstraintOptions): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (target: Function) => { - register({ type: 'foreignKeyConstraint', item: { object: target, options } }); - }; -}; diff --git a/server/src/sql-tools/decorators/generated-column.decorator.ts b/server/src/sql-tools/decorators/generated-column.decorator.ts deleted file mode 100644 index 4338b4146c..0000000000 --- a/server/src/sql-tools/decorators/generated-column.decorator.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Column, ColumnOptions, ColumnValue } from 'src/sql-tools/decorators/column.decorator'; -import { ColumnType } from 'src/sql-tools/types'; - -export type GeneratedColumnStrategy = 'uuid' | 'identity'; - -export type GenerateColumnOptions = Omit & { - strategy?: GeneratedColumnStrategy; -}; - -export const GeneratedColumn = ({ strategy = 'uuid', ...options }: GenerateColumnOptions): PropertyDecorator => { - let columnType: ColumnType | undefined; - let columnDefault: ColumnValue | undefined; - - switch (strategy) { - case 'uuid': { - columnType = 'uuid'; - columnDefault = () => 'uuid_generate_v4()'; - break; - } - - case 'identity': { - columnType = 'integer'; - options.identity = true; - break; - } - - default: { - throw new Error(`Unsupported strategy for @GeneratedColumn ${strategy}`); - } - } - - return Column({ - type: columnType, - default: columnDefault, - ...options, - }); -}; diff --git a/server/src/sql-tools/decorators/index.decorator.ts b/server/src/sql-tools/decorators/index.decorator.ts deleted file mode 100644 index 1b6d38e390..0000000000 --- a/server/src/sql-tools/decorators/index.decorator.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { asOptions } from 'src/sql-tools/helpers'; -import { register } from 'src/sql-tools/register'; - -export type IndexOptions = { - name?: string; - unique?: boolean; - expression?: string; - using?: string; - with?: string; - where?: string; - columns?: string[]; - synchronize?: boolean; -}; -export const Index = (options: string | IndexOptions = {}): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => void register({ type: 'index', item: { object, options: asOptions(options) } }); -}; diff --git a/server/src/sql-tools/decorators/primary-column.decorator.ts b/server/src/sql-tools/decorators/primary-column.decorator.ts deleted file mode 100644 index e605b4be5d..0000000000 --- a/server/src/sql-tools/decorators/primary-column.decorator.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; - -export const PrimaryColumn = (options: Omit = {}) => Column({ ...options, primary: true }); diff --git a/server/src/sql-tools/decorators/primary-generated-column.decorator.ts b/server/src/sql-tools/decorators/primary-generated-column.decorator.ts deleted file mode 100644 index 25e125ebf6..0000000000 --- a/server/src/sql-tools/decorators/primary-generated-column.decorator.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { GenerateColumnOptions, GeneratedColumn } from 'src/sql-tools/decorators/generated-column.decorator'; - -export const PrimaryGeneratedColumn = (options: Omit = {}) => - GeneratedColumn({ ...options, primary: true }); diff --git a/server/src/sql-tools/decorators/table.decorator.ts b/server/src/sql-tools/decorators/table.decorator.ts deleted file mode 100644 index 7ea5882147..0000000000 --- a/server/src/sql-tools/decorators/table.decorator.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { asOptions } from 'src/sql-tools/helpers'; -import { register } from 'src/sql-tools/register'; - -export type TableOptions = { - name?: string; - primaryConstraintName?: string; - synchronize?: boolean; -}; - -/** Table comments here */ -export const Table = (options: string | TableOptions = {}): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => void register({ type: 'table', item: { object, options: asOptions(options) } }); -}; diff --git a/server/src/sql-tools/decorators/trigger-function.decorator.ts b/server/src/sql-tools/decorators/trigger-function.decorator.ts deleted file mode 100644 index 17016f7946..0000000000 --- a/server/src/sql-tools/decorators/trigger-function.decorator.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Trigger, TriggerOptions } from 'src/sql-tools/decorators/trigger.decorator'; -import { DatabaseFunction } from 'src/sql-tools/types'; - -export type TriggerFunctionOptions = Omit & { function: DatabaseFunction }; -export const TriggerFunction = (options: TriggerFunctionOptions) => - Trigger({ - name: options.function.name, - ...options, - functionName: options.function.name, - }); diff --git a/server/src/sql-tools/decorators/trigger.decorator.ts b/server/src/sql-tools/decorators/trigger.decorator.ts deleted file mode 100644 index ce9a5c17f7..0000000000 --- a/server/src/sql-tools/decorators/trigger.decorator.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { register } from 'src/sql-tools/register'; -import { TriggerAction, TriggerScope, TriggerTiming } from 'src/sql-tools/types'; - -export type TriggerOptions = { - name?: string; - timing: TriggerTiming; - actions: TriggerAction[]; - scope: TriggerScope; - functionName: string; - referencingNewTableAs?: string; - referencingOldTableAs?: string; - when?: string; - synchronize?: boolean; -}; - -export const Trigger = (options: TriggerOptions): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => void register({ type: 'trigger', item: { object, options } }); -}; diff --git a/server/src/sql-tools/decorators/unique.decorator.ts b/server/src/sql-tools/decorators/unique.decorator.ts deleted file mode 100644 index 1f61fccb6f..0000000000 --- a/server/src/sql-tools/decorators/unique.decorator.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { register } from 'src/sql-tools/register'; - -export type UniqueOptions = { - name?: string; - columns: string[]; - synchronize?: boolean; -}; -export const Unique = (options: UniqueOptions): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => void register({ type: 'uniqueConstraint', item: { object, options } }); -}; diff --git a/server/src/sql-tools/decorators/update-date-column.decorator.ts b/server/src/sql-tools/decorators/update-date-column.decorator.ts deleted file mode 100644 index 68dd50c617..0000000000 --- a/server/src/sql-tools/decorators/update-date-column.decorator.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; - -export const UpdateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => { - return Column({ - type: 'timestamp with time zone', - default: () => 'now()', - ...options, - }); -}; diff --git a/server/src/sql-tools/helpers.ts b/server/src/sql-tools/helpers.ts deleted file mode 100644 index e0daf8262f..0000000000 --- a/server/src/sql-tools/helpers.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { createHash } from 'node:crypto'; -import { ColumnValue } from 'src/sql-tools/decorators/column.decorator'; -import { Comparer, DatabaseColumn, DatabaseOverride, IgnoreOptions, SchemaDiff } from 'src/sql-tools/types'; - -export const asOptions = (options: string | T): T => { - if (typeof options === 'string') { - return { name: options } as T; - } - - return options; -}; - -export const sha1 = (value: string) => createHash('sha1').update(value).digest('hex'); - -export const fromColumnValue = (columnValue?: ColumnValue) => { - if (columnValue === undefined) { - return; - } - - if (typeof columnValue === 'function') { - return columnValue() as string; - } - - const value = columnValue; - - if (value === null) { - return value; - } - - if (typeof value === 'number') { - return String(value); - } - - if (typeof value === 'boolean') { - return value ? 'true' : 'false'; - } - - if (value instanceof Date) { - return `'${value.toISOString()}'`; - } - - if (Array.isArray(value)) { - return "'{}'"; - } - - return `'${String(value)}'`; -}; - -export const setIsEqual = (source: Set, target: Set) => - source.size === target.size && [...source].every((x) => target.has(x)); - -export const haveEqualColumns = (sourceColumns?: string[], targetColumns?: string[]) => { - return setIsEqual(new Set(sourceColumns), new Set(targetColumns)); -}; - -export const haveEqualOverrides = (source: T, target: T) => { - if (!source.override || !target.override) { - return false; - } - - const sourceValue = source.override.value; - const targetValue = target.override.value; - - return sourceValue.name === targetValue.name && sourceValue.sql === targetValue.sql; -}; - -export const compare = ( - sources: T[], - targets: T[], - options: IgnoreOptions | undefined, - comparer: Comparer, -) => { - options = options || {}; - const sourceMap = Object.fromEntries(sources.map((table) => [table.name, table])); - const targetMap = Object.fromEntries(targets.map((table) => [table.name, table])); - const items: SchemaDiff[] = []; - - const keys = new Set([...Object.keys(sourceMap), ...Object.keys(targetMap)]); - const missingKeys = new Set(); - const extraKeys = new Set(); - - // common keys - for (const key of keys) { - const source = sourceMap[key]; - const target = targetMap[key]; - - if (isIgnored(source, target, options ?? true)) { - continue; - } - - if (isSynchronizeDisabled(source, target)) { - continue; - } - - if (source && !target) { - missingKeys.add(key); - continue; - } - - if (!source && target) { - extraKeys.add(key); - continue; - } - - if ( - haveEqualOverrides( - source as unknown as { override?: DatabaseOverride }, - target as unknown as { override?: DatabaseOverride }, - ) - ) { - continue; - } - - items.push(...comparer.onCompare(source, target)); - } - - // renames - if (comparer.getRenameKey && comparer.onRename) { - const renameMap: Record = {}; - for (const sourceKey of missingKeys) { - const source = sourceMap[sourceKey]; - const renameKey = comparer.getRenameKey(source); - renameMap[renameKey] = sourceKey; - } - - for (const targetKey of extraKeys) { - const target = targetMap[targetKey]; - const renameKey = comparer.getRenameKey(target); - const sourceKey = renameMap[renameKey]; - if (!sourceKey) { - continue; - } - - const source = sourceMap[sourceKey]; - - items.push(...comparer.onRename(source, target)); - - missingKeys.delete(sourceKey); - extraKeys.delete(targetKey); - } - } - - // missing - for (const key of missingKeys) { - items.push(...comparer.onMissing(sourceMap[key])); - } - - // extra - for (const key of extraKeys) { - items.push(...comparer.onExtra(targetMap[key])); - } - - return items; -}; - -const isIgnored = ( - source: { synchronize?: boolean } | undefined, - target: { synchronize?: boolean } | undefined, - options: IgnoreOptions, -) => { - if (typeof options === 'boolean') { - return !options; - } - return (options.ignoreExtra && !source) || (options.ignoreMissing && !target); -}; - -const isSynchronizeDisabled = (source?: { synchronize?: boolean }, target?: { synchronize?: boolean }) => { - return source?.synchronize === false || target?.synchronize === false; -}; - -export const isDefaultEqual = (source: DatabaseColumn, target: DatabaseColumn) => { - if (source.default === target.default) { - return true; - } - - if (source.default === undefined || target.default === undefined) { - return false; - } - - if ( - withTypeCast(source.default, getColumnType(source)) === target.default || - withTypeCast(target.default, getColumnType(target)) === source.default - ) { - return true; - } - - return false; -}; - -export const getColumnType = (column: DatabaseColumn) => { - let type = column.enumName || column.type; - if (column.isArray) { - type += `[${column.length ?? ''}]`; - } else if (column.length !== undefined) { - type += `(${column.length})`; - } - - return type; -}; - -const withTypeCast = (value: string, type: string) => { - if (!value.startsWith(`'`)) { - value = `'${value}'`; - } - return `${value}::${type}`; -}; - -export const getColumnModifiers = (column: DatabaseColumn) => { - const modifiers: string[] = []; - - if (!column.nullable) { - modifiers.push('NOT NULL'); - } - - if (column.default) { - modifiers.push(`DEFAULT ${column.default}`); - } - if (column.identity) { - modifiers.push(`GENERATED ALWAYS AS IDENTITY`); - } - - return modifiers.length === 0 ? '' : ' ' + modifiers.join(' '); -}; - -export const asColumnComment = (tableName: string, columnName: string, comment: string): string => { - return `COMMENT ON COLUMN "${tableName}"."${columnName}" IS '${comment}';`; -}; - -export const asColumnList = (columns: string[]) => columns.map((column) => `"${column}"`).join(', '); - -export const asJsonString = (value: unknown): string => { - return `'${escape(JSON.stringify(value))}'::jsonb`; -}; - -const escape = (value: string) => { - return value - .replaceAll("'", "''") - .replaceAll(/[\\]/g, '\\\\') - .replaceAll(/[\b]/g, String.raw`\b`) - .replaceAll(/[\f]/g, String.raw`\f`) - .replaceAll(/[\n]/g, String.raw`\n`) - .replaceAll(/[\r]/g, String.raw`\r`) - .replaceAll(/[\t]/g, String.raw`\t`); -}; - -export const asRenameKey = (values: Array) => - values.map((value) => value ?? '').join('|'); diff --git a/server/src/sql-tools/index.ts b/server/src/sql-tools/index.ts deleted file mode 100644 index 0d3e53df51..0000000000 --- a/server/src/sql-tools/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from 'src/sql-tools/public_api'; diff --git a/server/src/sql-tools/naming/default.naming.ts b/server/src/sql-tools/naming/default.naming.ts deleted file mode 100644 index 807580169d..0000000000 --- a/server/src/sql-tools/naming/default.naming.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { sha1 } from 'src/sql-tools/helpers'; -import { NamingItem } from 'src/sql-tools/naming/naming.interface'; - -const asSnakeCase = (name: string): string => name.replaceAll(/([a-z])([A-Z])/g, '$1_$2').toLowerCase(); - -export class DefaultNamingStrategy { - getName(item: NamingItem): string { - switch (item.type) { - case 'database': { - return asSnakeCase(item.name); - } - - case 'table': { - return asSnakeCase(item.name); - } - - case 'column': { - return item.name; - } - - case 'primaryKey': { - return `${item.tableName}_pkey`; - } - - case 'foreignKey': { - return `${item.tableName}_${item.columnNames.join('_')}_fkey`; - } - - case 'check': { - return `${item.tableName}_${sha1(item.expression).slice(0, 8)}_chk`; - } - - case 'unique': { - return `${item.tableName}_${item.columnNames.join('_')}_uq`; - } - - case 'index': { - if (item.columnNames) { - return `${item.tableName}_${item.columnNames.join('_')}_idx`; - } - - return `${item.tableName}_${sha1(item.expression || item.where || '').slice(0, 8)}_idx`; - } - - case 'trigger': { - return `${item.tableName}_${item.functionName}`; - } - } - } -} diff --git a/server/src/sql-tools/naming/hash.naming.ts b/server/src/sql-tools/naming/hash.naming.ts deleted file mode 100644 index 575d0f1239..0000000000 --- a/server/src/sql-tools/naming/hash.naming.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { sha1 } from 'src/sql-tools/helpers'; -import { DefaultNamingStrategy } from 'src/sql-tools/naming/default.naming'; -import { NamingInterface, NamingItem } from 'src/sql-tools/naming/naming.interface'; - -const fallback = new DefaultNamingStrategy(); - -const asKey = (prefix: string, tableName: string, values: string[]) => - (prefix + sha1(`${tableName}_${values.toSorted().join('_')}`)).slice(0, 30); - -export class HashNamingStrategy implements NamingInterface { - getName(item: NamingItem): string { - switch (item.type) { - case 'primaryKey': { - return asKey('PK_', item.tableName, item.columnNames); - } - - case 'foreignKey': { - return asKey('FK_', item.tableName, item.columnNames); - } - - case 'check': { - return asKey('CHK_', item.tableName, [item.expression]); - } - - case 'unique': { - return asKey('UQ_', item.tableName, item.columnNames); - } - - case 'index': { - const items: string[] = []; - for (const columnName of item.columnNames ?? []) { - items.push(columnName); - } - - if (item.where) { - items.push(item.where); - } - - return asKey('IDX_', item.tableName, items); - } - - case 'trigger': { - return asKey('TR_', item.tableName, [...item.actions, item.scope, item.timing, item.functionName]); - } - - default: { - return fallback.getName(item); - } - } - } -} diff --git a/server/src/sql-tools/naming/naming.interface.ts b/server/src/sql-tools/naming/naming.interface.ts deleted file mode 100644 index f331a22c46..0000000000 --- a/server/src/sql-tools/naming/naming.interface.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { TriggerAction, TriggerScope, TriggerTiming } from 'src/sql-tools/types'; - -export type NamingItem = - | { - type: 'database'; - name: string; - } - | { - type: 'table'; - name: string; - } - | { - type: 'column'; - name: string; - } - | { - type: 'primaryKey'; - tableName: string; - columnNames: string[]; - } - | { - type: 'foreignKey'; - tableName: string; - columnNames: string[]; - referenceTableName: string; - referenceColumnNames: string[]; - } - | { - type: 'check'; - tableName: string; - expression: string; - } - | { - type: 'unique'; - tableName: string; - columnNames: string[]; - } - | { - type: 'index'; - tableName: string; - columnNames?: string[]; - expression?: string; - where?: string; - } - | { - type: 'trigger'; - tableName: string; - functionName: string; - actions: TriggerAction[]; - scope: TriggerScope; - timing: TriggerTiming; - columnNames?: string[]; - expression?: string; - where?: string; - }; - -export interface NamingInterface { - getName(item: NamingItem): string; -} diff --git a/server/src/sql-tools/processors/check-constraint.processor.ts b/server/src/sql-tools/processors/check-constraint.processor.ts deleted file mode 100644 index 5eba1015bf..0000000000 --- a/server/src/sql-tools/processors/check-constraint.processor.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ConstraintType, Processor } from 'src/sql-tools/types'; - -export const processCheckConstraints: Processor = (ctx, items) => { - for (const { - item: { object, options }, - } of items.filter((item) => item.type === 'checkConstraint')) { - const table = ctx.getTableByObject(object); - if (!table) { - ctx.warnMissingTable('@Check', object); - continue; - } - - const tableName = table.name; - - table.constraints.push({ - type: ConstraintType.CHECK, - name: options.name || ctx.getNameFor({ type: 'check', tableName, expression: options.expression }), - tableName, - expression: options.expression, - synchronize: options.synchronize ?? true, - }); - } -}; diff --git a/server/src/sql-tools/processors/column.processor.ts b/server/src/sql-tools/processors/column.processor.ts deleted file mode 100644 index 9b499b380b..0000000000 --- a/server/src/sql-tools/processors/column.processor.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; -import { fromColumnValue } from 'src/sql-tools/helpers'; -import { Processor } from 'src/sql-tools/types'; - -export const processColumns: Processor = (ctx, items) => { - for (const { - type, - item: { object, propertyName, options }, - } of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) { - const table = ctx.getTableByObject(object.constructor); - if (!table) { - ctx.warnMissingTable(type === 'column' ? '@Column' : '@ForeignKeyColumn', object, propertyName); - continue; - } - - const columnName = options.name ?? ctx.getNameFor({ type: 'column', name: String(propertyName) }); - const existingColumn = table.columns.find((column) => column.name === columnName); - if (existingColumn) { - // TODO log warnings if column name is not unique - continue; - } - - let defaultValue = fromColumnValue(options.default); - let nullable = options.nullable ?? false; - - // map `{ default: null }` to `{ nullable: true }` - if (defaultValue === null) { - nullable = true; - defaultValue = undefined; - } - - const isEnum = !!(options as ColumnOptions).enum; - - ctx.addColumn( - table, - { - name: columnName, - tableName: table.name, - primary: options.primary ?? false, - default: defaultValue, - nullable, - isArray: (options as ColumnOptions).array ?? false, - length: options.length, - type: isEnum ? 'enum' : options.type || 'character varying', - enumName: isEnum ? (options as ColumnOptions).enum!.name : undefined, - comment: options.comment, - storage: options.storage, - identity: options.identity, - synchronize: options.synchronize ?? true, - }, - options, - propertyName, - ); - } -}; diff --git a/server/src/sql-tools/processors/configuration-parameter.processor.ts b/server/src/sql-tools/processors/configuration-parameter.processor.ts deleted file mode 100644 index dbb5cd4636..0000000000 --- a/server/src/sql-tools/processors/configuration-parameter.processor.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { fromColumnValue } from 'src/sql-tools/helpers'; -import { Processor } from 'src/sql-tools/types'; - -export const processConfigurationParameters: Processor = (ctx, items) => { - for (const { - item: { options }, - } of items.filter((item) => item.type === 'configurationParameter')) { - ctx.parameters.push({ - databaseName: ctx.databaseName, - name: options.name, - value: fromColumnValue(options.value), - scope: options.scope, - synchronize: options.synchronize ?? true, - }); - } -}; diff --git a/server/src/sql-tools/processors/database.processor.ts b/server/src/sql-tools/processors/database.processor.ts deleted file mode 100644 index 9f2e847fd6..0000000000 --- a/server/src/sql-tools/processors/database.processor.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Processor } from 'src/sql-tools/types'; - -export const processDatabases: Processor = (ctx, items) => { - for (const { - item: { object, options }, - } of items.filter((item) => item.type === 'database')) { - ctx.databaseName = options.name || ctx.getNameFor({ type: 'database', name: object.name }); - } -}; diff --git a/server/src/sql-tools/processors/enum.processor.ts b/server/src/sql-tools/processors/enum.processor.ts deleted file mode 100644 index 1ef65231c9..0000000000 --- a/server/src/sql-tools/processors/enum.processor.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Processor } from 'src/sql-tools/types'; - -export const processEnums: Processor = (ctx, items) => { - for (const { item } of items.filter((item) => item.type === 'enum')) { - // TODO log warnings if enum name is not unique - ctx.enums.push(item); - } -}; diff --git a/server/src/sql-tools/processors/extension.processor.ts b/server/src/sql-tools/processors/extension.processor.ts deleted file mode 100644 index 068c66883c..0000000000 --- a/server/src/sql-tools/processors/extension.processor.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Processor } from 'src/sql-tools/types'; - -export const processExtensions: Processor = (ctx, items) => { - if (ctx.options.extensions === false) { - return; - } - - for (const { - item: { options }, - } of items.filter((item) => item.type === 'extension')) { - ctx.extensions.push({ - name: options.name, - synchronize: options.synchronize ?? true, - }); - } -}; diff --git a/server/src/sql-tools/processors/foreign-key-column.processor.ts b/server/src/sql-tools/processors/foreign-key-column.processor.ts deleted file mode 100644 index 6d147a78eb..0000000000 --- a/server/src/sql-tools/processors/foreign-key-column.processor.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types'; - -export const processForeignKeyColumns: Processor = (ctx, items) => { - for (const { - item: { object, propertyName, options, target }, - } of items.filter((item) => item.type === 'foreignKeyColumn')) { - const { table, column } = ctx.getColumnByObjectAndPropertyName(object, propertyName); - if (!table) { - ctx.warnMissingTable('@ForeignKeyColumn', object); - continue; - } - - if (!column) { - // should be impossible since they are pre-created in `column.processor.ts` - ctx.warnMissingColumn('@ForeignKeyColumn', object, propertyName); - continue; - } - - const referenceTable = ctx.getTableByObject(target()); - if (!referenceTable) { - ctx.warnMissingTable('@ForeignKeyColumn', object, propertyName); - continue; - } - - const columnNames = [column.name]; - const referenceColumns = referenceTable.columns.filter((column) => column.primary); - - // infer FK column type from reference table - if (referenceColumns.length === 1) { - column.type = referenceColumns[0].type; - } - - const referenceTableName = referenceTable.name; - const referenceColumnNames = referenceColumns.map((column) => column.name); - const name = - options.constraintName || - ctx.getNameFor({ - type: 'foreignKey', - tableName: table.name, - columnNames, - referenceTableName, - referenceColumnNames, - }); - - table.constraints.push({ - name, - tableName: table.name, - columnNames, - type: ConstraintType.FOREIGN_KEY, - referenceTableName, - referenceColumnNames, - onUpdate: options.onUpdate as ActionType, - onDelete: options.onDelete as ActionType, - synchronize: options.synchronize ?? true, - }); - - if (options.unique || options.uniqueConstraintName) { - table.constraints.push({ - name: options.uniqueConstraintName || ctx.getNameFor({ type: 'unique', tableName: table.name, columnNames }), - tableName: table.name, - columnNames, - type: ConstraintType.UNIQUE, - synchronize: options.synchronize ?? true, - }); - } - } -}; diff --git a/server/src/sql-tools/processors/foreign-key-constraint.processor.ts b/server/src/sql-tools/processors/foreign-key-constraint.processor.ts deleted file mode 100644 index 39d7508d11..0000000000 --- a/server/src/sql-tools/processors/foreign-key-constraint.processor.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types'; - -export const processForeignKeyConstraints: Processor = (ctx, items) => { - for (const { - item: { object, options }, - } of items.filter((item) => item.type === 'foreignKeyConstraint')) { - const table = ctx.getTableByObject(object); - if (!table) { - ctx.warnMissingTable('@ForeignKeyConstraint', { name: 'referenceTable' }); - continue; - } - - const referenceTable = ctx.getTableByObject(options.referenceTable()); - if (!referenceTable) { - const referenceTableName = options.referenceTable()?.name; - ctx.warn( - '@ForeignKeyConstraint.referenceTable', - `Unable to find table` + (referenceTableName ? ` (${referenceTableName})` : ''), - ); - continue; - } - - let missingColumn = false; - - for (const columnName of options.columns) { - if (!table.columns.some(({ name }) => name === columnName)) { - const metadata = ctx.getTableMetadata(table); - ctx.warn('@ForeignKeyConstraint.columns', `Unable to find column (${metadata.object.name}.${columnName})`); - missingColumn = true; - } - } - - for (const columnName of options.referenceColumns || []) { - if (!referenceTable.columns.some(({ name }) => name === columnName)) { - const metadata = ctx.getTableMetadata(referenceTable); - ctx.warn( - '@ForeignKeyConstraint.referenceColumns', - `Unable to find column (${metadata.object.name}.${columnName})`, - ); - missingColumn = true; - } - } - - if (missingColumn) { - continue; - } - - const referenceTableName = referenceTable.name; - const referenceColumnNames = - options.referenceColumns || referenceTable.columns.filter(({ primary }) => primary).map(({ name }) => name); - - const name = - options.name || - ctx.getNameFor({ - type: 'foreignKey', - tableName: table.name, - columnNames: options.columns, - referenceTableName, - referenceColumnNames, - }); - - table.constraints.push({ - type: ConstraintType.FOREIGN_KEY, - name, - tableName: table.name, - columnNames: options.columns, - referenceTableName, - referenceColumnNames, - onUpdate: options.onUpdate as ActionType, - onDelete: options.onDelete as ActionType, - synchronize: options.synchronize ?? true, - }); - - if (options.index === false) { - continue; - } - - if (options.index || options.indexName || ctx.options.createForeignKeyIndexes) { - const indexName = - options.indexName || - ctx.getNameFor({ - type: 'index', - tableName: table.name, - columnNames: options.columns, - }); - table.indexes.push({ - name: indexName, - tableName: table.name, - columnNames: options.columns, - unique: false, - synchronize: options.synchronize ?? true, - }); - } - } -}; diff --git a/server/src/sql-tools/processors/function.processor.ts b/server/src/sql-tools/processors/function.processor.ts deleted file mode 100644 index 9b351b77f7..0000000000 --- a/server/src/sql-tools/processors/function.processor.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Processor } from 'src/sql-tools/types'; - -export const processFunctions: Processor = (ctx, items) => { - if (ctx.options.functions === false) { - return; - } - - for (const { item } of items.filter((item) => item.type === 'function')) { - // TODO log warnings if function name is not unique - ctx.functions.push(item); - } -}; diff --git a/server/src/sql-tools/processors/index.processor.ts b/server/src/sql-tools/processors/index.processor.ts deleted file mode 100644 index 766e83fe8b..0000000000 --- a/server/src/sql-tools/processors/index.processor.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Processor } from 'src/sql-tools/types'; - -export const processIndexes: Processor = (ctx, items) => { - for (const { - item: { object, options }, - } of items.filter((item) => item.type === 'index')) { - const table = ctx.getTableByObject(object); - if (!table) { - ctx.warnMissingTable('@Check', object); - continue; - } - - const indexName = - options.name || - ctx.getNameFor({ - type: 'index', - tableName: table.name, - columnNames: options.columns, - where: options.where, - }); - - table.indexes.push({ - name: indexName, - tableName: table.name, - unique: options.unique ?? false, - expression: options.expression, - using: options.using, - with: options.with, - where: options.where, - columnNames: options.columns, - synchronize: options.synchronize ?? true, - }); - } - - // column indexes - for (const { - type, - item: { object, propertyName, options }, - } of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) { - const { table, column } = ctx.getColumnByObjectAndPropertyName(object, propertyName); - if (!table) { - ctx.warnMissingTable('@Column', object); - continue; - } - - if (!column) { - // should be impossible since they are created in `column.processor.ts` - ctx.warnMissingColumn('@Column', object, propertyName); - continue; - } - - if (options.index === false) { - continue; - } - - const isIndexRequested = - options.indexName || options.index || (type === 'foreignKeyColumn' && ctx.options.createForeignKeyIndexes); - if (!isIndexRequested) { - continue; - } - - const indexName = - options.indexName || - ctx.getNameFor({ - type: 'index', - tableName: table.name, - columnNames: [column.name], - }); - - const isIndexPresent = table.indexes.some((index) => index.name === indexName); - if (isIndexPresent) { - continue; - } - - const isOnlyPrimaryColumn = options.primary && table.columns.filter(({ primary }) => primary === true).length === 1; - if (isOnlyPrimaryColumn) { - // will have an index created by the primary key constraint - continue; - } - - table.indexes.push({ - name: indexName, - tableName: table.name, - unique: false, - columnNames: [column.name], - synchronize: options.synchronize ?? true, - }); - } -}; diff --git a/server/src/sql-tools/processors/index.ts b/server/src/sql-tools/processors/index.ts deleted file mode 100644 index feb0a82f05..0000000000 --- a/server/src/sql-tools/processors/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { processCheckConstraints } from 'src/sql-tools/processors/check-constraint.processor'; -import { processColumns } from 'src/sql-tools/processors/column.processor'; -import { processConfigurationParameters } from 'src/sql-tools/processors/configuration-parameter.processor'; -import { processDatabases } from 'src/sql-tools/processors/database.processor'; -import { processEnums } from 'src/sql-tools/processors/enum.processor'; -import { processExtensions } from 'src/sql-tools/processors/extension.processor'; -import { processForeignKeyColumns } from 'src/sql-tools/processors/foreign-key-column.processor'; -import { processForeignKeyConstraints } from 'src/sql-tools/processors/foreign-key-constraint.processor'; -import { processFunctions } from 'src/sql-tools/processors/function.processor'; -import { processIndexes } from 'src/sql-tools/processors/index.processor'; -import { processOverrides } from 'src/sql-tools/processors/override.processor'; -import { processPrimaryKeyConstraints } from 'src/sql-tools/processors/primary-key-contraint.processor'; -import { processTables } from 'src/sql-tools/processors/table.processor'; -import { processTriggers } from 'src/sql-tools/processors/trigger.processor'; -import { processUniqueConstraints } from 'src/sql-tools/processors/unique-constraint.processor'; -import { Processor } from 'src/sql-tools/types'; - -export const processors: Processor[] = [ - processDatabases, - processConfigurationParameters, - processEnums, - processExtensions, - processFunctions, - processTables, - processColumns, - processForeignKeyColumns, - processForeignKeyConstraints, - processUniqueConstraints, - processCheckConstraints, - processPrimaryKeyConstraints, - processIndexes, - processTriggers, - processOverrides, -]; diff --git a/server/src/sql-tools/processors/override.processor.ts b/server/src/sql-tools/processors/override.processor.ts deleted file mode 100644 index 67b92fbd40..0000000000 --- a/server/src/sql-tools/processors/override.processor.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { asFunctionCreate } from 'src/sql-tools/transformers/function.transformer'; -import { asIndexCreate } from 'src/sql-tools/transformers/index.transformer'; -import { asTriggerCreate } from 'src/sql-tools/transformers/trigger.transformer'; -import { Processor } from 'src/sql-tools/types'; - -export const processOverrides: Processor = (ctx) => { - if (ctx.options.overrides === false) { - return; - } - - for (const func of ctx.functions) { - if (!func.synchronize) { - continue; - } - - ctx.overrides.push({ - name: `function_${func.name}`, - value: { type: 'function', name: func.name, sql: asFunctionCreate(func) }, - synchronize: true, - }); - } - - for (const { triggers, indexes } of ctx.tables) { - for (const trigger of triggers) { - if (!trigger.synchronize) { - continue; - } - - ctx.overrides.push({ - name: `trigger_${trigger.name}`, - value: { type: 'trigger', name: trigger.name, sql: asTriggerCreate(trigger) }, - synchronize: true, - }); - } - - for (const index of indexes) { - if (!index.synchronize) { - continue; - } - - if (index.expression || index.using || index.with || index.where) { - ctx.overrides.push({ - name: `index_${index.name}`, - value: { type: 'index', name: index.name, sql: asIndexCreate(index) }, - synchronize: true, - }); - } - } - } -}; diff --git a/server/src/sql-tools/processors/primary-key-contraint.processor.ts b/server/src/sql-tools/processors/primary-key-contraint.processor.ts deleted file mode 100644 index 0971bfc337..0000000000 --- a/server/src/sql-tools/processors/primary-key-contraint.processor.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ConstraintType, Processor } from 'src/sql-tools/types'; - -export const processPrimaryKeyConstraints: Processor = (ctx) => { - for (const table of ctx.tables) { - const columnNames: string[] = []; - - for (const column of table.columns) { - if (column.primary) { - columnNames.push(column.name); - } - } - - if (columnNames.length > 0) { - const tableMetadata = ctx.getTableMetadata(table); - table.constraints.push({ - type: ConstraintType.PRIMARY_KEY, - name: - tableMetadata.options.primaryConstraintName || - ctx.getNameFor({ - type: 'primaryKey', - tableName: table.name, - columnNames, - }), - tableName: table.name, - columnNames, - synchronize: tableMetadata.options.synchronize ?? true, - }); - } - } -}; diff --git a/server/src/sql-tools/processors/table.processor.ts b/server/src/sql-tools/processors/table.processor.ts deleted file mode 100644 index 993c9ec45d..0000000000 --- a/server/src/sql-tools/processors/table.processor.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Processor } from 'src/sql-tools/types'; - -export const processTables: Processor = (ctx, items) => { - for (const { - item: { options, object }, - } of items.filter((item) => item.type === 'table')) { - const test = ctx.getTableByObject(object); - if (test) { - throw new Error( - `Table ${test.name} has already been registered. Does ${object.name} have two @Table() decorators?`, - ); - } - - ctx.addTable( - { - name: options.name || ctx.getNameFor({ type: 'table', name: object.name }), - columns: [], - constraints: [], - indexes: [], - triggers: [], - synchronize: options.synchronize ?? true, - }, - options, - object, - ); - } -}; diff --git a/server/src/sql-tools/processors/trigger.processor.ts b/server/src/sql-tools/processors/trigger.processor.ts deleted file mode 100644 index b50b42cc49..0000000000 --- a/server/src/sql-tools/processors/trigger.processor.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Processor } from 'src/sql-tools/types'; - -export const processTriggers: Processor = (ctx, items) => { - for (const { - item: { object, options }, - } of items.filter((item) => item.type === 'trigger')) { - const table = ctx.getTableByObject(object); - if (!table) { - ctx.warnMissingTable('@Trigger', object); - continue; - } - - const triggerName = - options.name || - ctx.getNameFor({ - type: 'trigger', - tableName: table.name, - actions: options.actions, - scope: options.scope, - timing: options.timing, - functionName: options.functionName, - }); - - table.triggers.push({ - name: triggerName, - tableName: table.name, - timing: options.timing, - actions: options.actions, - when: options.when, - scope: options.scope, - referencingNewTableAs: options.referencingNewTableAs, - referencingOldTableAs: options.referencingOldTableAs, - functionName: options.functionName, - synchronize: options.synchronize ?? true, - }); - } -}; diff --git a/server/src/sql-tools/processors/unique-constraint.processor.ts b/server/src/sql-tools/processors/unique-constraint.processor.ts deleted file mode 100644 index 0cbfc26a70..0000000000 --- a/server/src/sql-tools/processors/unique-constraint.processor.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { ConstraintType, Processor } from 'src/sql-tools/types'; - -export const processUniqueConstraints: Processor = (ctx, items) => { - for (const { - item: { object, options }, - } of items.filter((item) => item.type === 'uniqueConstraint')) { - const table = ctx.getTableByObject(object); - if (!table) { - ctx.warnMissingTable('@Unique', object); - continue; - } - - const tableName = table.name; - const columnNames = options.columns; - - table.constraints.push({ - type: ConstraintType.UNIQUE, - name: options.name || ctx.getNameFor({ type: 'unique', tableName, columnNames }), - tableName, - columnNames, - synchronize: options.synchronize ?? true, - }); - } - - // column level constraints - for (const { - type, - item: { object, propertyName, options }, - } of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) { - const { table, column } = ctx.getColumnByObjectAndPropertyName(object, propertyName); - if (!table) { - ctx.warnMissingTable('@Column', object); - continue; - } - - if (!column) { - // should be impossible since they are created in `column.processor.ts` - ctx.warnMissingColumn('@Column', object, propertyName); - continue; - } - - if (type === 'column' && !options.primary && (options.unique || options.uniqueConstraintName)) { - const uniqueConstraintName = - options.uniqueConstraintName || - ctx.getNameFor({ - type: 'unique', - tableName: table.name, - columnNames: [column.name], - }); - - table.constraints.push({ - type: ConstraintType.UNIQUE, - name: uniqueConstraintName, - tableName: table.name, - columnNames: [column.name], - synchronize: options.synchronize ?? true, - }); - } - } -}; diff --git a/server/src/sql-tools/public_api.ts b/server/src/sql-tools/public_api.ts deleted file mode 100644 index 9e7983383e..0000000000 --- a/server/src/sql-tools/public_api.ts +++ /dev/null @@ -1,31 +0,0 @@ -export * from 'src/sql-tools/decorators/after-delete.decorator'; -export * from 'src/sql-tools/decorators/after-insert.decorator'; -export * from 'src/sql-tools/decorators/before-update.decorator'; -export * from 'src/sql-tools/decorators/check.decorator'; -export * from 'src/sql-tools/decorators/column.decorator'; -export * from 'src/sql-tools/decorators/configuration-parameter.decorator'; -export * from 'src/sql-tools/decorators/create-date-column.decorator'; -export * from 'src/sql-tools/decorators/database.decorator'; -export * from 'src/sql-tools/decorators/delete-date-column.decorator'; -export * from 'src/sql-tools/decorators/extension.decorator'; -export * from 'src/sql-tools/decorators/extensions.decorator'; -export * from 'src/sql-tools/decorators/foreign-key-column.decorator'; -export * from 'src/sql-tools/decorators/foreign-key-constraint.decorator'; -export * from 'src/sql-tools/decorators/generated-column.decorator'; -export * from 'src/sql-tools/decorators/index.decorator'; -export * from 'src/sql-tools/decorators/primary-column.decorator'; -export * from 'src/sql-tools/decorators/primary-generated-column.decorator'; -export * from 'src/sql-tools/decorators/table.decorator'; -export * from 'src/sql-tools/decorators/trigger-function.decorator'; -export * from 'src/sql-tools/decorators/trigger.decorator'; -export * from 'src/sql-tools/decorators/unique.decorator'; -export * from 'src/sql-tools/decorators/update-date-column.decorator'; -export * from 'src/sql-tools/naming/default.naming'; -export * from 'src/sql-tools/naming/hash.naming'; -export * from 'src/sql-tools/naming/naming.interface'; -export * from 'src/sql-tools/register-enum'; -export * from 'src/sql-tools/register-function'; -export { schemaDiff, schemaDiffToSql } from 'src/sql-tools/schema-diff'; -export { schemaFromCode } from 'src/sql-tools/schema-from-code'; -export { schemaFromDatabase } from 'src/sql-tools/schema-from-database'; -export * from 'src/sql-tools/types'; diff --git a/server/src/sql-tools/readers/column.reader.ts b/server/src/sql-tools/readers/column.reader.ts deleted file mode 100644 index 249bd77f2c..0000000000 --- a/server/src/sql-tools/readers/column.reader.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { sql } from 'kysely'; -import { jsonArrayFrom } from 'kysely/helpers/postgres'; -import { ColumnType, DatabaseColumn, Reader } from 'src/sql-tools/types'; - -export const readColumns: Reader = async (ctx, db) => { - const columns = await db - .selectFrom('information_schema.columns as c') - .leftJoin('information_schema.element_types as o', (join) => - join - .onRef('c.table_catalog', '=', 'o.object_catalog') - .onRef('c.table_schema', '=', 'o.object_schema') - .onRef('c.table_name', '=', 'o.object_name') - .on('o.object_type', '=', sql.lit('TABLE')) - .onRef('c.dtd_identifier', '=', 'o.collection_type_identifier'), - ) - .leftJoin('pg_type as t', (join) => - join.onRef('t.typname', '=', 'c.udt_name').on('c.data_type', '=', sql.lit('USER-DEFINED')), - ) - .leftJoin('pg_enum as e', (join) => join.onRef('e.enumtypid', '=', 't.oid')) - .select([ - 'c.table_name', - 'c.column_name', - - // is ARRAY, USER-DEFINED, or data type - 'c.data_type', - 'c.column_default', - 'c.is_nullable', - 'c.character_maximum_length', - - // number types - 'c.numeric_precision', - 'c.numeric_scale', - - // date types - 'c.datetime_precision', - - // user defined type - 'c.udt_catalog', - 'c.udt_schema', - 'c.udt_name', - - // data type for ARRAYs - 'o.data_type as array_type', - ]) - .where('table_schema', '=', ctx.schemaName) - .execute(); - - const enumRaw = await db - .selectFrom('pg_type') - .innerJoin('pg_namespace', (join) => - join.onRef('pg_namespace.oid', '=', 'pg_type.typnamespace').on('pg_namespace.nspname', '=', ctx.schemaName), - ) - .where('typtype', '=', sql.lit('e')) - .select((eb) => [ - 'pg_type.typname as name', - jsonArrayFrom( - eb.selectFrom('pg_enum as e').select(['e.enumlabel as value']).whereRef('e.enumtypid', '=', 'pg_type.oid'), - ).as('values'), - ]) - .execute(); - - const enums = enumRaw.map((item) => ({ name: item.name, values: item.values.map(({ value }) => value) })); - for (const { name, values } of enums) { - ctx.enums.push({ name, values, synchronize: true }); - } - - const enumMap = Object.fromEntries(enums.map((e) => [e.name, e.values])); - // add columns to tables - for (const column of columns) { - const table = ctx.getTableByName(column.table_name); - if (!table) { - continue; - } - - const columnName = column.column_name; - - const item: DatabaseColumn = { - type: column.data_type as ColumnType, - // TODO infer this from PK constraints - primary: false, - name: columnName, - tableName: column.table_name, - nullable: column.is_nullable === 'YES', - isArray: column.array_type !== null, - numericPrecision: column.numeric_precision ?? undefined, - numericScale: column.numeric_scale ?? undefined, - length: column.character_maximum_length ?? undefined, - default: column.column_default ?? undefined, - synchronize: true, - }; - - const columnLabel = `${table.name}.${columnName}`; - - switch (column.data_type) { - // array types - case 'ARRAY': { - if (!column.array_type) { - ctx.warnings.push(`Unable to find type for ${columnLabel} (ARRAY)`); - continue; - } - item.type = column.array_type as ColumnType; - break; - } - - // enum types - case 'USER-DEFINED': { - if (!enumMap[column.udt_name]) { - ctx.warnings.push(`Unable to find type for ${columnLabel} (ENUM)`); - continue; - } - - item.type = 'enum'; - item.enumName = column.udt_name; - break; - } - } - - table.columns.push(item); - } -}; diff --git a/server/src/sql-tools/readers/comment.reader.ts b/server/src/sql-tools/readers/comment.reader.ts deleted file mode 100644 index 05cc91e7a9..0000000000 --- a/server/src/sql-tools/readers/comment.reader.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Reader } from 'src/sql-tools/types'; - -export const readComments: Reader = async (ctx, db) => { - const comments = await db - .selectFrom('pg_description as d') - .innerJoin('pg_class as c', 'd.objoid', 'c.oid') - .leftJoin('pg_attribute as a', (join) => - join.onRef('a.attrelid', '=', 'c.oid').onRef('a.attnum', '=', 'd.objsubid'), - ) - .select([ - 'c.relname as object_name', - 'c.relkind as object_type', - 'd.description as value', - 'a.attname as column_name', - ]) - .where('d.description', 'is not', null) - .orderBy('object_type') - .orderBy('object_name') - .execute(); - - for (const comment of comments) { - if (comment.object_type === 'r') { - const table = ctx.getTableByName(comment.object_name); - if (!table) { - continue; - } - - if (comment.column_name) { - const column = table.columns.find(({ name }) => name === comment.column_name); - if (column) { - column.comment = comment.value; - } - } - } - } -}; diff --git a/server/src/sql-tools/readers/constraint.reader.ts b/server/src/sql-tools/readers/constraint.reader.ts deleted file mode 100644 index 662c6f414a..0000000000 --- a/server/src/sql-tools/readers/constraint.reader.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { sql } from 'kysely'; -import { ActionType, ConstraintType, Reader } from 'src/sql-tools/types'; - -export const readConstraints: Reader = async (ctx, db) => { - const constraints = await db - .selectFrom('pg_constraint') - .innerJoin('pg_namespace', 'pg_namespace.oid', 'pg_constraint.connamespace') // namespace - .innerJoin('pg_class as source_table', (join) => - join.onRef('source_table.oid', '=', 'pg_constraint.conrelid').on('source_table.relkind', 'in', [ - // ordinary table - sql.lit('r'), - // partitioned table - sql.lit('p'), - // foreign table - sql.lit('f'), - ]), - ) // table - .leftJoin('pg_class as reference_table', 'reference_table.oid', 'pg_constraint.confrelid') // reference table - .select((eb) => [ - 'pg_constraint.contype as constraint_type', - 'pg_constraint.conname as constraint_name', - 'source_table.relname as table_name', - 'reference_table.relname as reference_table_name', - 'pg_constraint.confupdtype as update_action', - 'pg_constraint.confdeltype as delete_action', - // 'pg_constraint.oid as constraint_id', - eb - .selectFrom('pg_attribute') - // matching table for PK, FK, and UQ - .whereRef('pg_attribute.attrelid', '=', 'pg_constraint.conrelid') - .whereRef('pg_attribute.attnum', '=', sql`any("pg_constraint"."conkey")`) - .select((eb) => eb.fn('json_agg', ['pg_attribute.attname']).as('column_name')) - .as('column_names'), - eb - .selectFrom('pg_attribute') - // matching foreign table for FK - .whereRef('pg_attribute.attrelid', '=', 'pg_constraint.confrelid') - .whereRef('pg_attribute.attnum', '=', sql`any("pg_constraint"."confkey")`) - .select((eb) => eb.fn('json_agg', ['pg_attribute.attname']).as('column_name')) - .as('reference_column_names'), - eb.fn('pg_get_constraintdef', ['pg_constraint.oid']).as('expression'), - ]) - .where('pg_namespace.nspname', '=', ctx.schemaName) - .execute(); - - for (const constraint of constraints) { - const table = ctx.getTableByName(constraint.table_name); - if (!table) { - continue; - } - - const constraintName = constraint.constraint_name; - - switch (constraint.constraint_type) { - // primary key constraint - case 'p': { - if (!constraint.column_names) { - ctx.warnings.push(`Skipping CONSTRAINT "${constraintName}", no columns found`); - continue; - } - table.constraints.push({ - type: ConstraintType.PRIMARY_KEY, - name: constraintName, - tableName: constraint.table_name, - columnNames: constraint.column_names, - synchronize: true, - }); - break; - } - - // foreign key constraint - case 'f': { - if (!constraint.column_names || !constraint.reference_table_name || !constraint.reference_column_names) { - ctx.warnings.push( - `Skipping CONSTRAINT "${constraintName}", missing either columns, referenced table, or referenced columns,`, - ); - continue; - } - - table.constraints.push({ - type: ConstraintType.FOREIGN_KEY, - name: constraintName, - tableName: constraint.table_name, - columnNames: constraint.column_names, - referenceTableName: constraint.reference_table_name, - referenceColumnNames: constraint.reference_column_names, - onUpdate: asDatabaseAction(constraint.update_action), - onDelete: asDatabaseAction(constraint.delete_action), - synchronize: true, - }); - break; - } - - // unique constraint - case 'u': { - table.constraints.push({ - type: ConstraintType.UNIQUE, - name: constraintName, - tableName: constraint.table_name, - columnNames: constraint.column_names as string[], - synchronize: true, - }); - break; - } - - // check constraint - case 'c': { - table.constraints.push({ - type: ConstraintType.CHECK, - name: constraint.constraint_name, - tableName: constraint.table_name, - expression: constraint.expression.replace('CHECK ', ''), - synchronize: true, - }); - break; - } - } - } -}; - -const asDatabaseAction = (action: string) => { - switch (action) { - case 'a': { - return ActionType.NO_ACTION; - } - case 'c': { - return ActionType.CASCADE; - } - case 'r': { - return ActionType.RESTRICT; - } - case 'n': { - return ActionType.SET_NULL; - } - case 'd': { - return ActionType.SET_DEFAULT; - } - - default: { - return ActionType.NO_ACTION; - } - } -}; diff --git a/server/src/sql-tools/readers/extension.reader.ts b/server/src/sql-tools/readers/extension.reader.ts deleted file mode 100644 index aa33f4d21e..0000000000 --- a/server/src/sql-tools/readers/extension.reader.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Reader } from 'src/sql-tools/types'; - -export const readExtensions: Reader = async (ctx, db) => { - const extensions = await db - .selectFrom('pg_catalog.pg_extension') - // .innerJoin('pg_namespace', 'pg_namespace.oid', 'pg_catalog.pg_extension.extnamespace') - // .where('pg_namespace.nspname', '=', schemaName) - .select(['extname as name', 'extversion as version']) - .execute(); - - for (const { name } of extensions) { - ctx.extensions.push({ name, synchronize: true }); - } -}; diff --git a/server/src/sql-tools/readers/function.reader.ts b/server/src/sql-tools/readers/function.reader.ts deleted file mode 100644 index 4696747f52..0000000000 --- a/server/src/sql-tools/readers/function.reader.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { sql } from 'kysely'; -import { Reader } from 'src/sql-tools/types'; - -export const readFunctions: Reader = async (ctx, db) => { - const routines = await db - .selectFrom('pg_proc as p') - .innerJoin('pg_namespace', 'pg_namespace.oid', 'p.pronamespace') - .leftJoin('pg_depend as d', (join) => join.onRef('d.objid', '=', 'p.oid').on('d.deptype', '=', sql.lit('e'))) - .where('d.objid', 'is', sql.lit(null)) - .where('p.prokind', '=', sql.lit('f')) - .where('pg_namespace.nspname', '=', ctx.schemaName) - .select((eb) => [ - 'p.proname as name', - eb.fn('pg_get_function_identity_arguments', ['p.oid']).as('arguments'), - eb.fn('pg_get_functiondef', ['p.oid']).as('expression'), - ]) - .execute(); - - for (const { name, expression } of routines) { - ctx.functions.push({ - name, - // TODO read expression from the overrides table - expression, - synchronize: true, - }); - } -}; diff --git a/server/src/sql-tools/readers/index.reader.ts b/server/src/sql-tools/readers/index.reader.ts deleted file mode 100644 index 26b17a0d19..0000000000 --- a/server/src/sql-tools/readers/index.reader.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { sql } from 'kysely'; -import { Reader } from 'src/sql-tools/types'; - -export const readIndexes: Reader = async (ctx, db) => { - const indexes = await db - .selectFrom('pg_index as ix') - // matching index, which has column information - .innerJoin('pg_class as i', 'ix.indexrelid', 'i.oid') - .innerJoin('pg_am as a', 'i.relam', 'a.oid') - // matching table - .innerJoin('pg_class as t', 'ix.indrelid', 't.oid') - // namespace - .innerJoin('pg_namespace', 'pg_namespace.oid', 'i.relnamespace') - // PK and UQ constraints automatically have indexes, so we can ignore those - .leftJoin('pg_constraint', (join) => - join - .onRef('pg_constraint.conindid', '=', 'i.oid') - .on('pg_constraint.contype', 'in', [sql.lit('p'), sql.lit('u')]), - ) - .where('pg_constraint.oid', 'is', null) - .select((eb) => [ - 'i.relname as index_name', - 't.relname as table_name', - 'ix.indisunique as unique', - 'a.amname as using', - eb.fn('pg_get_expr', ['ix.indexprs', 'ix.indrelid']).as('expression'), - eb.fn('pg_get_expr', ['ix.indpred', 'ix.indrelid']).as('where'), - eb - .selectFrom('pg_attribute as a') - .where('t.relkind', '=', sql.lit('r')) - .whereRef('a.attrelid', '=', 't.oid') - // list of columns numbers in the index - .whereRef('a.attnum', '=', sql`any("ix"."indkey")`) - .select((eb) => eb.fn('json_agg', ['a.attname']).as('column_name')) - .as('column_names'), - ]) - .where('pg_namespace.nspname', '=', ctx.schemaName) - .where('ix.indisprimary', '=', sql.lit(false)) - .execute(); - - for (const index of indexes) { - const table = ctx.getTableByName(index.table_name); - if (!table) { - continue; - } - - table.indexes.push({ - name: index.index_name, - tableName: index.table_name, - columnNames: index.column_names ?? undefined, - expression: index.expression ?? undefined, - using: index.using, - where: index.where ?? undefined, - unique: index.unique, - synchronize: true, - }); - } -}; diff --git a/server/src/sql-tools/readers/index.ts b/server/src/sql-tools/readers/index.ts deleted file mode 100644 index 354f99c7ca..0000000000 --- a/server/src/sql-tools/readers/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { readColumns } from 'src/sql-tools/readers/column.reader'; -import { readComments } from 'src/sql-tools/readers/comment.reader'; -import { readConstraints } from 'src/sql-tools/readers/constraint.reader'; -import { readExtensions } from 'src/sql-tools/readers/extension.reader'; -import { readFunctions } from 'src/sql-tools/readers/function.reader'; -import { readIndexes } from 'src/sql-tools/readers/index.reader'; -import { readName } from 'src/sql-tools/readers/name.reader'; -import { readOverrides } from 'src/sql-tools/readers/override.reader'; -import { readParameters } from 'src/sql-tools/readers/parameter.reader'; -import { readTables } from 'src/sql-tools/readers/table.reader'; -import { readTriggers } from 'src/sql-tools/readers/trigger.reader'; -import { Reader } from 'src/sql-tools/types'; - -export const readers: Reader[] = [ - readName, - readParameters, - readExtensions, - readFunctions, - readTables, - readColumns, - readIndexes, - readConstraints, - readTriggers, - readComments, - readOverrides, -]; diff --git a/server/src/sql-tools/readers/name.reader.ts b/server/src/sql-tools/readers/name.reader.ts deleted file mode 100644 index de4f1af3a6..0000000000 --- a/server/src/sql-tools/readers/name.reader.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { QueryResult, sql } from 'kysely'; -import { Reader } from 'src/sql-tools/types'; - -export const readName: Reader = async (ctx, db) => { - const result = (await sql`SELECT current_database() as name`.execute(db)) as QueryResult<{ name: string }>; - - ctx.databaseName = result.rows[0].name; -}; diff --git a/server/src/sql-tools/readers/override.reader.ts b/server/src/sql-tools/readers/override.reader.ts deleted file mode 100644 index 34f0004f95..0000000000 --- a/server/src/sql-tools/readers/override.reader.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { sql } from 'kysely'; -import { OverrideType, Reader } from 'src/sql-tools/types'; - -export const readOverrides: Reader = async (ctx, db) => { - try { - const result = await sql - .raw<{ - name: string; - value: { type: OverrideType; name: string; sql: string }; - }>(`SELECT name, value FROM "${ctx.overrideTableName}"`) - .execute(db); - - for (const { name, value } of result.rows) { - ctx.overrides.push({ name, value, synchronize: true }); - } - } catch (error) { - ctx.warn('Overrides', `Error reading override table: ${error}`); - } -}; diff --git a/server/src/sql-tools/readers/parameter.reader.ts b/server/src/sql-tools/readers/parameter.reader.ts deleted file mode 100644 index c5f36591a3..0000000000 --- a/server/src/sql-tools/readers/parameter.reader.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { sql } from 'kysely'; -import { ParameterScope, Reader } from 'src/sql-tools/types'; - -export const readParameters: Reader = async (ctx, db) => { - const parameters = await db - .selectFrom('pg_settings') - .where('source', 'in', [sql.lit('database'), sql.lit('user')]) - .select(['name', 'setting as value', 'source as scope']) - .execute(); - - for (const parameter of parameters) { - ctx.parameters.push({ - name: parameter.name, - value: parameter.value, - databaseName: ctx.databaseName, - scope: parameter.scope as ParameterScope, - synchronize: true, - }); - } -}; diff --git a/server/src/sql-tools/readers/table.reader.ts b/server/src/sql-tools/readers/table.reader.ts deleted file mode 100644 index 4570179bbf..0000000000 --- a/server/src/sql-tools/readers/table.reader.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { sql } from 'kysely'; -import { Reader } from 'src/sql-tools/types'; - -export const readTables: Reader = async (ctx, db) => { - const tables = await db - .selectFrom('information_schema.tables') - .where('table_schema', '=', ctx.schemaName) - .where('table_type', '=', sql.lit('BASE TABLE')) - .selectAll() - .execute(); - - for (const table of tables) { - ctx.tables.push({ - name: table.table_name, - columns: [], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }); - } -}; diff --git a/server/src/sql-tools/readers/trigger.reader.ts b/server/src/sql-tools/readers/trigger.reader.ts deleted file mode 100644 index 92fb1d12bf..0000000000 --- a/server/src/sql-tools/readers/trigger.reader.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Reader, TriggerAction, TriggerScope, TriggerTiming } from 'src/sql-tools/types'; - -export const readTriggers: Reader = async (ctx, db) => { - const triggers = await db - .selectFrom('pg_trigger as t') - .innerJoin('pg_proc as p', 't.tgfoid', 'p.oid') - .innerJoin('pg_namespace as n', 'p.pronamespace', 'n.oid') - .innerJoin('pg_class as c', 't.tgrelid', 'c.oid') - .select((eb) => [ - 't.tgname as name', - 't.tgenabled as enabled', - 't.tgtype as type', - 't.tgconstraint as _constraint', - 't.tgdeferrable as is_deferrable', - 't.tginitdeferred as is_initially_deferred', - 't.tgargs as arguments', - 't.tgoldtable as referencing_old_table_as', - 't.tgnewtable as referencing_new_table_as', - eb.fn('pg_get_expr', ['t.tgqual', 't.tgrelid']).as('when_expression'), - 'p.proname as function_name', - 'c.relname as table_name', - ]) - .where('t.tgisinternal', '=', false) // Exclude internal system triggers - .where('n.nspname', '=', ctx.schemaName) - .execute(); - - // add triggers to tables - for (const trigger of triggers) { - const table = ctx.getTableByName(trigger.table_name); - if (!table) { - continue; - } - - table.triggers.push({ - name: trigger.name, - tableName: trigger.table_name, - functionName: trigger.function_name, - referencingNewTableAs: trigger.referencing_new_table_as ?? undefined, - referencingOldTableAs: trigger.referencing_old_table_as ?? undefined, - when: trigger.when_expression, - synchronize: true, - ...parseTriggerType(trigger.type), - }); - } -}; - -export const hasMask = (input: number, mask: number) => (input & mask) === mask; - -export const parseTriggerType = (type: number) => { - // eslint-disable-next-line unicorn/prefer-math-trunc - const scope: TriggerScope = hasMask(type, 1 << 0) ? 'row' : 'statement'; - - let timing: TriggerTiming = 'after'; - const timingMasks: Array<{ mask: number; value: TriggerTiming }> = [ - { mask: 1 << 1, value: 'before' }, - { mask: 1 << 6, value: 'instead of' }, - ]; - - for (const { mask, value } of timingMasks) { - if (hasMask(type, mask)) { - timing = value; - break; - } - } - - const actions: TriggerAction[] = []; - const actionMasks: Array<{ mask: number; value: TriggerAction }> = [ - { mask: 1 << 2, value: 'insert' }, - { mask: 1 << 3, value: 'delete' }, - { mask: 1 << 4, value: 'update' }, - { mask: 1 << 5, value: 'truncate' }, - ]; - - for (const { mask, value } of actionMasks) { - if (hasMask(type, mask)) { - actions.push(value); - break; - } - } - - if (actions.length === 0) { - throw new Error(`Unable to parse trigger type ${type}`); - } - - return { actions, timing, scope }; -}; diff --git a/server/src/sql-tools/register-enum.ts b/server/src/sql-tools/register-enum.ts deleted file mode 100644 index 5e9b41adcb..0000000000 --- a/server/src/sql-tools/register-enum.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { register } from 'src/sql-tools/register'; -import { DatabaseEnum } from 'src/sql-tools/types'; - -export type EnumOptions = { - name: string; - values: string[]; - synchronize?: boolean; -}; - -export const registerEnum = (options: EnumOptions) => { - const item: DatabaseEnum = { - name: options.name, - values: options.values, - synchronize: options.synchronize ?? true, - }; - - register({ type: 'enum', item }); - - return item; -}; diff --git a/server/src/sql-tools/register-function.ts b/server/src/sql-tools/register-function.ts deleted file mode 100644 index 9f1c84c4fa..0000000000 --- a/server/src/sql-tools/register-function.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { register } from 'src/sql-tools/register'; -import { ColumnType, DatabaseFunction } from 'src/sql-tools/types'; - -export type FunctionOptions = { - name: string; - arguments?: string[]; - returnType: ColumnType | string; - language?: 'SQL' | 'PLPGSQL'; - behavior?: 'immutable' | 'stable' | 'volatile'; - parallel?: 'safe' | 'unsafe' | 'restricted'; - strict?: boolean; - synchronize?: boolean; -} & ({ body: string } | { return: string }); - -export const registerFunction = (options: FunctionOptions) => { - const name = options.name; - const expression = asFunctionExpression(options); - - const item: DatabaseFunction = { - name, - expression, - synchronize: options.synchronize ?? true, - }; - - register({ type: 'function', item }); - - return item; -}; - -const asFunctionExpression = (options: FunctionOptions) => { - const name = options.name; - const sql: string[] = [ - `CREATE OR REPLACE FUNCTION ${name}(${(options.arguments || []).join(', ')})`, - `RETURNS ${options.returnType}`, - ]; - - const flags = [ - options.parallel ? `PARALLEL ${options.parallel.toUpperCase()}` : undefined, - options.strict ? 'STRICT' : undefined, - options.behavior ? options.behavior.toUpperCase() : undefined, - `LANGUAGE ${options.language ?? 'SQL'}`, - ].filter((x) => x !== undefined); - - if (flags.length > 0) { - sql.push(flags.join(' ')); - } - - if ('return' in options) { - sql.push(` RETURN ${options.return}`); - } - - if ('body' in options) { - const body = options.body; - sql.push(...(body.includes('\n') ? [`AS $$`, ' ' + body.trim(), `$$;`] : [`AS $$${body}$$;`])); - } - - return sql.join('\n ').trim(); -}; diff --git a/server/src/sql-tools/register-item.ts b/server/src/sql-tools/register-item.ts deleted file mode 100644 index fede281a1b..0000000000 --- a/server/src/sql-tools/register-item.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-function-type */ -import { CheckOptions } from 'src/sql-tools/decorators/check.decorator'; -import { ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; -import { ConfigurationParameterOptions } from 'src/sql-tools/decorators/configuration-parameter.decorator'; -import { DatabaseOptions } from 'src/sql-tools/decorators/database.decorator'; -import { ExtensionOptions } from 'src/sql-tools/decorators/extension.decorator'; -import { ForeignKeyColumnOptions } from 'src/sql-tools/decorators/foreign-key-column.decorator'; -import { ForeignKeyConstraintOptions } from 'src/sql-tools/decorators/foreign-key-constraint.decorator'; -import { IndexOptions } from 'src/sql-tools/decorators/index.decorator'; -import { TableOptions } from 'src/sql-tools/decorators/table.decorator'; -import { TriggerOptions } from 'src/sql-tools/decorators/trigger.decorator'; -import { UniqueOptions } from 'src/sql-tools/decorators/unique.decorator'; -import { DatabaseEnum, DatabaseFunction } from 'src/sql-tools/types'; - -export type ClassBased = { object: Function } & T; -export type PropertyBased = { object: object; propertyName: string | symbol } & T; -export type RegisterItem = - | { type: 'database'; item: ClassBased<{ options: DatabaseOptions }> } - | { type: 'table'; item: ClassBased<{ options: TableOptions }> } - | { type: 'index'; item: ClassBased<{ options: IndexOptions }> } - | { type: 'uniqueConstraint'; item: ClassBased<{ options: UniqueOptions }> } - | { type: 'checkConstraint'; item: ClassBased<{ options: CheckOptions }> } - | { type: 'column'; item: PropertyBased<{ options: ColumnOptions }> } - | { type: 'function'; item: DatabaseFunction } - | { type: 'enum'; item: DatabaseEnum } - | { type: 'trigger'; item: ClassBased<{ options: TriggerOptions }> } - | { type: 'extension'; item: ClassBased<{ options: ExtensionOptions }> } - | { type: 'configurationParameter'; item: ClassBased<{ options: ConfigurationParameterOptions }> } - | { type: 'foreignKeyColumn'; item: PropertyBased<{ options: ForeignKeyColumnOptions; target: () => Function }> } - | { type: 'foreignKeyConstraint'; item: ClassBased<{ options: ForeignKeyConstraintOptions }> }; -export type RegisterItemType = Extract['item']; diff --git a/server/src/sql-tools/register.ts b/server/src/sql-tools/register.ts deleted file mode 100644 index 4df04c935a..0000000000 --- a/server/src/sql-tools/register.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { RegisterItem } from 'src/sql-tools/register-item'; - -const items: RegisterItem[] = []; - -export const register = (item: RegisterItem) => void items.push(item); - -export const getRegisteredItems = () => items; - -export const resetRegisteredItems = () => { - items.length = 0; -}; diff --git a/server/src/sql-tools/schema-diff.spec.ts b/server/src/sql-tools/schema-diff.spec.ts deleted file mode 100644 index f45fb98bd3..0000000000 --- a/server/src/sql-tools/schema-diff.spec.ts +++ /dev/null @@ -1,689 +0,0 @@ -import { schemaDiff } from 'src/sql-tools/schema-diff'; -import { - ActionType, - ColumnType, - ConstraintType, - DatabaseColumn, - DatabaseConstraint, - DatabaseIndex, - DatabaseSchema, - DatabaseTable, -} from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const fromColumn = (column: Partial>): DatabaseSchema => { - const tableName = 'table1'; - - return { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: tableName, - columns: [ - { - name: 'column1', - primary: false, - synchronize: true, - isArray: false, - type: 'character varying', - nullable: false, - ...column, - tableName, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], - }; -}; - -const fromConstraint = (constraint?: DatabaseConstraint): DatabaseSchema => { - const tableName = constraint?.tableName || 'table1'; - - return { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: tableName, - columns: [ - { - name: 'column1', - primary: false, - synchronize: true, - isArray: false, - type: 'character varying', - nullable: false, - tableName, - }, - ], - indexes: [], - triggers: [], - constraints: constraint ? [constraint] : [], - synchronize: true, - }, - ], - warnings: [], - }; -}; - -const fromIndex = (index?: DatabaseIndex): DatabaseSchema => { - const tableName = index?.tableName || 'table1'; - - return { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: tableName, - columns: [ - { - name: 'column1', - primary: false, - synchronize: true, - isArray: false, - type: 'character varying', - nullable: false, - tableName, - }, - ], - indexes: index ? [index] : [], - constraints: [], - triggers: [], - synchronize: true, - }, - ], - warnings: [], - }; -}; - -const newSchema = (schema: { - name?: string; - tables: Array<{ - name: string; - columns?: Array<{ - name: string; - type?: ColumnType; - nullable?: boolean; - isArray?: boolean; - }>; - indexes?: DatabaseIndex[]; - constraints?: DatabaseConstraint[]; - }>; -}): DatabaseSchema => { - const tables: DatabaseTable[] = []; - - for (const table of schema.tables || []) { - const tableName = table.name; - const columns: DatabaseColumn[] = []; - - for (const column of table.columns || []) { - const columnName = column.name; - - columns.push({ - tableName, - name: columnName, - primary: false, - type: column.type || 'character varying', - isArray: column.isArray ?? false, - nullable: column.nullable ?? false, - synchronize: true, - }); - } - - tables.push({ - name: tableName, - columns, - indexes: table.indexes ?? [], - constraints: table.constraints ?? [], - triggers: [], - synchronize: true, - }); - } - - return { - databaseName: 'immich', - schemaName: schema?.name || 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables, - warnings: [], - }; -}; - -describe(schemaDiff.name, () => { - it('should work', () => { - const diff = schemaDiff(newSchema({ tables: [] }), newSchema({ tables: [] })); - expect(diff.items).toEqual([]); - }); - - describe('table', () => { - describe('TableCreate', () => { - it('should find a missing table', () => { - const column: DatabaseColumn = { - type: 'character varying', - tableName: 'table1', - primary: false, - name: 'column1', - isArray: false, - nullable: false, - synchronize: true, - }; - const diff = schemaDiff( - newSchema({ tables: [{ name: 'table1', columns: [column] }] }), - newSchema({ tables: [] }), - ); - - expect(diff.items).toHaveLength(1); - expect(diff.items[0]).toEqual({ - type: 'TableCreate', - table: { - name: 'table1', - columns: [column], - constraints: [], - indexes: [], - triggers: [], - synchronize: true, - }, - reason: 'missing in target', - }); - }); - }); - - describe('TableDrop', () => { - it('should find an extra table', () => { - const diff = schemaDiff( - newSchema({ tables: [] }), - newSchema({ - tables: [{ name: 'table1', columns: [{ name: 'column1' }] }], - }), - { tables: { ignoreExtra: false } }, - ); - - expect(diff.items).toHaveLength(1); - expect(diff.items[0]).toEqual({ - type: 'TableDrop', - tableName: 'table1', - reason: 'missing in source', - }); - }); - }); - - it('should skip identical tables', () => { - const diff = schemaDiff( - newSchema({ - tables: [{ name: 'table1', columns: [{ name: 'column1' }] }], - }), - newSchema({ - tables: [{ name: 'table1', columns: [{ name: 'column1' }] }], - }), - ); - - expect(diff.items).toEqual([]); - }); - }); - - describe('column', () => { - describe('ColumnAdd', () => { - it('should find a new column', () => { - const diff = schemaDiff( - newSchema({ - tables: [ - { - name: 'table1', - columns: [{ name: 'column1' }, { name: 'column2' }], - }, - ], - }), - newSchema({ - tables: [{ name: 'table1', columns: [{ name: 'column1' }] }], - }), - ); - - expect(diff.items).toEqual([ - { - type: 'ColumnAdd', - column: { - tableName: 'table1', - isArray: false, - primary: false, - name: 'column2', - nullable: false, - type: 'character varying', - synchronize: true, - }, - reason: 'missing in target', - }, - ]); - }); - }); - - describe('ColumnDrop', () => { - it('should find an extra column', () => { - const diff = schemaDiff( - newSchema({ - tables: [{ name: 'table1', columns: [{ name: 'column1' }] }], - }), - newSchema({ - tables: [ - { - name: 'table1', - columns: [{ name: 'column1' }, { name: 'column2' }], - }, - ], - }), - ); - - expect(diff.items).toEqual([ - { - type: 'ColumnDrop', - tableName: 'table1', - columnName: 'column2', - reason: 'missing in source', - }, - ]); - }); - }); - - describe('nullable', () => { - it('should make a column nullable', () => { - const diff = schemaDiff( - fromColumn({ name: 'column1', nullable: true }), - fromColumn({ name: 'column1', nullable: false }), - ); - - expect(diff.items).toEqual([ - { - type: 'ColumnAlter', - tableName: 'table1', - columnName: 'column1', - changes: { - nullable: true, - }, - reason: 'nullable is different (true vs false)', - }, - ]); - }); - - it('should make a column non-nullable', () => { - const diff = schemaDiff( - fromColumn({ name: 'column1', nullable: false }), - fromColumn({ name: 'column1', nullable: true }), - ); - - expect(diff.items).toEqual([ - { - type: 'ColumnAlter', - tableName: 'table1', - columnName: 'column1', - changes: { - nullable: false, - }, - reason: 'nullable is different (false vs true)', - }, - ]); - }); - }); - - describe('default', () => { - it('should set a default value to a function', () => { - const diff = schemaDiff( - fromColumn({ name: 'column1', default: 'uuid_generate_v4()' }), - fromColumn({ name: 'column1' }), - ); - - expect(diff.items).toEqual([ - { - type: 'ColumnAlter', - tableName: 'table1', - columnName: 'column1', - changes: { - default: 'uuid_generate_v4()', - }, - reason: 'default is different (uuid_generate_v4() vs undefined)', - }, - ]); - }); - - it('should ignore explicit casts for strings', () => { - const diff = schemaDiff( - fromColumn({ name: 'column1', type: 'character varying', default: `''` }), - fromColumn({ name: 'column1', type: 'character varying', default: `''::character varying` }), - ); - - expect(diff.items).toEqual([]); - }); - - it('should ignore explicit casts for numbers', () => { - const diff = schemaDiff( - fromColumn({ name: 'column1', type: 'bigint', default: `0` }), - fromColumn({ name: 'column1', type: 'bigint', default: `'0'::bigint` }), - ); - - expect(diff.items).toEqual([]); - }); - - it('should ignore explicit casts for enums', () => { - const diff = schemaDiff( - fromColumn({ name: 'column1', type: 'enum', enumName: 'enum1', default: `test` }), - fromColumn({ name: 'column1', type: 'enum', enumName: 'enum1', default: `'test'::enum1` }), - ); - - expect(diff.items).toEqual([]); - }); - - it('should support arrays, ignoring types', () => { - const diff = schemaDiff( - fromColumn({ name: 'column1', type: 'character varying', isArray: true, default: "'{}'" }), - fromColumn({ - name: 'column1', - type: 'character varying', - isArray: true, - default: "'{}'::character varying[]", - }), - ); - - expect(diff.items).toEqual([]); - }); - }); - }); - - describe('constraint', () => { - describe('ConstraintAdd', () => { - it('should detect a new constraint', () => { - const diff = schemaDiff( - fromConstraint({ - name: 'PK_test', - type: ConstraintType.PRIMARY_KEY, - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }), - fromConstraint(), - ); - - expect(diff.items).toEqual([ - { - type: 'ConstraintAdd', - constraint: { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_test', - columnNames: ['id'], - tableName: 'table1', - synchronize: true, - }, - reason: 'missing in target', - }, - ]); - }); - }); - - describe('ConstraintDrop', () => { - it('should detect an extra constraint', () => { - const diff = schemaDiff( - fromConstraint(), - fromConstraint({ - name: 'PK_test', - type: ConstraintType.PRIMARY_KEY, - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }), - ); - - expect(diff.items).toEqual([ - { - type: 'ConstraintDrop', - tableName: 'table1', - constraintName: 'PK_test', - reason: 'missing in source', - }, - ]); - }); - }); - - describe('primary key', () => { - it('should skip identical primary key constraints', () => { - const constraint: DatabaseConstraint = { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_test', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }; - - const diff = schemaDiff(fromConstraint({ ...constraint }), fromConstraint({ ...constraint })); - - expect(diff.items).toEqual([]); - }); - }); - - describe('foreign key', () => { - it('should skip identical foreign key constraints', () => { - const constraint: DatabaseConstraint = { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_test', - tableName: 'table1', - columnNames: ['parentId'], - referenceTableName: 'table2', - referenceColumnNames: ['id'], - synchronize: true, - }; - - const diff = schemaDiff(fromConstraint(constraint), fromConstraint(constraint)); - - expect(diff.items).toEqual([]); - }); - - it('should drop and recreate when the column changes', () => { - const constraint: DatabaseConstraint = { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_test', - tableName: 'table1', - columnNames: ['parentId'], - referenceTableName: 'table2', - referenceColumnNames: ['id'], - synchronize: true, - }; - - const diff = schemaDiff( - fromConstraint(constraint), - fromConstraint({ ...constraint, columnNames: ['parentId2'] }), - ); - - expect(diff.items).toEqual([ - { - constraintName: 'FK_test', - reason: 'columns are different (parentId vs parentId2)', - tableName: 'table1', - type: 'ConstraintDrop', - }, - { - constraint: { - columnNames: ['parentId'], - name: 'FK_test', - referenceColumnNames: ['id'], - referenceTableName: 'table2', - synchronize: true, - tableName: 'table1', - type: 'foreign-key', - }, - reason: 'columns are different (parentId vs parentId2)', - type: 'ConstraintAdd', - }, - ]); - }); - - it('should drop and recreate when the ON DELETE action changes', () => { - const constraint: DatabaseConstraint = { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_test', - tableName: 'table1', - columnNames: ['parentId'], - referenceTableName: 'table2', - referenceColumnNames: ['id'], - onDelete: ActionType.CASCADE, - synchronize: true, - }; - - const diff = schemaDiff(fromConstraint(constraint), fromConstraint({ ...constraint, onDelete: undefined })); - - expect(diff.items).toEqual([ - { - constraintName: 'FK_test', - reason: 'ON DELETE action is different (CASCADE vs NO ACTION)', - tableName: 'table1', - type: 'ConstraintDrop', - }, - { - constraint: { - columnNames: ['parentId'], - name: 'FK_test', - referenceColumnNames: ['id'], - referenceTableName: 'table2', - onDelete: ActionType.CASCADE, - synchronize: true, - tableName: 'table1', - type: 'foreign-key', - }, - reason: 'ON DELETE action is different (CASCADE vs NO ACTION)', - type: 'ConstraintAdd', - }, - ]); - }); - }); - - describe('unique', () => { - it('should skip identical unique constraints', () => { - const constraint: DatabaseConstraint = { - type: ConstraintType.UNIQUE, - name: 'UQ_test', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }; - - const diff = schemaDiff(fromConstraint({ ...constraint }), fromConstraint({ ...constraint })); - - expect(diff.items).toEqual([]); - }); - }); - - describe('check', () => { - it('should skip identical check constraints', () => { - const constraint: DatabaseConstraint = { - type: ConstraintType.CHECK, - name: 'CHK_test', - tableName: 'table1', - expression: 'column1 > 0', - synchronize: true, - }; - - const diff = schemaDiff(fromConstraint({ ...constraint }), fromConstraint({ ...constraint })); - - expect(diff.items).toEqual([]); - }); - }); - }); - - describe('index', () => { - describe('IndexCreate', () => { - it('should detect a new index', () => { - const diff = schemaDiff( - fromIndex({ - name: 'IDX_test', - tableName: 'table1', - columnNames: ['id'], - unique: false, - synchronize: true, - }), - fromIndex(), - ); - - expect(diff.items).toEqual([ - { - type: 'IndexCreate', - index: { - name: 'IDX_test', - columnNames: ['id'], - tableName: 'table1', - unique: false, - synchronize: true, - }, - reason: 'missing in target', - }, - ]); - }); - }); - - describe('IndexDrop', () => { - it('should detect an extra index', () => { - const diff = schemaDiff( - fromIndex(), - fromIndex({ - name: 'IDX_test', - unique: true, - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }), - ); - - expect(diff.items).toEqual([ - { - type: 'IndexDrop', - indexName: 'IDX_test', - reason: 'missing in source', - }, - ]); - }); - }); - - it('should recreate the index if unique changes', () => { - const index: DatabaseIndex = { - name: 'IDX_test', - tableName: 'table1', - columnNames: ['id'], - unique: true, - synchronize: true, - }; - const diff = schemaDiff(fromIndex(index), fromIndex({ ...index, unique: false })); - - expect(diff.items).toEqual([ - { - type: 'IndexDrop', - indexName: 'IDX_test', - reason: 'uniqueness is different (true vs false)', - }, - { - type: 'IndexCreate', - index, - reason: 'uniqueness is different (true vs false)', - }, - ]); - }); - }); -}); diff --git a/server/src/sql-tools/schema-diff.ts b/server/src/sql-tools/schema-diff.ts deleted file mode 100644 index 846210931b..0000000000 --- a/server/src/sql-tools/schema-diff.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { compareEnums } from 'src/sql-tools/comparers/enum.comparer'; -import { compareExtensions } from 'src/sql-tools/comparers/extension.comparer'; -import { compareFunctions } from 'src/sql-tools/comparers/function.comparer'; -import { compareOverrides } from 'src/sql-tools/comparers/override.comparer'; -import { compareParameters } from 'src/sql-tools/comparers/parameter.comparer'; -import { compareTables } from 'src/sql-tools/comparers/table.comparer'; -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { compare } from 'src/sql-tools/helpers'; -import { transformers } from 'src/sql-tools/transformers'; -import { - ConstraintType, - DatabaseSchema, - SchemaDiff, - SchemaDiffOptions, - SchemaDiffToSqlOptions, -} from 'src/sql-tools/types'; - -/** - * Compute the difference between two database schemas - */ -export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, options: SchemaDiffOptions = {}) => { - const items = [ - ...compare(source.parameters, target.parameters, options.parameters, compareParameters()), - ...compare(source.extensions, target.extensions, options.extensions, compareExtensions()), - ...compare(source.functions, target.functions, options.functions, compareFunctions()), - ...compare(source.enums, target.enums, options.enums, compareEnums()), - ...compare(source.tables, target.tables, options.tables, compareTables(options)), - ...compare(source.overrides, target.overrides, options.overrides, compareOverrides()), - ]; - - type SchemaName = SchemaDiff['type']; - const itemMap: Record = { - ColumnRename: [], - ConstraintRename: [], - IndexRename: [], - - ExtensionDrop: [], - ExtensionCreate: [], - - ParameterSet: [], - ParameterReset: [], - - FunctionDrop: [], - FunctionCreate: [], - - EnumDrop: [], - EnumCreate: [], - - TriggerDrop: [], - ConstraintDrop: [], - TableDrop: [], - ColumnDrop: [], - ColumnAdd: [], - ColumnAlter: [], - TableCreate: [], - ConstraintAdd: [], - TriggerCreate: [], - - IndexCreate: [], - IndexDrop: [], - - OverrideCreate: [], - OverrideUpdate: [], - OverrideDrop: [], - }; - - for (const item of items) { - itemMap[item.type].push(item); - } - - const constraintAdds = itemMap.ConstraintAdd.filter((item) => item.type === 'ConstraintAdd'); - - const orderedItems = [ - ...itemMap.ExtensionCreate, - ...itemMap.FunctionCreate, - ...itemMap.ParameterSet, - ...itemMap.ParameterReset, - ...itemMap.EnumCreate, - ...itemMap.TriggerDrop, - ...itemMap.IndexDrop, - ...itemMap.ConstraintDrop, - ...itemMap.TableCreate, - ...itemMap.ColumnAlter, - ...itemMap.ColumnAdd, - ...itemMap.ColumnRename, - ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.PRIMARY_KEY), - ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.FOREIGN_KEY), - ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.UNIQUE), - ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.CHECK), - ...itemMap.ConstraintRename, - ...itemMap.IndexCreate, - ...itemMap.IndexRename, - ...itemMap.TriggerCreate, - ...itemMap.ColumnDrop, - ...itemMap.TableDrop, - ...itemMap.EnumDrop, - ...itemMap.FunctionDrop, - ...itemMap.OverrideCreate, - ...itemMap.OverrideUpdate, - ...itemMap.OverrideDrop, - ]; - - return { - items: orderedItems, - asSql: (options?: SchemaDiffToSqlOptions) => schemaDiffToSql(orderedItems, options), - asHuman: () => schemaDiffToHuman(orderedItems), - }; -}; - -/** - * Convert schema diffs into SQL statements - */ -export const schemaDiffToSql = (items: SchemaDiff[], options: SchemaDiffToSqlOptions = {}): string[] => { - return items.flatMap((item) => asSql(item, options)); -}; - -/** - * Convert schema diff into human readable statements - */ -export const schemaDiffToHuman = (items: SchemaDiff[]): string[] => { - return items.flatMap((item) => asHuman(item)); -}; - -export const asSql = (item: SchemaDiff, options: SchemaDiffToSqlOptions): string[] => { - const ctx = new BaseContext(options); - for (const transform of transformers) { - const result = transform(ctx, item); - if (!result) { - continue; - } - - return asArray(result).map((result) => result + withComments(options.comments, item)); - } - - throw new Error(`Unhandled schema diff type: ${item.type}`); -}; - -export const asHuman = (item: SchemaDiff): string => { - switch (item.type) { - case 'ExtensionCreate': { - return `The extension "${item.extension.name}" is missing and needs to be created`; - } - case 'ExtensionDrop': { - return `The extension "${item.extensionName}" exists but is no longer needed`; - } - case 'FunctionCreate': { - return `The function "${item.function.name}" is missing and needs to be created`; - } - case 'FunctionDrop': { - return `The function "${item.functionName}" exists but should be removed`; - } - case 'TableCreate': { - return `The table "${item.table.name}" is missing and needs to be created`; - } - case 'TableDrop': { - return `The table "${item.tableName}" exists but should be removed`; - } - case 'ColumnAdd': { - return `The column "${item.column.tableName}"."${item.column.name}" is missing and needs to be created`; - } - case 'ColumnRename': { - return `The column "${item.tableName}"."${item.oldName}" was renamed to "${item.tableName}"."${item.newName}"`; - } - case 'ColumnAlter': { - return `The column "${item.tableName}"."${item.columnName}" has changes that need to be applied ${JSON.stringify( - item.changes, - )}`; - } - case 'ColumnDrop': { - return `The column "${item.tableName}"."${item.columnName}" exists but should be removed`; - } - case 'ConstraintAdd': { - return `The constraint "${item.constraint.tableName}"."${item.constraint.name}" (${item.constraint.type}) is missing and needs to be created`; - } - case 'ConstraintRename': { - return `The constraint "${item.tableName}"."${item.oldName}" was renamed to "${item.tableName}"."${item.newName}"`; - } - case 'ConstraintDrop': { - return `The constraint "${item.tableName}"."${item.constraintName}" exists but should be removed`; - } - case 'IndexCreate': { - return `The index "${item.index.tableName}"."${item.index.name}" is missing and needs to be created`; - } - case 'IndexRename': { - return `The index "${item.tableName}"."${item.oldName}" was renamed to "${item.tableName}"."${item.newName}"`; - } - case 'IndexDrop': { - return `The index "${item.indexName}" exists but is no longer needed`; - } - case 'TriggerCreate': { - return `The trigger "${item.trigger.tableName}"."${item.trigger.name}" is missing and needs to be created`; - } - case 'TriggerDrop': { - return `The trigger "${item.tableName}"."${item.triggerName}" exists but is no longer needed`; - } - case 'ParameterSet': { - return `The configuration parameter "${item.parameter.name}" has a different value and needs to be updated to "${item.parameter.value}"`; - } - case 'ParameterReset': { - return `The configuration parameter "${item.parameterName}" is set, but should be reset to the default value`; - } - case 'EnumCreate': { - return `The enum "${item.enum.name}" is missing and needs to be created`; - } - case 'EnumDrop': { - return `The enum "${item.enumName}" exists but is no longer needed`; - } - case 'OverrideCreate': { - return `The override "${item.override.name}" is missing and needs to be created`; - } - case 'OverrideUpdate': { - return `The override "${item.override.name}" needs to be updated`; - } - case 'OverrideDrop': { - return `The override "${item.overrideName}" exists but is no longer needed`; - } - } -}; - -const withComments = (comments: boolean | undefined, item: SchemaDiff): string => { - if (!comments) { - return ''; - } - - return ` -- ${item.reason}`; -}; - -const asArray = (items: T | T[]): T[] => { - if (Array.isArray(items)) { - return items; - } - - return [items]; -}; diff --git a/server/src/sql-tools/schema-from-code.spec.ts b/server/src/sql-tools/schema-from-code.spec.ts deleted file mode 100644 index b0c88d1f57..0000000000 --- a/server/src/sql-tools/schema-from-code.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { readdirSync } from 'node:fs'; -import { join } from 'node:path'; -import { schemaFromCode } from 'src/sql-tools/schema-from-code'; -import { SchemaFromCodeOptions } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const importModule = async (filePath: string) => { - const module = await import(filePath); - const options: SchemaFromCodeOptions = module.options; - - return { module, options }; -}; - -describe(schemaFromCode.name, () => { - it('should work', () => { - expect(schemaFromCode({ reset: true })).toEqual({ - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [], - warnings: [], - }); - }); - - describe('test files', () => { - const errorStubs = readdirSync('test/sql-tools/errors', { withFileTypes: true }); - for (const file of errorStubs) { - const filePath = join(file.parentPath, file.name); - it(filePath, async () => { - const { module, options } = await importModule(filePath); - - expect(module.message).toBeDefined(); - expect(() => schemaFromCode({ ...options, reset: true })).toThrowError(module.message); - }); - } - - const stubs = readdirSync('test/sql-tools', { withFileTypes: true }); - for (const file of stubs) { - if (file.isDirectory()) { - continue; - } - - const filePath = join(file.parentPath, file.name); - it(filePath, async () => { - const { module, options } = await importModule(filePath); - - expect(module.description).toBeDefined(); - expect(module.schema).toBeDefined(); - expect(schemaFromCode({ ...options, reset: true }), module.description).toEqual(module.schema); - }); - } - }); -}); diff --git a/server/src/sql-tools/schema-from-code.ts b/server/src/sql-tools/schema-from-code.ts deleted file mode 100644 index 2e19f414e4..0000000000 --- a/server/src/sql-tools/schema-from-code.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ProcessorContext } from 'src/sql-tools/contexts/processor-context'; -import { processors } from 'src/sql-tools/processors'; -import { getRegisteredItems, resetRegisteredItems } from 'src/sql-tools/register'; -import { ConstraintType, SchemaFromCodeOptions } from 'src/sql-tools/types'; - -/** - * Load schema from code (decorators, etc) - */ -export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => { - try { - const ctx = new ProcessorContext(options); - const items = getRegisteredItems(); - - for (const processor of processors) { - processor(ctx, items); - } - - if (ctx.options.overrides) { - ctx.tables.push({ - name: ctx.overrideTableName, - columns: [ - { - name: 'name', - tableName: ctx.overrideTableName, - primary: true, - type: 'character varying', - nullable: false, - isArray: false, - synchronize: true, - }, - { - name: 'value', - tableName: ctx.overrideTableName, - primary: false, - type: 'jsonb', - nullable: false, - isArray: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: `${ctx.overrideTableName}_pkey`, - tableName: ctx.overrideTableName, - columnNames: ['name'], - synchronize: true, - }, - ], - synchronize: true, - }); - } - - return ctx.build(); - } finally { - if (options.reset) { - resetRegisteredItems(); - } - } -}; diff --git a/server/src/sql-tools/schema-from-database.ts b/server/src/sql-tools/schema-from-database.ts deleted file mode 100644 index ee34e9dd8d..0000000000 --- a/server/src/sql-tools/schema-from-database.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Kysely } from 'kysely'; -import { PostgresJSDialect } from 'kysely-postgres-js'; -import { Sql } from 'postgres'; -import { ReaderContext } from 'src/sql-tools/contexts/reader-context'; -import { readers } from 'src/sql-tools/readers'; -import { DatabaseSchema, PostgresDB, SchemaFromDatabaseOptions } from 'src/sql-tools/types'; - -export type DatabaseLike = Sql | Kysely; - -const isKysely = (db: DatabaseLike): db is Kysely => db instanceof Kysely; - -/** - * Load schema from a database url - */ -export const schemaFromDatabase = async ( - database: DatabaseLike, - options: SchemaFromDatabaseOptions = {}, -): Promise => { - const db = isKysely(database) - ? (database as Kysely) - : new Kysely({ dialect: new PostgresJSDialect({ postgres: database }) }); - const ctx = new ReaderContext(options); - - try { - for (const reader of readers) { - await reader(ctx, db); - } - - return ctx.build(); - } finally { - // only close the connection it we created it - if (!isKysely(database)) { - await db.destroy(); - } - } -}; diff --git a/server/src/sql-tools/transformers/column.transformer.spec.ts b/server/src/sql-tools/transformers/column.transformer.spec.ts deleted file mode 100644 index 6828e2a72d..0000000000 --- a/server/src/sql-tools/transformers/column.transformer.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { transformColumns } from 'src/sql-tools/transformers/column.transformer'; -import { describe, expect, it } from 'vitest'; - -const ctx = new BaseContext({}); - -describe(transformColumns.name, () => { - describe('ColumnAdd', () => { - it('should work', () => { - expect( - transformColumns(ctx, { - type: 'ColumnAdd', - column: { - name: 'column1', - tableName: 'table1', - primary: false, - type: 'character varying', - nullable: false, - isArray: false, - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('ALTER TABLE "table1" ADD "column1" character varying NOT NULL;'); - }); - - it('should add a nullable column', () => { - expect( - transformColumns(ctx, { - type: 'ColumnAdd', - column: { - name: 'column1', - tableName: 'table1', - primary: false, - type: 'character varying', - nullable: true, - isArray: false, - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('ALTER TABLE "table1" ADD "column1" character varying;'); - }); - - it('should add a column with an enum type', () => { - expect( - transformColumns(ctx, { - type: 'ColumnAdd', - column: { - name: 'column1', - tableName: 'table1', - primary: false, - type: 'character varying', - enumName: 'table1_column1_enum', - nullable: true, - isArray: false, - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('ALTER TABLE "table1" ADD "column1" table1_column1_enum;'); - }); - - it('should add a column that is an array type', () => { - expect( - transformColumns(ctx, { - type: 'ColumnAdd', - column: { - name: 'column1', - tableName: 'table1', - primary: false, - type: 'boolean', - nullable: true, - isArray: true, - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('ALTER TABLE "table1" ADD "column1" boolean[];'); - }); - }); - - describe('ColumnAlter', () => { - it('should make a column nullable', () => { - expect( - transformColumns(ctx, { - type: 'ColumnAlter', - tableName: 'table1', - columnName: 'column1', - changes: { nullable: true }, - reason: 'unknown', - }), - ).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" DROP NOT NULL;`]); - }); - - it('should make a column non-nullable', () => { - expect( - transformColumns(ctx, { - type: 'ColumnAlter', - tableName: 'table1', - columnName: 'column1', - changes: { nullable: false }, - reason: 'unknown', - }), - ).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" SET NOT NULL;`]); - }); - - it('should update the default value', () => { - expect( - transformColumns(ctx, { - type: 'ColumnAlter', - tableName: 'table1', - columnName: 'column1', - changes: { default: 'uuid_generate_v4()' }, - reason: 'unknown', - }), - ).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" SET DEFAULT uuid_generate_v4();`]); - }); - - it('should update the default value to NULL', () => { - expect( - transformColumns(ctx, { - type: 'ColumnAlter', - tableName: 'table1', - columnName: 'column1', - changes: { - default: 'NULL', - }, - reason: 'unknown', - }), - ).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" SET DEFAULT NULL;`]); - }); - }); - - describe('ColumnDrop', () => { - it('should work', () => { - expect( - transformColumns(ctx, { - type: 'ColumnDrop', - tableName: 'table1', - columnName: 'column1', - reason: 'unknown', - }), - ).toEqual(`ALTER TABLE "table1" DROP COLUMN "column1";`); - }); - }); -}); diff --git a/server/src/sql-tools/transformers/column.transformer.ts b/server/src/sql-tools/transformers/column.transformer.ts deleted file mode 100644 index ffa565e533..0000000000 --- a/server/src/sql-tools/transformers/column.transformer.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { asColumnComment, getColumnModifiers, getColumnType } from 'src/sql-tools/helpers'; -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { ColumnChanges, DatabaseColumn } from 'src/sql-tools/types'; - -export const transformColumns: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'ColumnAdd': { - return asColumnAdd(item.column); - } - - case 'ColumnAlter': { - return asColumnAlter(item.tableName, item.columnName, item.changes); - } - - case 'ColumnRename': { - return `ALTER TABLE "${item.tableName}" RENAME COLUMN "${item.oldName}" TO "${item.newName}";`; - } - - case 'ColumnDrop': { - return `ALTER TABLE "${item.tableName}" DROP COLUMN "${item.columnName}";`; - } - - default: { - return false; - } - } -}; - -const asColumnAdd = (column: DatabaseColumn): string => { - return ( - `ALTER TABLE "${column.tableName}" ADD "${column.name}" ${getColumnType(column)}` + getColumnModifiers(column) + ';' - ); -}; - -export const asColumnAlter = (tableName: string, columnName: string, changes: ColumnChanges): string[] => { - const base = `ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}"`; - const items: string[] = []; - if (changes.nullable !== undefined) { - items.push(changes.nullable ? `${base} DROP NOT NULL;` : `${base} SET NOT NULL;`); - } - - if (changes.default !== undefined) { - items.push(`${base} SET DEFAULT ${changes.default};`); - } - - if (changes.storage !== undefined) { - items.push(`${base} SET STORAGE ${changes.storage.toUpperCase()};`); - } - - if (changes.comment !== undefined) { - items.push(asColumnComment(tableName, columnName, changes.comment)); - } - - return items; -}; diff --git a/server/src/sql-tools/transformers/constraint.transformer.spec.ts b/server/src/sql-tools/transformers/constraint.transformer.spec.ts deleted file mode 100644 index 6e512afdca..0000000000 --- a/server/src/sql-tools/transformers/constraint.transformer.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { transformConstraints } from 'src/sql-tools/transformers/constraint.transformer'; -import { ConstraintType } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const ctx = new BaseContext({}); - -describe(transformConstraints.name, () => { - describe('ConstraintAdd', () => { - describe('primary keys', () => { - it('should work', () => { - expect( - transformConstraints(ctx, { - type: 'ConstraintAdd', - constraint: { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_test', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('ALTER TABLE "table1" ADD CONSTRAINT "PK_test" PRIMARY KEY ("id");'); - }); - }); - - describe('foreign keys', () => { - it('should work', () => { - expect( - transformConstraints(ctx, { - type: 'ConstraintAdd', - constraint: { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_test', - tableName: 'table1', - columnNames: ['parentId'], - referenceColumnNames: ['id'], - referenceTableName: 'table2', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual( - 'ALTER TABLE "table1" ADD CONSTRAINT "FK_test" FOREIGN KEY ("parentId") REFERENCES "table2" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION;', - ); - }); - }); - - describe('unique', () => { - it('should work', () => { - expect( - transformConstraints(ctx, { - type: 'ConstraintAdd', - constraint: { - type: ConstraintType.UNIQUE, - name: 'UQ_test', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('ALTER TABLE "table1" ADD CONSTRAINT "UQ_test" UNIQUE ("id");'); - }); - }); - - describe('check', () => { - it('should work', () => { - expect( - transformConstraints(ctx, { - type: 'ConstraintAdd', - constraint: { - type: ConstraintType.CHECK, - name: 'CHK_test', - tableName: 'table1', - expression: '"id" IS NOT NULL', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('ALTER TABLE "table1" ADD CONSTRAINT "CHK_test" CHECK ("id" IS NOT NULL);'); - }); - }); - }); - - describe('ConstraintDrop', () => { - it('should work', () => { - expect( - transformConstraints(ctx, { - type: 'ConstraintDrop', - tableName: 'table1', - constraintName: 'PK_test', - reason: 'unknown', - }), - ).toEqual(`ALTER TABLE "table1" DROP CONSTRAINT "PK_test";`); - }); - }); -}); diff --git a/server/src/sql-tools/transformers/constraint.transformer.ts b/server/src/sql-tools/transformers/constraint.transformer.ts deleted file mode 100644 index 94421e56fa..0000000000 --- a/server/src/sql-tools/transformers/constraint.transformer.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { asColumnList } from 'src/sql-tools/helpers'; -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { ActionType, ConstraintType, DatabaseConstraint } from 'src/sql-tools/types'; - -export const transformConstraints: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'ConstraintAdd': { - return `ALTER TABLE "${item.constraint.tableName}" ADD ${asConstraintBody(item.constraint)};`; - } - - case 'ConstraintRename': { - return `ALTER TABLE "${item.tableName}" RENAME CONSTRAINT "${item.oldName}" TO "${item.newName}";`; - } - - case 'ConstraintDrop': { - return `ALTER TABLE "${item.tableName}" DROP CONSTRAINT "${item.constraintName}";`; - } - default: { - return false; - } - } -}; - -const withAction = (constraint: { onDelete?: ActionType; onUpdate?: ActionType }) => - ` ON UPDATE ${constraint.onUpdate ?? ActionType.NO_ACTION} ON DELETE ${constraint.onDelete ?? ActionType.NO_ACTION}`; - -export const asConstraintBody = (constraint: DatabaseConstraint): string => { - const base = `CONSTRAINT "${constraint.name}"`; - - switch (constraint.type) { - case ConstraintType.PRIMARY_KEY: { - const columnNames = asColumnList(constraint.columnNames); - return `${base} PRIMARY KEY (${columnNames})`; - } - - case ConstraintType.FOREIGN_KEY: { - const columnNames = asColumnList(constraint.columnNames); - const referenceColumnNames = asColumnList(constraint.referenceColumnNames); - return ( - `${base} FOREIGN KEY (${columnNames}) REFERENCES "${constraint.referenceTableName}" (${referenceColumnNames})` + - withAction(constraint) - ); - } - - case ConstraintType.UNIQUE: { - const columnNames = asColumnList(constraint.columnNames); - return `${base} UNIQUE (${columnNames})`; - } - - case ConstraintType.CHECK: { - return `${base} CHECK (${constraint.expression})`; - } - - default: { - throw new Error(`Unknown constraint type: ${(constraint as any).type}`); - } - } -}; diff --git a/server/src/sql-tools/transformers/enum.transformer.ts b/server/src/sql-tools/transformers/enum.transformer.ts deleted file mode 100644 index cd7bddc2d2..0000000000 --- a/server/src/sql-tools/transformers/enum.transformer.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { DatabaseEnum } from 'src/sql-tools/types'; - -export const transformEnums: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'EnumCreate': { - return asEnumCreate(item.enum); - } - - case 'EnumDrop': { - return asEnumDrop(item.enumName); - } - - default: { - return false; - } - } -}; - -const asEnumCreate = ({ name, values }: DatabaseEnum): string => { - return `CREATE TYPE "${name}" AS ENUM (${values.map((value) => `'${value}'`)});`; -}; - -const asEnumDrop = (enumName: string): string => { - return `DROP TYPE "${enumName}";`; -}; diff --git a/server/src/sql-tools/transformers/extension.transformer.spec.ts b/server/src/sql-tools/transformers/extension.transformer.spec.ts deleted file mode 100644 index 2ab0402875..0000000000 --- a/server/src/sql-tools/transformers/extension.transformer.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { transformExtensions } from 'src/sql-tools/transformers/extension.transformer'; -import { describe, expect, it } from 'vitest'; - -const ctx = new BaseContext({}); - -describe(transformExtensions.name, () => { - describe('ExtensionDrop', () => { - it('should work', () => { - expect( - transformExtensions(ctx, { - type: 'ExtensionDrop', - extensionName: 'cube', - reason: 'unknown', - }), - ).toEqual(`DROP EXTENSION "cube";`); - }); - }); - - describe('ExtensionCreate', () => { - it('should work', () => { - expect( - transformExtensions(ctx, { - type: 'ExtensionCreate', - extension: { - name: 'cube', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual(`CREATE EXTENSION IF NOT EXISTS "cube";`); - }); - }); -}); diff --git a/server/src/sql-tools/transformers/extension.transformer.ts b/server/src/sql-tools/transformers/extension.transformer.ts deleted file mode 100644 index 26e76c1157..0000000000 --- a/server/src/sql-tools/transformers/extension.transformer.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { DatabaseExtension } from 'src/sql-tools/types'; - -export const transformExtensions: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'ExtensionCreate': { - return asExtensionCreate(item.extension); - } - - case 'ExtensionDrop': { - return asExtensionDrop(item.extensionName); - } - - default: { - return false; - } - } -}; - -const asExtensionCreate = (extension: DatabaseExtension): string => { - return `CREATE EXTENSION IF NOT EXISTS "${extension.name}";`; -}; - -const asExtensionDrop = (extensionName: string): string => { - return `DROP EXTENSION "${extensionName}";`; -}; diff --git a/server/src/sql-tools/transformers/function.transformer.spec.ts b/server/src/sql-tools/transformers/function.transformer.spec.ts deleted file mode 100644 index 5b0ba71c7d..0000000000 --- a/server/src/sql-tools/transformers/function.transformer.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { transformFunctions } from 'src/sql-tools/transformers/function.transformer'; -import { describe, expect, it } from 'vitest'; - -const ctx = new BaseContext({}); - -describe(transformFunctions.name, () => { - describe('FunctionDrop', () => { - it('should work', () => { - expect( - transformFunctions(ctx, { - type: 'FunctionDrop', - functionName: 'test_func', - reason: 'unknown', - }), - ).toEqual(`DROP FUNCTION test_func;`); - }); - }); -}); diff --git a/server/src/sql-tools/transformers/function.transformer.ts b/server/src/sql-tools/transformers/function.transformer.ts deleted file mode 100644 index 42a56cbe13..0000000000 --- a/server/src/sql-tools/transformers/function.transformer.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { DatabaseFunction } from 'src/sql-tools/types'; - -export const transformFunctions: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'FunctionCreate': { - return asFunctionCreate(item.function); - } - - case 'FunctionDrop': { - return asFunctionDrop(item.functionName); - } - - default: { - return false; - } - } -}; - -export const asFunctionCreate = (func: DatabaseFunction): string => { - return func.expression; -}; - -const asFunctionDrop = (functionName: string): string => { - return `DROP FUNCTION ${functionName};`; -}; diff --git a/server/src/sql-tools/transformers/index.transformer.spec.ts b/server/src/sql-tools/transformers/index.transformer.spec.ts deleted file mode 100644 index c9656463bf..0000000000 --- a/server/src/sql-tools/transformers/index.transformer.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { transformIndexes } from 'src/sql-tools/transformers/index.transformer'; -import { describe, expect, it } from 'vitest'; - -const ctx = new BaseContext({}); - -describe(transformIndexes.name, () => { - describe('IndexCreate', () => { - it('should work', () => { - expect( - transformIndexes(ctx, { - type: 'IndexCreate', - index: { - name: 'IDX_test', - tableName: 'table1', - columnNames: ['column1'], - unique: false, - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('CREATE INDEX "IDX_test" ON "table1" ("column1");'); - }); - - it('should create an unique index', () => { - expect( - transformIndexes(ctx, { - type: 'IndexCreate', - index: { - name: 'IDX_test', - tableName: 'table1', - columnNames: ['column1'], - unique: true, - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('CREATE UNIQUE INDEX "IDX_test" ON "table1" ("column1");'); - }); - - it('should create an index with a custom expression', () => { - expect( - transformIndexes(ctx, { - type: 'IndexCreate', - index: { - name: 'IDX_test', - tableName: 'table1', - unique: false, - expression: '"id" IS NOT NULL', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('CREATE INDEX "IDX_test" ON "table1" ("id" IS NOT NULL);'); - }); - - it('should create an index with a where clause', () => { - expect( - transformIndexes(ctx, { - type: 'IndexCreate', - index: { - name: 'IDX_test', - tableName: 'table1', - columnNames: ['id'], - unique: false, - where: '("id" IS NOT NULL)', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('CREATE INDEX "IDX_test" ON "table1" ("id") WHERE ("id" IS NOT NULL);'); - }); - - it('should create an index with a custom expression', () => { - expect( - transformIndexes(ctx, { - type: 'IndexCreate', - index: { - name: 'IDX_test', - tableName: 'table1', - unique: false, - using: 'gin', - expression: '"id" IS NOT NULL', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('CREATE INDEX "IDX_test" ON "table1" USING gin ("id" IS NOT NULL);'); - }); - }); - - describe('IndexDrop', () => { - it('should work', () => { - expect( - transformIndexes(ctx, { - type: 'IndexDrop', - indexName: 'IDX_test', - reason: 'unknown', - }), - ).toEqual(`DROP INDEX "IDX_test";`); - }); - }); -}); diff --git a/server/src/sql-tools/transformers/index.transformer.ts b/server/src/sql-tools/transformers/index.transformer.ts deleted file mode 100644 index acd65140ee..0000000000 --- a/server/src/sql-tools/transformers/index.transformer.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { asColumnList } from 'src/sql-tools/helpers'; -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { DatabaseIndex } from 'src/sql-tools/types'; - -export const transformIndexes: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'IndexCreate': { - return asIndexCreate(item.index); - } - - case 'IndexRename': { - return `ALTER INDEX "${item.oldName}" RENAME TO "${item.newName}";`; - } - - case 'IndexDrop': { - return `DROP INDEX "${item.indexName}";`; - } - - default: { - return false; - } - } -}; - -export const asIndexCreate = (index: DatabaseIndex): string => { - let sql = `CREATE`; - - if (index.unique) { - sql += ' UNIQUE'; - } - - sql += ` INDEX "${index.name}" ON "${index.tableName}"`; - - if (index.columnNames) { - const columnNames = asColumnList(index.columnNames); - sql += ` (${columnNames})`; - } - - if (index.using && index.using !== 'btree') { - sql += ` USING ${index.using}`; - } - - if (index.expression) { - sql += ` (${index.expression})`; - } - - if (index.with) { - sql += ` WITH (${index.with})`; - } - - if (index.where) { - sql += ` WHERE ${index.where}`; - } - - return sql + ';'; -}; diff --git a/server/src/sql-tools/transformers/index.ts b/server/src/sql-tools/transformers/index.ts deleted file mode 100644 index 395d69f2e2..0000000000 --- a/server/src/sql-tools/transformers/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { transformColumns } from 'src/sql-tools/transformers/column.transformer'; -import { transformConstraints } from 'src/sql-tools/transformers/constraint.transformer'; -import { transformEnums } from 'src/sql-tools/transformers/enum.transformer'; -import { transformExtensions } from 'src/sql-tools/transformers/extension.transformer'; -import { transformFunctions } from 'src/sql-tools/transformers/function.transformer'; -import { transformIndexes } from 'src/sql-tools/transformers/index.transformer'; -import { transformOverrides } from 'src/sql-tools/transformers/override.transformer'; -import { transformParameters } from 'src/sql-tools/transformers/parameter.transformer'; -import { transformTables } from 'src/sql-tools/transformers/table.transformer'; -import { transformTriggers } from 'src/sql-tools/transformers/trigger.transformer'; -import { SqlTransformer } from 'src/sql-tools/transformers/types'; - -export const transformers: SqlTransformer[] = [ - transformColumns, - transformConstraints, - transformEnums, - transformExtensions, - transformFunctions, - transformIndexes, - transformParameters, - transformTables, - transformTriggers, - transformOverrides, -]; diff --git a/server/src/sql-tools/transformers/override.transformer.ts b/server/src/sql-tools/transformers/override.transformer.ts deleted file mode 100644 index 1e2e981128..0000000000 --- a/server/src/sql-tools/transformers/override.transformer.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { asJsonString } from 'src/sql-tools/helpers'; -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { DatabaseOverride } from 'src/sql-tools/types'; - -export const transformOverrides: SqlTransformer = (ctx, item) => { - const tableName = ctx.overrideTableName; - - switch (item.type) { - case 'OverrideCreate': { - return asOverrideCreate(tableName, item.override); - } - - case 'OverrideUpdate': { - return asOverrideUpdate(tableName, item.override); - } - - case 'OverrideDrop': { - return asOverrideDrop(tableName, item.overrideName); - } - - default: { - return false; - } - } -}; - -export const asOverrideCreate = (tableName: string, override: DatabaseOverride): string => { - return `INSERT INTO "${tableName}" ("name", "value") VALUES ('${override.name}', ${asJsonString(override.value)});`; -}; - -export const asOverrideUpdate = (tableName: string, override: DatabaseOverride): string => { - return `UPDATE "${tableName}" SET "value" = ${asJsonString(override.value)} WHERE "name" = '${override.name}';`; -}; - -export const asOverrideDrop = (tableName: string, overrideName: string): string => { - return `DELETE FROM "${tableName}" WHERE "name" = '${overrideName}';`; -}; diff --git a/server/src/sql-tools/transformers/parameter.transformer.ts b/server/src/sql-tools/transformers/parameter.transformer.ts deleted file mode 100644 index d23472f991..0000000000 --- a/server/src/sql-tools/transformers/parameter.transformer.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { DatabaseParameter } from 'src/sql-tools/types'; - -export const transformParameters: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'ParameterSet': { - return asParameterSet(item.parameter); - } - - case 'ParameterReset': { - return asParameterReset(item.databaseName, item.parameterName); - } - - default: { - return false; - } - } -}; - -const asParameterSet = (parameter: DatabaseParameter): string => { - let sql = ''; - if (parameter.scope === 'database') { - sql += `ALTER DATABASE "${parameter.databaseName}" `; - } - - sql += `SET ${parameter.name} TO ${parameter.value}`; - - return sql; -}; - -const asParameterReset = (databaseName: string, parameterName: string): string => { - return `ALTER DATABASE "${databaseName}" RESET "${parameterName}"`; -}; diff --git a/server/src/sql-tools/transformers/table.transformer.spec.ts b/server/src/sql-tools/transformers/table.transformer.spec.ts deleted file mode 100644 index 0d89fcd278..0000000000 --- a/server/src/sql-tools/transformers/table.transformer.spec.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { transformTables } from 'src/sql-tools/transformers/table.transformer'; -import { ConstraintType, DatabaseTable } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const ctx = new BaseContext({}); - -const table1: DatabaseTable = { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - primary: true, - type: 'character varying', - nullable: true, - isArray: false, - synchronize: true, - }, - { - name: 'column2', - primary: false, - tableName: 'table1', - type: 'character varying', - nullable: true, - isArray: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'index1', - tableName: 'table1', - columnNames: ['column2'], - unique: false, - synchronize: true, - }, - ], - constraints: [ - { - name: 'constraint1', - tableName: 'table1', - columnNames: ['column1'], - type: ConstraintType.PRIMARY_KEY, - synchronize: true, - }, - { - name: 'constraint2', - tableName: 'table1', - columnNames: ['column1'], - type: ConstraintType.FOREIGN_KEY, - referenceTableName: 'table2', - referenceColumnNames: ['parentId'], - synchronize: true, - }, - { - name: 'constraint3', - tableName: 'table1', - columnNames: ['column1'], - type: ConstraintType.UNIQUE, - synchronize: true, - }, - ], - triggers: [], - synchronize: true, -}; - -describe(transformTables.name, () => { - describe('TableDrop', () => { - it('should work', () => { - expect( - transformTables(ctx, { - type: 'TableDrop', - tableName: 'table1', - reason: 'unknown', - }), - ).toEqual(`DROP TABLE "table1";`); - }); - }); - - describe('TableCreate', () => { - it('should work', () => { - expect( - transformTables(ctx, { - type: 'TableCreate', - table: table1, - reason: 'unknown', - }), - ).toEqual([ - `CREATE TABLE "table1" ( - "column1" character varying, - "column2" character varying, - CONSTRAINT "constraint1" PRIMARY KEY ("column1"), - CONSTRAINT "constraint2" FOREIGN KEY ("column1") REFERENCES "table2" ("parentId") ON UPDATE NO ACTION ON DELETE NO ACTION, - CONSTRAINT "constraint3" UNIQUE ("column1") -);`, - `CREATE INDEX "index1" ON "table1" ("column2");`, - ]); - }); - - it('should handle a non-nullable column', () => { - expect( - transformTables(ctx, { - type: 'TableCreate', - table: { - name: 'table1', - columns: [ - { - tableName: 'table1', - primary: false, - name: 'column1', - type: 'character varying', - isArray: false, - nullable: false, - synchronize: true, - }, - ], - indexes: [], - constraints: [], - triggers: [], - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual([ - `CREATE TABLE "table1" ( - "column1" character varying NOT NULL -);`, - ]); - }); - - it('should handle a default value', () => { - expect( - transformTables(ctx, { - type: 'TableCreate', - table: { - name: 'table1', - columns: [ - { - tableName: 'table1', - name: 'column1', - primary: false, - type: 'character varying', - isArray: false, - nullable: true, - default: 'uuid_generate_v4()', - synchronize: true, - }, - ], - indexes: [], - constraints: [], - triggers: [], - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual([ - `CREATE TABLE "table1" ( - "column1" character varying DEFAULT uuid_generate_v4() -);`, - ]); - }); - - it('should handle a string with a fixed length', () => { - expect( - transformTables(ctx, { - type: 'TableCreate', - table: { - name: 'table1', - columns: [ - { - tableName: 'table1', - primary: false, - name: 'column1', - type: 'character varying', - length: 2, - isArray: false, - nullable: true, - synchronize: true, - }, - ], - indexes: [], - constraints: [], - triggers: [], - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual([ - `CREATE TABLE "table1" ( - "column1" character varying(2) -);`, - ]); - }); - - it('should handle an array type', () => { - expect( - transformTables(ctx, { - type: 'TableCreate', - table: { - name: 'table1', - columns: [ - { - tableName: 'table1', - primary: false, - name: 'column1', - type: 'character varying', - isArray: true, - nullable: true, - synchronize: true, - }, - ], - indexes: [], - constraints: [], - triggers: [], - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual([ - `CREATE TABLE "table1" ( - "column1" character varying[] -);`, - ]); - }); - }); -}); diff --git a/server/src/sql-tools/transformers/table.transformer.ts b/server/src/sql-tools/transformers/table.transformer.ts deleted file mode 100644 index a81bfc25aa..0000000000 --- a/server/src/sql-tools/transformers/table.transformer.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { asColumnComment, getColumnModifiers, getColumnType } from 'src/sql-tools/helpers'; -import { asColumnAlter } from 'src/sql-tools/transformers/column.transformer'; -import { asConstraintBody } from 'src/sql-tools/transformers/constraint.transformer'; -import { asIndexCreate } from 'src/sql-tools/transformers/index.transformer'; -import { asTriggerCreate } from 'src/sql-tools/transformers/trigger.transformer'; -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { DatabaseTable } from 'src/sql-tools/types'; - -export const transformTables: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'TableCreate': { - return asTableCreate(item.table); - } - - case 'TableDrop': { - return asTableDrop(item.tableName); - } - - default: { - return false; - } - } -}; - -const asTableCreate = (table: DatabaseTable) => { - const tableName = table.name; - - const items: string[] = []; - for (const column of table.columns) { - items.push(`"${column.name}" ${getColumnType(column)}${getColumnModifiers(column)}`); - } - - for (const constraint of table.constraints) { - items.push(asConstraintBody(constraint)); - } - - const sql = [`CREATE TABLE "${tableName}" (\n ${items.join(',\n ')}\n);`]; - - for (const column of table.columns) { - if (column.comment) { - sql.push(asColumnComment(tableName, column.name, column.comment)); - } - - if (column.storage) { - sql.push(...asColumnAlter(tableName, column.name, { storage: column.storage })); - } - } - - for (const index of table.indexes) { - sql.push(asIndexCreate(index)); - } - - for (const trigger of table.triggers) { - sql.push(asTriggerCreate(trigger)); - } - - return sql; -}; - -const asTableDrop = (tableName: string) => { - return `DROP TABLE "${tableName}";`; -}; diff --git a/server/src/sql-tools/transformers/trigger.transformer.spec.ts b/server/src/sql-tools/transformers/trigger.transformer.spec.ts deleted file mode 100644 index f6ba889c29..0000000000 --- a/server/src/sql-tools/transformers/trigger.transformer.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { transformTriggers } from 'src/sql-tools/transformers/trigger.transformer'; -import { describe, expect, it } from 'vitest'; - -const ctx = new BaseContext({}); - -describe(transformTriggers.name, () => { - describe('TriggerCreate', () => { - it('should work', () => { - expect( - transformTriggers(ctx, { - type: 'TriggerCreate', - trigger: { - name: 'trigger1', - tableName: 'table1', - timing: 'before', - actions: ['update'], - scope: 'row', - functionName: 'function1', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual( - `CREATE OR REPLACE TRIGGER "trigger1" - BEFORE UPDATE ON "table1" - FOR EACH ROW - EXECUTE FUNCTION function1();`, - ); - }); - - it('should work with multiple actions', () => { - expect( - transformTriggers(ctx, { - type: 'TriggerCreate', - trigger: { - name: 'trigger1', - tableName: 'table1', - timing: 'before', - actions: ['update', 'delete'], - scope: 'row', - functionName: 'function1', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual( - `CREATE OR REPLACE TRIGGER "trigger1" - BEFORE UPDATE OR DELETE ON "table1" - FOR EACH ROW - EXECUTE FUNCTION function1();`, - ); - }); - - it('should work with old/new reference table aliases', () => { - expect( - transformTriggers(ctx, { - type: 'TriggerCreate', - trigger: { - name: 'trigger1', - tableName: 'table1', - timing: 'before', - actions: ['update'], - referencingNewTableAs: 'new', - referencingOldTableAs: 'old', - scope: 'row', - functionName: 'function1', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual( - `CREATE OR REPLACE TRIGGER "trigger1" - BEFORE UPDATE ON "table1" - REFERENCING OLD TABLE AS "old" NEW TABLE AS "new" - FOR EACH ROW - EXECUTE FUNCTION function1();`, - ); - }); - }); - - describe('TriggerDrop', () => { - it('should work', () => { - expect( - transformTriggers(ctx, { - type: 'TriggerDrop', - tableName: 'table1', - triggerName: 'trigger1', - reason: 'unknown', - }), - ).toEqual(`DROP TRIGGER "trigger1" ON "table1";`); - }); - }); -}); diff --git a/server/src/sql-tools/transformers/trigger.transformer.ts b/server/src/sql-tools/transformers/trigger.transformer.ts deleted file mode 100644 index fca557abfc..0000000000 --- a/server/src/sql-tools/transformers/trigger.transformer.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { DatabaseTrigger } from 'src/sql-tools/types'; - -export const transformTriggers: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'TriggerCreate': { - return asTriggerCreate(item.trigger); - } - - case 'TriggerDrop': { - return asTriggerDrop(item.tableName, item.triggerName); - } - - default: { - return false; - } - } -}; - -export const asTriggerCreate = (trigger: DatabaseTrigger): string => { - const sql: string[] = [ - `CREATE OR REPLACE TRIGGER "${trigger.name}"`, - `${trigger.timing.toUpperCase()} ${trigger.actions.map((action) => action.toUpperCase()).join(' OR ')} ON "${trigger.tableName}"`, - ]; - - if (trigger.referencingOldTableAs || trigger.referencingNewTableAs) { - let statement = `REFERENCING`; - if (trigger.referencingOldTableAs) { - statement += ` OLD TABLE AS "${trigger.referencingOldTableAs}"`; - } - if (trigger.referencingNewTableAs) { - statement += ` NEW TABLE AS "${trigger.referencingNewTableAs}"`; - } - sql.push(statement); - } - - if (trigger.scope) { - sql.push(`FOR EACH ${trigger.scope.toUpperCase()}`); - } - - if (trigger.when) { - sql.push(`WHEN (${trigger.when})`); - } - - sql.push(`EXECUTE FUNCTION ${trigger.functionName}();`); - - return sql.join('\n '); -}; - -export const asTriggerDrop = (tableName: string, triggerName: string): string => { - return `DROP TRIGGER "${triggerName}" ON "${tableName}";`; -}; diff --git a/server/src/sql-tools/transformers/types.ts b/server/src/sql-tools/transformers/types.ts deleted file mode 100644 index 96cbe4d918..0000000000 --- a/server/src/sql-tools/transformers/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { SchemaDiff } from 'src/sql-tools/types'; - -export type SqlTransformer = (ctx: BaseContext, item: SchemaDiff) => string | string[] | false; diff --git a/server/src/sql-tools/types.ts b/server/src/sql-tools/types.ts deleted file mode 100644 index 9d93a79ff1..0000000000 --- a/server/src/sql-tools/types.ts +++ /dev/null @@ -1,538 +0,0 @@ -import { Kysely, ColumnType as KyselyColumnType } from 'kysely'; -import { ProcessorContext } from 'src/sql-tools/contexts/processor-context'; -import { ReaderContext } from 'src/sql-tools/contexts/reader-context'; -import { NamingInterface } from 'src/sql-tools/naming/naming.interface'; -import { RegisterItem } from 'src/sql-tools/register-item'; - -export type BaseContextOptions = { - databaseName?: string; - schemaName?: string; - overrideTableName?: string; - namingStrategy?: 'default' | 'hash' | NamingInterface; -}; - -export type SchemaFromCodeOptions = BaseContextOptions & { - /** automatically create indexes on foreign key columns */ - createForeignKeyIndexes?: boolean; - reset?: boolean; - - functions?: boolean; - extensions?: boolean; - parameters?: boolean; - overrides?: boolean; -}; - -export type SchemaFromDatabaseOptions = BaseContextOptions; - -export type SchemaDiffToSqlOptions = BaseContextOptions & { - comments?: boolean; -}; - -export type SchemaDiffOptions = BaseContextOptions & { - tables?: IgnoreOptions; - columns?: IgnoreOptions; - indexes?: IgnoreOptions; - triggers?: IgnoreOptions; - constraints?: IgnoreOptions; - functions?: IgnoreOptions; - enums?: IgnoreOptions; - extensions?: IgnoreOptions; - parameters?: IgnoreOptions; - overrides?: IgnoreOptions; -}; - -export type IgnoreOptions = - | boolean - | { - ignoreExtra?: boolean; - ignoreMissing?: boolean; - }; - -export type Processor = (ctx: ProcessorContext, items: RegisterItem[]) => void; -export type Reader = (ctx: ReaderContext, db: DatabaseClient) => Promise; - -export type PostgresDB = { - pg_am: { - oid: number; - amname: string; - amhandler: string; - amtype: string; - }; - - pg_attribute: { - attrelid: number; - attname: string; - attnum: number; - atttypeid: number; - attstattarget: number; - attstatarget: number; - aanum: number; - }; - - pg_class: { - oid: number; - relname: string; - relkind: string; - relnamespace: string; - reltype: string; - relowner: string; - relam: string; - relfilenode: string; - reltablespace: string; - relpages: number; - reltuples: number; - relallvisible: number; - reltoastrelid: string; - relhasindex: PostgresYesOrNo; - relisshared: PostgresYesOrNo; - relpersistence: string; - }; - - pg_constraint: { - oid: number; - conname: string; - conrelid: string; - contype: string; - connamespace: string; - conkey: number[]; - confkey: number[]; - confrelid: string; - confupdtype: string; - confdeltype: string; - confmatchtype: number; - condeferrable: PostgresYesOrNo; - condeferred: PostgresYesOrNo; - convalidated: PostgresYesOrNo; - conindid: number; - }; - - pg_description: { - objoid: string; - classoid: string; - objsubid: number; - description: string; - }; - - pg_trigger: { - oid: string; - tgisinternal: boolean; - tginitdeferred: boolean; - tgdeferrable: boolean; - tgrelid: string; - tgfoid: string; - tgname: string; - tgenabled: string; - tgtype: number; - tgconstraint: string; - tgdeferred: boolean; - tgargs: Buffer; - tgoldtable: string; - tgnewtable: string; - tgqual: string; - }; - - 'pg_catalog.pg_extension': { - oid: string; - extname: string; - extowner: string; - extnamespace: string; - extrelocatable: boolean; - extversion: string; - extconfig: string[]; - extcondition: string[]; - }; - - pg_enum: { - oid: string; - enumtypid: string; - enumsortorder: number; - enumlabel: string; - }; - - pg_index: { - indexrelid: string; - indrelid: string; - indisready: boolean; - indexprs: string | null; - indpred: string | null; - indkey: number[]; - indisprimary: boolean; - indisunique: boolean; - }; - - pg_indexes: { - schemaname: string; - tablename: string; - indexname: string; - tablespace: string | null; - indexrelid: string; - indexdef: string; - }; - - pg_namespace: { - oid: number; - nspname: string; - nspowner: number; - nspacl: string[]; - }; - - pg_type: { - oid: string; - typname: string; - typnamespace: string; - typowner: string; - typtype: string; - typcategory: string; - typarray: string; - }; - - pg_depend: { - objid: string; - deptype: string; - }; - - pg_proc: { - oid: string; - proname: string; - pronamespace: string; - prokind: string; - }; - - pg_settings: { - name: string; - setting: string; - unit: string | null; - category: string; - short_desc: string | null; - extra_desc: string | null; - context: string; - vartype: string; - source: string; - min_val: string | null; - max_val: string | null; - enumvals: string[] | null; - boot_val: string | null; - reset_val: string | null; - sourcefile: string | null; - sourceline: number | null; - pending_restart: PostgresYesOrNo; - }; - - 'information_schema.tables': { - table_catalog: string; - table_schema: string; - table_name: string; - table_type: 'VIEW' | 'BASE TABLE' | string; - is_insertable_info: PostgresYesOrNo; - is_typed: PostgresYesOrNo; - commit_action: string | null; - }; - - 'information_schema.columns': { - table_catalog: string; - table_schema: string; - table_name: string; - column_name: string; - ordinal_position: number; - column_default: string | null; - is_nullable: PostgresYesOrNo; - data_type: string; - dtd_identifier: string; - character_maximum_length: number | null; - character_octet_length: number | null; - numeric_precision: number | null; - numeric_precision_radix: number | null; - numeric_scale: number | null; - datetime_precision: number | null; - interval_type: string | null; - interval_precision: number | null; - udt_catalog: string; - udt_schema: string; - udt_name: string; - maximum_cardinality: number | null; - is_updatable: PostgresYesOrNo; - }; - - 'information_schema.element_types': { - object_catalog: string; - object_schema: string; - object_name: string; - object_type: string; - collection_type_identifier: string; - data_type: string; - }; - - 'information_schema.routines': { - specific_catalog: string; - specific_schema: string; - specific_name: string; - routine_catalog: string; - routine_schema: string; - routine_name: string; - routine_type: string; - data_type: string; - type_udt_catalog: string; - type_udt_schema: string; - type_udt_name: string; - dtd_identifier: string; - routine_body: string; - routine_definition: string; - external_name: string; - external_language: string; - is_deterministic: PostgresYesOrNo; - security_type: string; - }; -}; - -type PostgresYesOrNo = 'YES' | 'NO'; - -export type DatabaseClient = Kysely; - -export enum ConstraintType { - PRIMARY_KEY = 'primary-key', - FOREIGN_KEY = 'foreign-key', - UNIQUE = 'unique', - CHECK = 'check', -} - -export enum ActionType { - NO_ACTION = 'NO ACTION', - RESTRICT = 'RESTRICT', - CASCADE = 'CASCADE', - SET_NULL = 'SET NULL', - SET_DEFAULT = 'SET DEFAULT', -} - -export type ColumnStorage = 'default' | 'external' | 'extended' | 'main'; - -export type ColumnType = - | 'bigint' - | 'boolean' - | 'bytea' - | 'character' - | 'character varying' - | 'date' - | 'double precision' - | 'integer' - | 'jsonb' - | 'polygon' - | 'text' - | 'time' - | 'time with time zone' - | 'time without time zone' - | 'timestamp' - | 'timestamp with time zone' - | 'timestamp without time zone' - | 'uuid' - | 'vector' - | 'enum' - | 'serial' - | 'real'; - -export type DatabaseSchema = { - databaseName: string; - schemaName: string; - functions: DatabaseFunction[]; - enums: DatabaseEnum[]; - tables: DatabaseTable[]; - extensions: DatabaseExtension[]; - parameters: DatabaseParameter[]; - overrides: DatabaseOverride[]; - warnings: string[]; -}; - -export type DatabaseParameter = { - name: string; - databaseName: string; - value: string | number | null | undefined; - scope: ParameterScope; - synchronize: boolean; -}; - -export type ParameterScope = 'database' | 'user'; - -export type DatabaseOverride = { - name: string; - value: { name: string; type: OverrideType; sql: string }; - synchronize: boolean; -}; - -export type OverrideType = 'function' | 'index' | 'trigger'; - -export type DatabaseEnum = { - name: string; - values: string[]; - synchronize: boolean; -}; - -export type DatabaseFunction = { - name: string; - expression: string; - synchronize: boolean; - override?: DatabaseOverride; -}; - -export type DatabaseExtension = { - name: string; - synchronize: boolean; -}; - -export type DatabaseTable = { - name: string; - columns: DatabaseColumn[]; - indexes: DatabaseIndex[]; - constraints: DatabaseConstraint[]; - triggers: DatabaseTrigger[]; - synchronize: boolean; -}; - -export type DatabaseConstraint = - | DatabasePrimaryKeyConstraint - | DatabaseForeignKeyConstraint - | DatabaseUniqueConstraint - | DatabaseCheckConstraint; - -export type DatabaseColumn = { - primary: boolean; - name: string; - tableName: string; - comment?: string; - - type: ColumnType; - nullable: boolean; - isArray: boolean; - synchronize: boolean; - - default?: string; - length?: number; - storage?: ColumnStorage; - identity?: boolean; - - // enum values - enumName?: string; - - // numeric types - numericPrecision?: number; - numericScale?: number; -}; - -export type ColumnChanges = { - nullable?: boolean; - default?: string; - comment?: string; - storage?: ColumnStorage; -}; - -type ColumBasedConstraint = { - name: string; - tableName: string; - columnNames: string[]; -}; - -export type DatabasePrimaryKeyConstraint = ColumBasedConstraint & { - type: ConstraintType.PRIMARY_KEY; - synchronize: boolean; -}; - -export type DatabaseUniqueConstraint = ColumBasedConstraint & { - type: ConstraintType.UNIQUE; - synchronize: boolean; -}; - -export type DatabaseForeignKeyConstraint = ColumBasedConstraint & { - type: ConstraintType.FOREIGN_KEY; - referenceTableName: string; - referenceColumnNames: string[]; - onUpdate?: ActionType; - onDelete?: ActionType; - synchronize: boolean; -}; - -export type DatabaseCheckConstraint = { - type: ConstraintType.CHECK; - name: string; - tableName: string; - expression: string; - synchronize: boolean; -}; - -export type DatabaseTrigger = { - name: string; - tableName: string; - timing: TriggerTiming; - actions: TriggerAction[]; - scope: TriggerScope; - referencingNewTableAs?: string; - referencingOldTableAs?: string; - when?: string; - functionName: string; - override?: DatabaseOverride; - synchronize: boolean; -}; -export type TriggerTiming = 'before' | 'after' | 'instead of'; -export type TriggerAction = 'insert' | 'update' | 'delete' | 'truncate'; -export type TriggerScope = 'row' | 'statement'; - -export type DatabaseIndex = { - name: string; - tableName: string; - columnNames?: string[]; - expression?: string; - unique: boolean; - using?: string; - with?: string; - where?: string; - override?: DatabaseOverride; - synchronize: boolean; -}; - -export type SchemaDiff = { reason: string } & ( - | { type: 'ExtensionCreate'; extension: DatabaseExtension } - | { type: 'ExtensionDrop'; extensionName: string } - | { type: 'FunctionCreate'; function: DatabaseFunction } - | { type: 'FunctionDrop'; functionName: string } - | { type: 'TableCreate'; table: DatabaseTable } - | { type: 'TableDrop'; tableName: string } - | { type: 'ColumnAdd'; column: DatabaseColumn } - | { type: 'ColumnRename'; tableName: string; oldName: string; newName: string } - | { type: 'ColumnAlter'; tableName: string; columnName: string; changes: ColumnChanges } - | { type: 'ColumnDrop'; tableName: string; columnName: string } - | { type: 'ConstraintAdd'; constraint: DatabaseConstraint } - | { type: 'ConstraintRename'; tableName: string; oldName: string; newName: string } - | { type: 'ConstraintDrop'; tableName: string; constraintName: string } - | { type: 'IndexCreate'; index: DatabaseIndex } - | { type: 'IndexRename'; tableName: string; oldName: string; newName: string } - | { type: 'IndexDrop'; indexName: string } - | { type: 'TriggerCreate'; trigger: DatabaseTrigger } - | { type: 'TriggerDrop'; tableName: string; triggerName: string } - | { type: 'ParameterSet'; parameter: DatabaseParameter } - | { type: 'ParameterReset'; databaseName: string; parameterName: string } - | { type: 'EnumCreate'; enum: DatabaseEnum } - | { type: 'EnumDrop'; enumName: string } - | { type: 'OverrideCreate'; override: DatabaseOverride } - | { type: 'OverrideUpdate'; override: DatabaseOverride } - | { type: 'OverrideDrop'; overrideName: string } -); - -export type CompareFunction = (source: T, target: T) => SchemaDiff[]; -export type Comparer = { - onMissing: (source: T) => SchemaDiff[]; - onExtra: (target: T) => SchemaDiff[]; - onCompare: CompareFunction; - /** if two items have the same key, they are considered identical and can be renamed via `onRename` */ - getRenameKey?: (item: T) => string; - onRename?: (source: T, target: T) => SchemaDiff[]; -}; - -export enum Reason { - MissingInSource = 'missing in source', - MissingInTarget = 'missing in target', - Rename = 'name has changed', -} - -export type Timestamp = KyselyColumnType; -export type Generated = - T extends KyselyColumnType - ? KyselyColumnType - : KyselyColumnType; -export type Int8 = KyselyColumnType; diff --git a/server/src/types.ts b/server/src/types.ts index 543add2461..886f2d9254 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -8,7 +8,6 @@ import { SetMaintenanceModeDto } from 'src/dtos/maintenance.dto'; import { AssetOrder, AssetType, - DatabaseSslMode, ExifOrientation, ImageFormat, IntegrityReportType, @@ -443,23 +442,6 @@ export type JobItem = export type VectorExtension = (typeof VECTOR_EXTENSIONS)[number]; -export type DatabaseConnectionURL = { - connectionType: 'url'; - url: string; -}; - -export type DatabaseConnectionParts = { - connectionType: 'parts'; - host: string; - port: number; - username: string; - password: string; - database: string; - ssl?: DatabaseSslMode; -}; - -export type DatabaseConnectionParams = DatabaseConnectionURL | DatabaseConnectionParts; - export interface ExtensionVersion { name: VectorExtension; availableVersion: string | null; diff --git a/server/src/utils/database.spec.ts b/server/src/utils/database.spec.ts deleted file mode 100644 index 4c6a82ad8f..0000000000 --- a/server/src/utils/database.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { asPostgresConnectionConfig } from 'src/utils/database'; - -describe('database utils', () => { - describe('asPostgresConnectionConfig', () => { - it('should handle sslmode=require', () => { - expect( - asPostgresConnectionConfig({ - connectionType: 'url', - url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=require', - }), - ).toMatchObject({ ssl: {} }); - }); - - it('should handle sslmode=prefer', () => { - expect( - asPostgresConnectionConfig({ - connectionType: 'url', - url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=prefer', - }), - ).toMatchObject({ ssl: {} }); - }); - - it('should handle sslmode=verify-ca', () => { - expect( - asPostgresConnectionConfig({ - connectionType: 'url', - url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-ca', - }), - ).toMatchObject({ ssl: {} }); - }); - - it('should handle sslmode=verify-full', () => { - expect( - asPostgresConnectionConfig({ - connectionType: 'url', - url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-full', - }), - ).toMatchObject({ ssl: {} }); - }); - - it('should handle sslmode=no-verify', () => { - expect( - asPostgresConnectionConfig({ - connectionType: 'url', - url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=no-verify', - }), - ).toMatchObject({ ssl: { rejectUnauthorized: false } }); - }); - - it('should handle ssl=true', () => { - expect( - asPostgresConnectionConfig({ - connectionType: 'url', - url: 'postgres://postgres1:postgres2@database1:54320/immich?ssl=true', - }), - ).toMatchObject({ ssl: true }); - }); - - it('should reject invalid ssl', () => { - expect(() => - asPostgresConnectionConfig({ - connectionType: 'url', - url: 'postgres://postgres1:postgres2@database1:54320/immich?ssl=invalid', - }), - ).toThrowError('Invalid ssl option'); - }); - - it('should handle socket: URLs', () => { - expect( - asPostgresConnectionConfig({ connectionType: 'url', url: 'socket:/run/postgresql?db=database1' }), - ).toMatchObject({ host: '/run/postgresql', database: 'database1' }); - }); - - it('should handle sockets in postgres: URLs', () => { - expect( - asPostgresConnectionConfig({ connectionType: 'url', url: 'postgres:///database2?host=/path/to/socket' }), - ).toMatchObject({ - host: '/path/to/socket', - database: 'database2', - }); - }); - }); -}); diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 9ae15fd7d5..4dd0c9b302 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -1,3 +1,4 @@ +import { createPostgres, DatabaseConnectionParams } from '@immich/sql-tools'; import { AliasedRawBuilder, DeduplicateJoinsPlugin, @@ -14,90 +15,24 @@ import { } from 'kysely'; import { PostgresJSDialect } from 'kysely-postgres-js'; import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'; -import { parse } from 'pg-connection-string'; -import postgres, { Notice, PostgresError } from 'postgres'; +import { Notice, PostgresError } from 'postgres'; import { columns, Exif, lockableProperties, LockableProperty, Person } from 'src/database'; import { AssetEditActionItem } from 'src/dtos/editing.dto'; -import { AssetFileType, AssetVisibility, DatabaseExtension, DatabaseSslMode } from 'src/enum'; +import { AssetFileType, AssetVisibility, DatabaseExtension } from 'src/enum'; import { AssetSearchBuilderOptions } from 'src/repositories/search.repository'; import { DB } from 'src/schema'; -import { DatabaseConnectionParams, VectorExtension } from 'src/types'; - -type Ssl = 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object; - -const isValidSsl = (ssl?: string | boolean | object): ssl is Ssl => - typeof ssl !== 'string' || ssl === 'require' || ssl === 'allow' || ssl === 'prefer' || ssl === 'verify-full'; - -export const asPostgresConnectionConfig = (params: DatabaseConnectionParams) => { - if (params.connectionType === 'parts') { - return { - host: params.host, - port: params.port, - username: params.username, - password: params.password, - database: params.database, - ssl: params.ssl === DatabaseSslMode.Disable ? false : params.ssl, - }; - } - - const { host, port, user, password, database, ...rest } = parse(params.url); - let ssl: Ssl | undefined; - if (rest.ssl) { - if (!isValidSsl(rest.ssl)) { - throw new Error(`Invalid ssl option: ${rest.ssl}`); - } - ssl = rest.ssl; - } - - return { - host: host ?? undefined, - port: port ? Number(port) : undefined, - username: user, - password, - database: database ?? undefined, - ssl, - }; -}; - -export const getKyselyConfig = ( - params: DatabaseConnectionParams, - options: Partial>> = {}, -): KyselyConfig => { - const config = asPostgresConnectionConfig(params); +import { VectorExtension } from 'src/types'; +export const getKyselyConfig = (connection: DatabaseConnectionParams): KyselyConfig => { return { dialect: new PostgresJSDialect({ - postgres: postgres({ - onnotice: (notice: Notice) => { + postgres: createPostgres({ + connection, + onNotice: (notice: Notice) => { if (notice['severity'] !== 'NOTICE') { console.warn('Postgres notice:', notice); } }, - max: 10, - types: { - date: { - to: 1184, - from: [1082, 1114, 1184], - serialize: (x: Date | string) => (x instanceof Date ? x.toISOString() : x), - parse: (x: string) => new Date(x), - }, - bigint: { - to: 20, - from: [20, 1700], - parse: (value: string) => Number.parseInt(value), - serialize: (value: number) => value.toString(), - }, - }, - connection: { - TimeZone: 'UTC', - }, - host: config.host, - port: config.port, - username: config.username, - password: config.password, - database: config.database, - ssl: config.ssl, - ...options, }), }), log(event) { diff --git a/server/src/utils/mime-types.spec.ts b/server/src/utils/mime-types.spec.ts index b0e31afe39..862ed310bc 100644 --- a/server/src/utils/mime-types.spec.ts +++ b/server/src/utils/mime-types.spec.ts @@ -153,6 +153,33 @@ describe('mimeTypes', () => { } }); + describe('canBeTransparent', () => { + for (const img of [ + 'a.avif', + 'a.bmp', + 'a.gif', + 'a.heic', + 'a.heif', + 'a.hif', + 'a.jxl', + 'a.png', + 'a.svg', + 'a.tif', + 'a.tiff', + 'a.webp', + ]) { + it(`should return true for ${img}`, () => { + expect(mimeTypes.canBeTransparent(img)).toBe(true); + }); + } + + for (const img of ['a.jpg', 'a.jpeg', 'a.jpe', 'a.insp', 'a.jp2', 'a.cr3', 'a.dng', 'a.nef', 'a.arw']) { + it(`should return false for ${img}`, () => { + expect(mimeTypes.canBeTransparent(img)).toBe(false); + }); + } + }); + describe('animated image', () => { for (const img of ['a.avif', 'a.gif', 'a.webp']) { it('should identify animated image mime types as such', () => { diff --git a/server/src/utils/mime-types.ts b/server/src/utils/mime-types.ts index 4e91bbd7f1..43421e7937 100644 --- a/server/src/utils/mime-types.ts +++ b/server/src/utils/mime-types.ts @@ -77,6 +77,21 @@ const extensionOverrides: Record = { 'image/jpeg': '.jpg', }; +const transparentCapableExtensions = new Set([ + '.avif', + '.bmp', + '.gif', + '.heic', + '.heif', + '.hif', + '.jxl', + '.png', + '.svg', + '.tif', + '.tiff', + '.webp', +]); + const profileExtensions = new Set(['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp', '.svg']); const profile: Record = Object.fromEntries( Object.entries(image).filter(([key]) => profileExtensions.has(key)), @@ -134,6 +149,7 @@ export const mimeTypes = { isProfile: (filename: string) => isType(filename, profile), isSidecar: (filename: string) => isType(filename, sidecar), isVideo: (filename: string) => isType(filename, video), + canBeTransparent: (filename: string) => transparentCapableExtensions.has(extname(filename).toLowerCase()), isRaw: (filename: string) => isType(filename, raw), lookup, /** return an extension (including a leading `.`) for a mime-type */ diff --git a/server/src/validation.ts b/server/src/validation.ts index ed97f648b1..188f4d72f9 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -247,7 +247,7 @@ export const ValidateHexColor = () => { }; type DateOptions = OptionalOptions & { optional?: boolean; format?: 'date' | 'date-time' }; -export const ValidateDate = (options?: DateOptions & ApiPropertyOptions) => { +export const ValidateDate = (options?: DateOptions & PropertyOptions) => { const { optional, nullable = false, @@ -257,7 +257,7 @@ export const ValidateDate = (options?: DateOptions & ApiPropertyOptions) => { } = options || {}; return applyDecorators( - ApiProperty({ format, ...apiPropertyOptions }), + Property({ format, ...apiPropertyOptions }), IsDate(), optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(), Transform(({ key, value }) => { diff --git a/server/test/factories/asset-edit.factory.ts b/server/test/factories/asset-edit.factory.ts index e16b0c2e4b..ba15da4b69 100644 --- a/server/test/factories/asset-edit.factory.ts +++ b/server/test/factories/asset-edit.factory.ts @@ -1,5 +1,5 @@ import { Selectable } from 'kysely'; -import { AssetEditAction } from 'src/dtos/editing.dto'; +import { AssetEditAction, AssetEditActionItem } from 'src/dtos/editing.dto'; import { AssetEditTable } from 'src/schema/tables/asset-edit.table'; import { AssetFactory } from 'test/factories/asset.factory'; import { build } from 'test/factories/builder.factory'; @@ -33,6 +33,6 @@ export class AssetEditFactory { } build() { - return { ...this.value } as Selectable>; + return { ...this.value } as Omit, 'action' | 'parameters'> & AssetEditActionItem; } } diff --git a/server/test/factories/asset-file.factory.ts b/server/test/factories/asset-file.factory.ts index 109cd5adc4..511ab45bb7 100644 --- a/server/test/factories/asset-file.factory.ts +++ b/server/test/factories/asset-file.factory.ts @@ -26,6 +26,7 @@ export class AssetFileFactory { path: `/data/12/34/thumbs/${id.slice(0, 2)}/${id.slice(2, 4)}/${id}${isEdited ? '_edited' : ''}.jpg`, updateId: newUuidV7(), isProgressive: false, + isTransparent: false, isEdited, ...dto, }); diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index af5360d2b6..8fd415a6e7 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -6,7 +6,7 @@ import { Stats } from 'node:fs'; import { Writable } from 'node:stream'; import { AssetFace } from 'src/database'; import { AuthDto, LoginResponseDto } from 'src/dtos/auth.dto'; -import { AssetEditActionListDto } from 'src/dtos/editing.dto'; +import { AssetEditActionItem, AssetEditsCreateDto } from 'src/dtos/editing.dto'; import { AlbumUserRole, AssetType, @@ -283,8 +283,8 @@ export class MediumTestContext { return { tagsAssets, result }; } - async newEdits(assetId: string, dto: AssetEditActionListDto) { - const edits = await this.get(AssetEditRepository).replaceAll(assetId, dto.edits); + async newEdits(assetId: string, dto: AssetEditsCreateDto) { + const edits = await this.get(AssetEditRepository).replaceAll(assetId, dto.edits as AssetEditActionItem[]); return { edits }; } } @@ -636,7 +636,7 @@ const personInsert = (person: Partial> & { ownerId: stri }; }; -const sha256 = (value: string) => createHash('sha256').update(value).digest('base64'); +const sha256 = (value: string) => createHash('sha256').update(value).digest(); const sessionInsert = ({ id = newUuid(), diff --git a/server/test/medium/specs/services/asset.service.spec.ts b/server/test/medium/specs/services/asset.service.spec.ts index 477414dafe..2569b29353 100644 --- a/server/test/medium/specs/services/asset.service.spec.ts +++ b/server/test/medium/specs/services/asset.service.spec.ts @@ -887,15 +887,16 @@ describe(AssetService.name, () => { await ctx.newExif({ assetId: asset.id, exifImageHeight: 42, exifImageWidth: 69, orientation: '1' }); const editAction = { action: AssetEditAction.Rotate, parameters: { angle: 90 } } as const; + const editResponse = { ...editAction, id: expect.any(String) }; await expect(sut.editAsset(auth, asset.id, { edits: [editAction] })).resolves.toEqual({ assetId: asset.id, - edits: [editAction], + edits: [editResponse], }); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toEqual( expect.objectContaining({ isEdited: true }), ); - await expect(ctx.get(AssetEditRepository).getAll(asset.id)).resolves.toEqual([editAction]); + await expect(ctx.get(AssetEditRepository).getAll(asset.id)).resolves.toEqual([editResponse]); }); }); }); diff --git a/server/test/medium/specs/sync/sync-asset-face.spec.ts b/server/test/medium/specs/sync/sync-asset-face.spec.ts index 8b4310e600..34a1e8e73c 100644 --- a/server/test/medium/specs/sync/sync-asset-face.spec.ts +++ b/server/test/medium/specs/sync/sync-asset-face.spec.ts @@ -97,3 +97,134 @@ describe(SyncEntityType.AssetFaceV1, () => { await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV1]); }); }); + +describe(SyncEntityType.AssetFaceV2, () => { + it('should detect and sync the first asset face', async () => { + const { auth, ctx } = await setup(); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const { person } = await ctx.newPerson({ ownerId: auth.user.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personId: person.id }); + + const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]); + expect(response).toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + id: assetFace.id, + assetId: asset.id, + personId: person.id, + imageWidth: assetFace.imageWidth, + imageHeight: assetFace.imageHeight, + boundingBoxX1: assetFace.boundingBoxX1, + boundingBoxY1: assetFace.boundingBoxY1, + boundingBoxX2: assetFace.boundingBoxX2, + boundingBoxY2: assetFace.boundingBoxY2, + sourceType: assetFace.sourceType, + }), + type: 'AssetFaceV2', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + + await ctx.syncAckAll(auth, response); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]); + }); + + it('should detect and sync a deleted asset face', async () => { + const { auth, ctx } = await setup(); + const personRepo = ctx.get(PersonRepository); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id }); + await personRepo.deleteAssetFace(assetFace.id); + + const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + assetFaceId: assetFace.id, + }, + type: 'AssetFaceDeleteV1', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + + await ctx.syncAckAll(auth, response); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]); + }); + + it('should not sync an asset face or asset face delete for an unrelated user', async () => { + const { auth, ctx } = await setup(); + const personRepo = ctx.get(PersonRepository); + const { user: user2 } = await ctx.newUser(); + const { session } = await ctx.newSession({ userId: user2.id }); + const { asset } = await ctx.newAsset({ ownerId: user2.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id }); + const auth2 = factory.auth({ session, user: user2 }); + + expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV2])).toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetFaceV2 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]); + + await personRepo.deleteAssetFace(assetFace.id); + + expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV2])).toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetFaceDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]); + }); + + it('should contain the deletedAt and isVisible fields in AssetFaceV2', async () => { + const { auth, ctx } = await setup(); + const personRepo = ctx.get(PersonRepository); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const { person } = await ctx.newPerson({ ownerId: auth.user.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personId: person.id }); + + let response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]); + expect(response).toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + id: assetFace.id, + assetId: asset.id, + personId: person.id, + imageWidth: assetFace.imageWidth, + imageHeight: assetFace.imageHeight, + boundingBoxX1: assetFace.boundingBoxX1, + boundingBoxY1: assetFace.boundingBoxY1, + boundingBoxX2: assetFace.boundingBoxX2, + boundingBoxY2: assetFace.boundingBoxY2, + sourceType: assetFace.sourceType, + deletedAt: null, + isVisible: true, + }), + type: 'AssetFaceV2', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + + await ctx.syncAckAll(auth, response); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]); + + await personRepo.deleteAssetFace(assetFace.id); + + response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + assetFaceId: assetFace.id, + }, + type: 'AssetFaceDeleteV1', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + + await ctx.syncAckAll(auth, response); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]); + }); +}); diff --git a/server/test/repositories/crypto.repository.mock.ts b/server/test/repositories/crypto.repository.mock.ts index 773891206e..fb2b933d97 100644 --- a/server/test/repositories/crypto.repository.mock.ts +++ b/server/test/repositories/crypto.repository.mock.ts @@ -8,7 +8,7 @@ export const newCryptoRepositoryMock = (): Mocked Promise.resolve(`${input} (hashed)`)), - hashSha256: vitest.fn().mockImplementation((input) => `${input} (hashed)`), + hashSha256: vitest.fn().mockImplementation((input) => Buffer.from(`${input} (hashed)`)), verifySha256: vitest.fn().mockImplementation(() => true), hashSha1: vitest.fn().mockImplementation((input) => Buffer.from(`${input.toString()} (hashed)`)), hashFile: vitest.fn().mockImplementation((input) => `${input} (file-hashed)`), diff --git a/server/test/repositories/media.repository.mock.ts b/server/test/repositories/media.repository.mock.ts index b6b1e82b52..bd8deb4b3a 100644 --- a/server/test/repositories/media.repository.mock.ts +++ b/server/test/repositories/media.repository.mock.ts @@ -12,6 +12,6 @@ export const newMediaRepositoryMock = (): Mocked = {}) => ({ updateId: newUuidV7(), deviceOS: 'android', deviceType: 'mobile', - token: 'abc123', + token: Buffer.from('abc123'), parentId: null, expiresAt: null, userId: newUuid(), diff --git a/server/test/sql-tools/check-constraint-default-name.stub.ts b/server/test/sql-tools/check-constraint-default-name.stub.ts deleted file mode 100644 index 1cb7c0644a..0000000000 --- a/server/test/sql-tools/check-constraint-default-name.stub.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Check, Column, ConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -@Check({ expression: '1=1' }) -export class Table1 { - @Column({ type: 'uuid' }) - id!: string; -} - -export const description = 'should create a check constraint with a default name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.CHECK, - name: 'CHK_8d2ecfd49b984941f6b2589799', - tableName: 'table1', - expression: '1=1', - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/check-constraint-override-name.stub.ts b/server/test/sql-tools/check-constraint-override-name.stub.ts deleted file mode 100644 index 3752dcfb22..0000000000 --- a/server/test/sql-tools/check-constraint-override-name.stub.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Check, Column, ConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -@Check({ name: 'CHK_test', expression: '1=1' }) -export class Table1 { - @Column({ type: 'uuid' }) - id!: string; -} - -export const description = 'should create a check constraint with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.CHECK, - name: 'CHK_test', - tableName: 'table1', - expression: '1=1', - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-create-date.stub.ts b/server/test/sql-tools/column-create-date.stub.ts deleted file mode 100644 index db5add2a12..0000000000 --- a/server/test/sql-tools/column-create-date.stub.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { CreateDateColumn, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @CreateDateColumn() - createdAt!: string; -} - -export const description = 'should register a table with an created at date column'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'createdAt', - tableName: 'table1', - type: 'timestamp with time zone', - default: 'now()', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-default-array.stub.ts b/server/test/sql-tools/column-default-array.stub.ts deleted file mode 100644 index b5e9b7d04a..0000000000 --- a/server/test/sql-tools/column-default-array.stub.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ type: 'character varying', array: true, default: [] }) - column1!: string[]; -} - -export const description = 'should register a table with a column with a default value (array)'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: true, - primary: false, - synchronize: true, - default: "'{}'", - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-default-boolean.stub.ts b/server/test/sql-tools/column-default-boolean.stub.ts deleted file mode 100644 index 6454333599..0000000000 --- a/server/test/sql-tools/column-default-boolean.stub.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ type: 'boolean', default: true }) - column1!: boolean; -} - -export const description = 'should register a table with a column with a default value (boolean)'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'boolean', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - default: 'true', - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-default-date.stub.ts b/server/test/sql-tools/column-default-date.stub.ts deleted file mode 100644 index 70f4d520f9..0000000000 --- a/server/test/sql-tools/column-default-date.stub.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -const date = new Date(2023, 0, 1); - -@Table() -export class Table1 { - @Column({ type: 'character varying', default: date }) - column1!: string; -} - -export const description = 'should register a table with a column with a default value (date)'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - default: "'2023-01-01T00:00:00.000Z'", - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-default-function.stub.ts b/server/test/sql-tools/column-default-function.stub.ts deleted file mode 100644 index 1066a9af21..0000000000 --- a/server/test/sql-tools/column-default-function.stub.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ type: 'character varying', default: () => 'now()' }) - column1!: string; -} - -export const description = 'should register a table with a column with a default function'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - default: 'now()', - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-default-null.stub.ts b/server/test/sql-tools/column-default-null.stub.ts deleted file mode 100644 index b517ca5a96..0000000000 --- a/server/test/sql-tools/column-default-null.stub.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ type: 'character varying', default: null }) - column1!: string; -} - -export const description = 'should register a nullable column from a default of null'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: true, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-default-number.stub.ts b/server/test/sql-tools/column-default-number.stub.ts deleted file mode 100644 index 7954f2498b..0000000000 --- a/server/test/sql-tools/column-default-number.stub.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ type: 'integer', default: 0 }) - column1!: string; -} - -export const description = 'should register a table with a column with a default value (number)'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'integer', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - default: '0', - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-default-string.stub.ts b/server/test/sql-tools/column-default-string.stub.ts deleted file mode 100644 index 0d0a18a0eb..0000000000 --- a/server/test/sql-tools/column-default-string.stub.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ type: 'character varying', default: 'foo' }) - column1!: string; -} - -export const description = 'should register a table with a column with a default value (string)'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - default: "'foo'", - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-delete-date.stub.ts b/server/test/sql-tools/column-delete-date.stub.ts deleted file mode 100644 index de494ad16e..0000000000 --- a/server/test/sql-tools/column-delete-date.stub.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { DatabaseSchema, DeleteDateColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @DeleteDateColumn() - deletedAt!: string; -} - -export const description = 'should register a table with a deleted at date column'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'deletedAt', - tableName: 'table1', - type: 'timestamp with time zone', - nullable: true, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-enum-type.stub.ts b/server/test/sql-tools/column-enum-type.stub.ts deleted file mode 100644 index 563835d720..0000000000 --- a/server/test/sql-tools/column-enum-type.stub.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Column, DatabaseSchema, registerEnum, Table } from 'src/sql-tools'; - -enum Test { - Foo = 'foo', - Bar = 'bar', -} - -const test_enum = registerEnum({ name: 'test_enum', values: Object.values(Test) }); - -@Table() -export class Table1 { - @Column({ enum: test_enum }) - column1!: string; -} - -export const description = 'should accept an enum type'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [ - { - name: 'test_enum', - values: ['foo', 'bar'], - synchronize: true, - }, - ], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'enum', - enumName: 'test_enum', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-generated-identity.ts b/server/test/sql-tools/column-generated-identity.ts deleted file mode 100644 index 29f7ba969a..0000000000 --- a/server/test/sql-tools/column-generated-identity.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ConstraintType, DatabaseSchema, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryGeneratedColumn({ strategy: 'identity' }) - column1!: string; -} - -export const description = 'should register a table with a generated identity column'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'integer', - identity: true, - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_50c4f9905061b1e506d38a2a380', - tableName: 'table1', - columnNames: ['column1'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-generated-uuid.stub.ts b/server/test/sql-tools/column-generated-uuid.stub.ts deleted file mode 100644 index 0d4d78a84f..0000000000 --- a/server/test/sql-tools/column-generated-uuid.stub.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ConstraintType, DatabaseSchema, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryGeneratedColumn({ strategy: 'uuid' }) - column1!: string; -} - -export const description = 'should register a table with a primary generated uuid column'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'uuid', - default: 'uuid_generate_v4()', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_50c4f9905061b1e506d38a2a380', - tableName: 'table1', - columnNames: ['column1'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-index-name-default.ts b/server/test/sql-tools/column-index-name-default.ts deleted file mode 100644 index ea1fb17fb4..0000000000 --- a/server/test/sql-tools/column-index-name-default.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ index: true }) - column1!: string; -} - -export const description = 'should create a column with an index'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_50c4f9905061b1e506d38a2a38', - columnNames: ['column1'], - tableName: 'table1', - unique: false, - synchronize: true, - }, - ], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-index-name.ts b/server/test/sql-tools/column-index-name.ts deleted file mode 100644 index 2a37469600..0000000000 --- a/server/test/sql-tools/column-index-name.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ indexName: 'IDX_test' }) - column1!: string; -} - -export const description = 'should create a column with an index if a name is provided'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_test', - columnNames: ['column1'], - tableName: 'table1', - unique: false, - synchronize: true, - }, - ], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-inferred-nullable.stub.ts b/server/test/sql-tools/column-inferred-nullable.stub.ts deleted file mode 100644 index 50810291d3..0000000000 --- a/server/test/sql-tools/column-inferred-nullable.stub.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ default: null }) - column1!: string; -} - -export const description = 'should infer nullable from the default value'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: true, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-name-default.stub.ts b/server/test/sql-tools/column-name-default.stub.ts deleted file mode 100644 index 57e15fc8b6..0000000000 --- a/server/test/sql-tools/column-name-default.stub.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column() - column1!: string; -} - -export const description = 'should register a table with a column with a default name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-name-override.stub.ts b/server/test/sql-tools/column-name-override.stub.ts deleted file mode 100644 index 8741162735..0000000000 --- a/server/test/sql-tools/column-name-override.stub.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ name: 'column-1' }) - column1!: string; -} - -export const description = 'should register a table with a column with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column-1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-name-string.stub.ts b/server/test/sql-tools/column-name-string.stub.ts deleted file mode 100644 index e4a60f51b9..0000000000 --- a/server/test/sql-tools/column-name-string.stub.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column('column-1') - column1!: string; -} - -export const description = 'should register a table with a column with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column-1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-nullable.stub.ts b/server/test/sql-tools/column-nullable.stub.ts deleted file mode 100644 index 31c72fe97c..0000000000 --- a/server/test/sql-tools/column-nullable.stub.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ nullable: true }) - column1!: string; -} - -export const description = 'should set nullable correctly'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: true, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-string-length.stub.ts b/server/test/sql-tools/column-string-length.stub.ts deleted file mode 100644 index a04cfbd117..0000000000 --- a/server/test/sql-tools/column-string-length.stub.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ length: 2 }) - column1!: string; -} - -export const description = 'should use create a string column with a fixed length'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - length: 2, - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-unique-constraint-name-default.stub.ts b/server/test/sql-tools/column-unique-constraint-name-default.stub.ts deleted file mode 100644 index 076a93bf57..0000000000 --- a/server/test/sql-tools/column-unique-constraint-name-default.stub.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ type: 'uuid', unique: true }) - id!: string; -} - -export const description = 'should create a unique key constraint with a default name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.UNIQUE, - name: 'UQ_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-unique-constraint-name-override.stub.ts b/server/test/sql-tools/column-unique-constraint-name-override.stub.ts deleted file mode 100644 index d4c3d5bb6a..0000000000 --- a/server/test/sql-tools/column-unique-constraint-name-override.stub.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ type: 'uuid', unique: true, uniqueConstraintName: 'UQ_test' }) - id!: string; -} - -export const description = 'should create a unique key constraint with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.UNIQUE, - name: 'UQ_test', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-update-date.stub.ts b/server/test/sql-tools/column-update-date.stub.ts deleted file mode 100644 index dfa09888c0..0000000000 --- a/server/test/sql-tools/column-update-date.stub.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { DatabaseSchema, Table, UpdateDateColumn } from 'src/sql-tools'; - -@Table() -export class Table1 { - @UpdateDateColumn() - updatedAt!: string; -} - -export const description = 'should register a table with an updated at date column'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'updatedAt', - tableName: 'table1', - type: 'timestamp with time zone', - default: 'now()', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/errors/table-duplicate-decorator.stub.ts b/server/test/sql-tools/errors/table-duplicate-decorator.stub.ts deleted file mode 100644 index 3b7a8781b9..0000000000 --- a/server/test/sql-tools/errors/table-duplicate-decorator.stub.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Table } from 'src/sql-tools'; - -@Table({ name: 'table-1' }) -@Table({ name: 'table-2' }) -export class Table1 {} - -export const message = 'Table table-2 has already been registered'; diff --git a/server/test/sql-tools/foreign-key-constraint-column-order.stub.ts b/server/test/sql-tools/foreign-key-constraint-column-order.stub.ts deleted file mode 100644 index 2523701e49..0000000000 --- a/server/test/sql-tools/foreign-key-constraint-column-order.stub.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id1!: string; - - @PrimaryColumn({ type: 'uuid' }) - id2!: string; -} - -@Table() -@ForeignKeyConstraint({ - columns: ['parentId1', 'parentId2'], - referenceTable: () => Table1, - referenceColumns: ['id2', 'id1'], -}) -export class Table2 { - @Column({ type: 'uuid' }) - parentId1!: string; - - @Column({ type: 'uuid' }) - parentId2!: string; -} - -export const description = 'should create a foreign key constraint to the target table'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id1', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - { - name: 'id2', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_e457e8b1301b7bc06ef78188ee4', - tableName: 'table1', - columnNames: ['id1', 'id2'], - synchronize: true, - }, - ], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'parentId1', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - { - name: 'parentId2', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_aed36d04470eba20161aa8b1dc', - tableName: 'table2', - columnNames: ['parentId1', 'parentId2'], - unique: false, - synchronize: true, - }, - ], - triggers: [], - constraints: [ - { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_aed36d04470eba20161aa8b1dc6', - tableName: 'table2', - columnNames: ['parentId1', 'parentId2'], - referenceColumnNames: ['id2', 'id1'], - referenceTableName: 'table1', - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/foreign-key-constraint-missing-column.stub.ts b/server/test/sql-tools/foreign-key-constraint-missing-column.stub.ts deleted file mode 100644 index dcd957676a..0000000000 --- a/server/test/sql-tools/foreign-key-constraint-missing-column.stub.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id!: string; -} - -@Table() -@ForeignKeyConstraint({ columns: ['parentId2'], referenceTable: () => Table1 }) -export class Table2 { - @Column({ type: 'uuid' }) - parentId!: string; -} - -export const description = 'should warn against missing column in foreign key constraint'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'parentId', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: ['[@ForeignKeyConstraint.columns] Unable to find column (Table2.parentId2)'], -}; diff --git a/server/test/sql-tools/foreign-key-constraint-missing-reference-column.stub.ts b/server/test/sql-tools/foreign-key-constraint-missing-reference-column.stub.ts deleted file mode 100644 index 238f4174f3..0000000000 --- a/server/test/sql-tools/foreign-key-constraint-missing-reference-column.stub.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id!: string; -} - -@Table() -@ForeignKeyConstraint({ columns: ['parentId'], referenceTable: () => Table1, referenceColumns: ['foo'] }) -export class Table2 { - @Column({ type: 'uuid' }) - parentId!: string; -} - -export const description = 'should warn against missing reference column in foreign key constraint'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'parentId', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: ['[@ForeignKeyConstraint.referenceColumns] Unable to find column (Table1.foo)'], -}; diff --git a/server/test/sql-tools/foreign-key-constraint-missing-reference-table.stub.ts b/server/test/sql-tools/foreign-key-constraint-missing-reference-table.stub.ts deleted file mode 100644 index c6d6fd5b09..0000000000 --- a/server/test/sql-tools/foreign-key-constraint-missing-reference-table.stub.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Column, DatabaseSchema, ForeignKeyConstraint, Table } from 'src/sql-tools'; - -class Foo {} - -@Table() -@ForeignKeyConstraint({ - columns: ['parentId'], - referenceTable: () => Foo, -}) -export class Table1 { - @Column() - parentId!: string; -} - -export const description = 'should warn against missing reference table'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'parentId', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: ['[@ForeignKeyConstraint.referenceTable] Unable to find table (Foo)'], -}; diff --git a/server/test/sql-tools/foreign-key-constraint-multiple-columns.stub.ts b/server/test/sql-tools/foreign-key-constraint-multiple-columns.stub.ts deleted file mode 100644 index a86611bb50..0000000000 --- a/server/test/sql-tools/foreign-key-constraint-multiple-columns.stub.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id1!: string; - - @PrimaryColumn({ type: 'uuid' }) - id2!: string; -} - -@Table() -@ForeignKeyConstraint({ columns: ['parentId1', 'parentId2'], referenceTable: () => Table1 }) -export class Table2 { - @Column({ type: 'uuid' }) - parentId1!: string; - - @Column({ type: 'uuid' }) - parentId2!: string; -} - -export const description = 'should create a foreign key constraint to the target table'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id1', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - { - name: 'id2', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_e457e8b1301b7bc06ef78188ee4', - tableName: 'table1', - columnNames: ['id1', 'id2'], - synchronize: true, - }, - ], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'parentId1', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - { - name: 'parentId2', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_aed36d04470eba20161aa8b1dc', - tableName: 'table2', - columnNames: ['parentId1', 'parentId2'], - unique: false, - synchronize: true, - }, - ], - triggers: [], - constraints: [ - { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_aed36d04470eba20161aa8b1dc6', - tableName: 'table2', - columnNames: ['parentId1', 'parentId2'], - referenceColumnNames: ['id1', 'id2'], - referenceTableName: 'table1', - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/foreign-key-constraint-no-index.stub.ts b/server/test/sql-tools/foreign-key-constraint-no-index.stub.ts deleted file mode 100644 index 8bb436c9ac..0000000000 --- a/server/test/sql-tools/foreign-key-constraint-no-index.stub.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id!: string; -} - -@Table() -@ForeignKeyConstraint({ columns: ['parentId'], referenceTable: () => Table1, index: false }) -export class Table2 { - @Column({ type: 'uuid' }) - parentId!: string; -} - -export const description = 'should create a foreign key constraint to the target table without an index'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'parentId', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_3fcca5cc563abf256fc346e3ff4', - tableName: 'table2', - columnNames: ['parentId'], - referenceColumnNames: ['id'], - referenceTableName: 'table1', - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/foreign-key-constraint-no-primary.stub.ts b/server/test/sql-tools/foreign-key-constraint-no-primary.stub.ts deleted file mode 100644 index 6680b13b91..0000000000 --- a/server/test/sql-tools/foreign-key-constraint-no-primary.stub.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column() - foo!: string; -} - -@Table() -@ForeignKeyConstraint({ - columns: ['bar'], - referenceTable: () => Table1, - referenceColumns: ['foo'], -}) -export class Table2 { - @Column() - bar!: string; -} - -export const description = 'should create a foreign key constraint to the target table without a primary key'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'foo', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'bar', - tableName: 'table2', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_7d9c784c98d12365d198d52e4e', - tableName: 'table2', - columnNames: ['bar'], - unique: false, - synchronize: true, - }, - ], - triggers: [], - constraints: [ - { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_7d9c784c98d12365d198d52e4e6', - tableName: 'table2', - columnNames: ['bar'], - referenceTableName: 'table1', - referenceColumnNames: ['foo'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/foreign-key-constraint.stub.ts b/server/test/sql-tools/foreign-key-constraint.stub.ts deleted file mode 100644 index 518c5aa6bb..0000000000 --- a/server/test/sql-tools/foreign-key-constraint.stub.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id!: string; -} - -@Table() -@ForeignKeyConstraint({ columns: ['parentId'], referenceTable: () => Table1 }) -export class Table2 { - @Column({ type: 'uuid' }) - parentId!: string; -} - -export const description = 'should create a foreign key constraint to the target table'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'parentId', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_3fcca5cc563abf256fc346e3ff', - tableName: 'table2', - columnNames: ['parentId'], - unique: false, - synchronize: true, - }, - ], - triggers: [], - constraints: [ - { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_3fcca5cc563abf256fc346e3ff4', - tableName: 'table2', - columnNames: ['parentId'], - referenceColumnNames: ['id'], - referenceTableName: 'table1', - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/foreign-key-inferred-type.stub.ts b/server/test/sql-tools/foreign-key-inferred-type.stub.ts deleted file mode 100644 index 33f1c2dfde..0000000000 --- a/server/test/sql-tools/foreign-key-inferred-type.stub.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { ConstraintType, DatabaseSchema, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id!: string; -} - -@Table() -export class Table2 { - @ForeignKeyColumn(() => Table1, {}) - parentId!: string; -} - -export const description = 'should infer the column type from the reference column'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'parentId', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_3fcca5cc563abf256fc346e3ff', - tableName: 'table2', - columnNames: ['parentId'], - unique: false, - synchronize: true, - }, - ], - triggers: [], - constraints: [ - { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_3fcca5cc563abf256fc346e3ff4', - tableName: 'table2', - columnNames: ['parentId'], - referenceColumnNames: ['id'], - referenceTableName: 'table1', - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts b/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts deleted file mode 100644 index 288f7c6698..0000000000 --- a/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { ConstraintType, DatabaseSchema, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id!: string; -} - -@Table() -export class Table2 { - @ForeignKeyColumn(() => Table1, { unique: true }) - parentId!: string; -} - -export const description = 'should create a foreign key constraint with a unique constraint'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'parentId', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_3fcca5cc563abf256fc346e3ff', - tableName: 'table2', - columnNames: ['parentId'], - unique: false, - synchronize: true, - }, - ], - triggers: [], - constraints: [ - { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_3fcca5cc563abf256fc346e3ff4', - tableName: 'table2', - columnNames: ['parentId'], - referenceColumnNames: ['id'], - referenceTableName: 'table1', - synchronize: true, - }, - { - type: ConstraintType.UNIQUE, - name: 'UQ_3fcca5cc563abf256fc346e3ff4', - tableName: 'table2', - columnNames: ['parentId'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/index-name-default.stub.ts b/server/test/sql-tools/index-name-default.stub.ts deleted file mode 100644 index 1918106eaa..0000000000 --- a/server/test/sql-tools/index-name-default.stub.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Column, DatabaseSchema, Index, Table } from 'src/sql-tools'; - -@Table() -@Index({ columns: ['id'] }) -export class Table1 { - @Column({ type: 'uuid' }) - id!: string; -} - -export const description = 'should create an index with a default name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_b249cc64cf63b8a22557cdc853', - tableName: 'table1', - unique: false, - columnNames: ['id'], - synchronize: true, - }, - ], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/index-name-override.stub.ts b/server/test/sql-tools/index-name-override.stub.ts deleted file mode 100644 index a48dc6e6d6..0000000000 --- a/server/test/sql-tools/index-name-override.stub.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Column, DatabaseSchema, Index, Table } from 'src/sql-tools'; - -@Table() -@Index({ name: 'IDX_test', columns: ['id'] }) -export class Table1 { - @Column({ type: 'uuid' }) - id!: string; -} - -export const description = 'should create an index with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_test', - tableName: 'table1', - unique: false, - columnNames: ['id'], - synchronize: true, - }, - ], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/index-with-expression.ts b/server/test/sql-tools/index-with-expression.ts deleted file mode 100644 index 07755b7f96..0000000000 --- a/server/test/sql-tools/index-with-expression.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Column, DatabaseSchema, Index, Table } from 'src/sql-tools'; - -@Table() -@Index({ expression: '"id" IS NOT NULL' }) -export class Table1 { - @Column({ nullable: true }) - column1!: string; -} - -export const description = 'should create an index based off of an expression'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: true, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_376788d186160c4faa5aaaef63', - tableName: 'table1', - unique: false, - expression: '"id" IS NOT NULL', - synchronize: true, - }, - ], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/index-with-where.stub.ts b/server/test/sql-tools/index-with-where.stub.ts deleted file mode 100644 index 86a4a3089d..0000000000 --- a/server/test/sql-tools/index-with-where.stub.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Column, DatabaseSchema, Index, Table } from 'src/sql-tools'; - -@Table() -@Index({ columns: ['id'], where: '"id" IS NOT NULL' }) -export class Table1 { - @Column({ nullable: true }) - column1!: string; -} - -export const description = 'should create an index with a where clause'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: true, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_9f4e073964c0395f51f9b39900', - tableName: 'table1', - unique: false, - columnNames: ['id'], - where: '"id" IS NOT NULL', - synchronize: true, - }, - ], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/primary-key-constraint-name-default.stub.ts b/server/test/sql-tools/primary-key-constraint-name-default.stub.ts deleted file mode 100644 index 7edfd6ff36..0000000000 --- a/server/test/sql-tools/primary-key-constraint-name-default.stub.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ConstraintType, DatabaseSchema, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id!: string; -} - -export const description = 'should add a primary key constraint to the table with a default name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/primary-key-constraint-name-override.stub.ts b/server/test/sql-tools/primary-key-constraint-name-override.stub.ts deleted file mode 100644 index ce1f2a096c..0000000000 --- a/server/test/sql-tools/primary-key-constraint-name-override.stub.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ConstraintType, DatabaseSchema, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table({ primaryConstraintName: 'PK_test' }) -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id!: string; -} - -export const description = 'should add a primary key constraint to the table with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_test', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/table-name-default.stub.ts b/server/test/sql-tools/table-name-default.stub.ts deleted file mode 100644 index 4384944364..0000000000 --- a/server/test/sql-tools/table-name-default.stub.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 {} - -export const description = 'should register a table with a default name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/table-name-override.stub.ts b/server/test/sql-tools/table-name-override.stub.ts deleted file mode 100644 index 5bccc429d0..0000000000 --- a/server/test/sql-tools/table-name-override.stub.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DatabaseSchema, Table } from 'src/sql-tools'; - -@Table({ name: 'table-1' }) -export class Table1 {} - -export const description = 'should register a table with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table-1', - columns: [], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/table-name-string-option.stub.ts b/server/test/sql-tools/table-name-string-option.stub.ts deleted file mode 100644 index f394699172..0000000000 --- a/server/test/sql-tools/table-name-string-option.stub.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DatabaseSchema, Table } from 'src/sql-tools'; - -@Table('table-1') -export class Table1 {} - -export const description = 'should register a table with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table-1', - columns: [], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/trigger-after-delete.stub.ts b/server/test/sql-tools/trigger-after-delete.stub.ts deleted file mode 100644 index dcceaf25ce..0000000000 --- a/server/test/sql-tools/trigger-after-delete.stub.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { AfterDeleteTrigger, DatabaseSchema, registerFunction, Table } from 'src/sql-tools'; - -const test_fn = registerFunction({ - name: 'test_fn', - body: 'SELECT 1;', - returnType: 'character varying', -}); - -@Table() -@AfterDeleteTrigger({ - name: 'my_trigger', - function: test_fn, - scope: 'row', -}) -export class Table1 {} - -export const description = 'should create a trigger'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [expect.any(Object)], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [], - indexes: [], - triggers: [ - { - name: 'my_trigger', - functionName: 'test_fn', - tableName: 'table1', - timing: 'after', - scope: 'row', - actions: ['delete'], - synchronize: true, - }, - ], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/trigger-before-update.stub.ts b/server/test/sql-tools/trigger-before-update.stub.ts deleted file mode 100644 index 6bf6afc721..0000000000 --- a/server/test/sql-tools/trigger-before-update.stub.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { BeforeUpdateTrigger, DatabaseSchema, registerFunction, Table } from 'src/sql-tools'; - -const test_fn = registerFunction({ - name: 'test_fn', - body: 'SELECT 1;', - returnType: 'character varying', -}); - -@Table() -@BeforeUpdateTrigger({ - name: 'my_trigger', - function: test_fn, - scope: 'row', -}) -export class Table1 {} - -export const description = 'should create a trigger '; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [expect.any(Object)], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [], - indexes: [], - triggers: [ - { - name: 'my_trigger', - functionName: 'test_fn', - tableName: 'table1', - timing: 'before', - scope: 'row', - actions: ['update'], - synchronize: true, - }, - ], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/trigger-name-default.stub.ts b/server/test/sql-tools/trigger-name-default.stub.ts deleted file mode 100644 index 382389bcf7..0000000000 --- a/server/test/sql-tools/trigger-name-default.stub.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { DatabaseSchema, Table, Trigger } from 'src/sql-tools'; - -@Table() -@Trigger({ - timing: 'before', - actions: ['insert'], - scope: 'row', - functionName: 'function1', -}) -export class Table1 {} - -export const description = 'should register a trigger with a default name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [], - indexes: [], - triggers: [ - { - name: 'TR_ca71832b10b77ed600ef05df631', - tableName: 'table1', - functionName: 'function1', - actions: ['insert'], - scope: 'row', - timing: 'before', - synchronize: true, - }, - ], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/trigger-name-override.stub.ts b/server/test/sql-tools/trigger-name-override.stub.ts deleted file mode 100644 index 33c4da6b67..0000000000 --- a/server/test/sql-tools/trigger-name-override.stub.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { DatabaseSchema, Table, Trigger } from 'src/sql-tools'; - -@Table() -@Trigger({ - name: 'trigger1', - timing: 'before', - actions: ['insert'], - scope: 'row', - functionName: 'function1', -}) -export class Table1 {} - -export const description = 'should a trigger with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [], - indexes: [], - triggers: [ - { - name: 'trigger1', - tableName: 'table1', - functionName: 'function1', - actions: ['insert'], - scope: 'row', - timing: 'before', - synchronize: true, - }, - ], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/unique-constraint-name-default.stub.ts b/server/test/sql-tools/unique-constraint-name-default.stub.ts deleted file mode 100644 index 90fbe09224..0000000000 --- a/server/test/sql-tools/unique-constraint-name-default.stub.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, Table, Unique } from 'src/sql-tools'; - -@Table() -@Unique({ columns: ['id'] }) -export class Table1 { - @Column({ type: 'uuid' }) - id!: string; -} - -export const description = 'should add a unique constraint to the table with a default name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.UNIQUE, - name: 'UQ_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/unique-constraint-name-override.stub.ts b/server/test/sql-tools/unique-constraint-name-override.stub.ts deleted file mode 100644 index 3da7584c0c..0000000000 --- a/server/test/sql-tools/unique-constraint-name-override.stub.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, Table, Unique } from 'src/sql-tools'; - -@Table() -@Unique({ name: 'UQ_test', columns: ['id'] }) -export class Table1 { - @Column({ type: 'uuid' }) - id!: string; -} - -export const description = 'should add a unique constraint to the table with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.UNIQUE, - name: 'UQ_test', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/utils.ts b/server/test/utils.ts index e22e42c230..121c251ab8 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -1,3 +1,4 @@ +import { createPostgres, DatabaseConnectionParams } from '@immich/sql-tools'; import { CallHandler, ExecutionContext, Provider, ValidationPipe } from '@nestjs/common'; import { APP_GUARD, APP_PIPE } from '@nestjs/core'; import { transformException } from '@nestjs/platform-express/multer/multer/multer.utils'; @@ -9,7 +10,6 @@ import multer from 'multer'; import { ChildProcessWithoutNullStreams } from 'node:child_process'; import { Duplex, Readable, Writable } from 'node:stream'; import { PNG } from 'pngjs'; -import postgres from 'postgres'; import { UploadFieldName } from 'src/dtos/asset-media.dto'; import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor'; import { AuthGuard } from 'src/middleware/auth.guard'; @@ -71,7 +71,7 @@ import { DB } from 'src/schema'; import { AuthService } from 'src/services/auth.service'; import { BaseService } from 'src/services/base.service'; import { RepositoryInterface } from 'src/types'; -import { asPostgresConnectionConfig, getKyselyConfig } from 'src/utils/database'; +import { getKyselyConfig } from 'src/utils/database'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; @@ -449,13 +449,8 @@ const withDatabase = (url: string, name: string) => url.replace(`/${templateName export const getKyselyDB = async (suffix?: string): Promise> => { const testUrl = process.env.IMMICH_TEST_POSTGRES_URL!; - const sql = postgres({ - ...asPostgresConnectionConfig({ - connectionType: 'url', - url: withDatabase(testUrl, 'postgres'), - }), - max: 1, - }); + const connection = { connectionType: 'url', url: withDatabase(testUrl, 'postgres') } as DatabaseConnectionParams; + const sql = createPostgres({ maxConnections: 1, connection }); const randomSuffix = Math.random().toString(36).slice(2, 7); const dbName = `immich_${suffix ?? randomSuffix}`; diff --git a/server/tsconfig.json b/server/tsconfig.json index e12b614f0d..fcb0ea2a97 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "module": "node16", + "module": "node20", "strict": true, "declaration": true, "removeComments": true, diff --git a/web/package.json b/web/package.json index b45e89fc97..03ccb35d7e 100644 --- a/web/package.json +++ b/web/package.json @@ -65,7 +65,7 @@ "uplot": "^1.6.32" }, "devDependencies": { - "@eslint/js": "^9.36.0", + "@eslint/js": "^10.0.0", "@faker-js/faker": "^10.0.0", "@koddsson/eslint-plugin-tscompat": "^0.2.0", "@socket.io/component-emitter": "^3.1.0", @@ -85,20 +85,20 @@ "@types/qrcode": "^1.5.5", "@vitest/coverage-v8": "^3.0.0", "dotenv": "^17.0.0", - "eslint": "^9.36.0", + "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-compat": "^6.0.2", "eslint-plugin-svelte": "^3.12.4", - "eslint-plugin-unicorn": "^62.0.0", + "eslint-plugin-unicorn": "^63.0.0", "factory.ts": "^1.4.1", - "globals": "^16.0.0", + "globals": "^17.0.0", "happy-dom": "^20.0.0", "prettier": "^3.7.4", "prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-sort-json": "^4.1.1", "prettier-plugin-svelte": "^3.3.3", "rollup-plugin-visualizer": "^6.0.0", - "svelte": "5.51.5", + "svelte": "5.53.0", "svelte-check": "^4.1.5", "svelte-eslint-parser": "^1.3.3", "tailwindcss": "^4.1.7", diff --git a/web/src/lib/components/Image.spec.ts b/web/src/lib/components/Image.spec.ts new file mode 100644 index 0000000000..8435e1bb25 --- /dev/null +++ b/web/src/lib/components/Image.spec.ts @@ -0,0 +1,87 @@ +import Image from '$lib/components/Image.svelte'; +import { cancelImageUrl } from '$lib/utils/sw-messaging'; +import { fireEvent, render } from '@testing-library/svelte'; + +vi.mock('$lib/utils/sw-messaging', () => ({ + cancelImageUrl: vi.fn(), +})); + +describe('Image component', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders an img element when src is provided', () => { + const { baseElement } = render(Image, { src: '/test.jpg', alt: 'test' }); + const img = baseElement.querySelector('img'); + expect(img).not.toBeNull(); + expect(img!.getAttribute('src')).toBe('/test.jpg'); + }); + + it('does not render an img element when src is undefined', () => { + const { baseElement } = render(Image, { src: undefined }); + const img = baseElement.querySelector('img'); + expect(img).toBeNull(); + }); + + it('calls onStart when src is set', () => { + const onStart = vi.fn(); + render(Image, { src: '/test.jpg', onStart }); + expect(onStart).toHaveBeenCalledOnce(); + }); + + it('calls onLoad when image loads', async () => { + const onLoad = vi.fn(); + const { baseElement } = render(Image, { src: '/test.jpg', onLoad }); + const img = baseElement.querySelector('img')!; + await fireEvent.load(img); + expect(onLoad).toHaveBeenCalledOnce(); + }); + + it('calls onError when image fails to load', async () => { + const onError = vi.fn(); + const { baseElement } = render(Image, { src: '/test.jpg', onError }); + const img = baseElement.querySelector('img')!; + await fireEvent.error(img); + expect(onError).toHaveBeenCalledOnce(); + expect(onError).toHaveBeenCalledWith(expect.any(Error)); + expect(onError.mock.calls[0][0].message).toBe('Failed to load image: /test.jpg'); + }); + + it('calls cancelImageUrl on unmount', () => { + const { unmount } = render(Image, { src: '/test.jpg' }); + expect(cancelImageUrl).not.toHaveBeenCalled(); + unmount(); + expect(cancelImageUrl).toHaveBeenCalledWith('/test.jpg'); + }); + + it('does not call onLoad after unmount', async () => { + const onLoad = vi.fn(); + const { baseElement, unmount } = render(Image, { src: '/test.jpg', onLoad }); + const img = baseElement.querySelector('img')!; + unmount(); + await fireEvent.load(img); + expect(onLoad).not.toHaveBeenCalled(); + }); + + it('does not call onError after unmount', async () => { + const onError = vi.fn(); + const { baseElement, unmount } = render(Image, { src: '/test.jpg', onError }); + const img = baseElement.querySelector('img')!; + unmount(); + await fireEvent.error(img); + expect(onError).not.toHaveBeenCalled(); + }); + + it('passes through additional HTML attributes', () => { + const { baseElement } = render(Image, { + src: '/test.jpg', + alt: 'test alt', + class: 'my-class', + draggable: false, + }); + const img = baseElement.querySelector('img')!; + expect(img.getAttribute('alt')).toBe('test alt'); + expect(img.getAttribute('draggable')).toBe('false'); + }); +}); diff --git a/web/src/lib/components/Image.svelte b/web/src/lib/components/Image.svelte new file mode 100644 index 0000000000..801a466ca8 --- /dev/null +++ b/web/src/lib/components/Image.svelte @@ -0,0 +1,54 @@ + + +{#if capturedSource} + {#key capturedSource} + + {/key} +{/if} diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte index 5fba526521..7c3e2facb4 100644 --- a/web/src/lib/components/album-page/albums-list.svelte +++ b/web/src/lib/components/album-page/albums-list.svelte @@ -46,6 +46,7 @@ userSettings, allowEdit = false, showOwner = false, + // eslint-disable-next-line no-useless-assignment albumGroupIds = $bindable([]), empty, }: Props = $props(); diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index b09c663aaf..21077c63ae 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -223,7 +223,7 @@ } void tracker.invoke(async () => { - let hasNext = false; + let hasNext: boolean; if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) { hasNext = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next(); diff --git a/web/src/lib/components/asset-viewer/editor/transform-tool/transform-tool.svelte b/web/src/lib/components/asset-viewer/editor/transform-tool/transform-tool.svelte index 62c5c771a9..ed9f854bb9 100644 --- a/web/src/lib/components/asset-viewer/editor/transform-tool/transform-tool.svelte +++ b/web/src/lib/components/asset-viewer/editor/transform-tool/transform-tool.svelte @@ -42,16 +42,7 @@ } function ratioSelected(ratio: AspectRatioOption): boolean { - let currentRatioRotated; - if (ratio.value === 'original') { - const { width, height } = transformManager.cropImageSize; - // Account for rotation when comparing to original - if (isRotated) { - currentRatioRotated = `${height}:${width}`; - } - currentRatioRotated = `${width}:${height}`; - } - currentRatioRotated = rotatedRatio(ratio); + const currentRatioRotated = rotatedRatio(ratio); return transformManager.cropAspectRatio === currentRatioRotated; } diff --git a/web/src/lib/components/assets/broken-asset.svelte b/web/src/lib/components/assets/broken-asset.svelte index a15a787e64..f66e80ef6d 100644 --- a/web/src/lib/components/assets/broken-asset.svelte +++ b/web/src/lib/components/assets/broken-asset.svelte @@ -2,9 +2,10 @@ import { Icon } from '@immich/ui'; import { mdiImageBrokenVariant } from '@mdi/js'; import { t } from 'svelte-i18n'; + import type { ClassValue } from 'svelte/elements'; interface Props { - class?: string; + class?: ClassValue; hideMessage?: boolean; width?: string | undefined; height?: string | undefined; @@ -14,7 +15,10 @@
diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.spec.ts b/web/src/lib/components/assets/thumbnail/image-thumbnail.spec.ts new file mode 100644 index 0000000000..04835e9209 --- /dev/null +++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.spec.ts @@ -0,0 +1,89 @@ +import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte'; +import { fireEvent, render } from '@testing-library/svelte'; + +vi.mock('$lib/utils/sw-messaging', () => ({ + cancelImageUrl: vi.fn(), +})); + +describe('ImageThumbnail component', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders an img element with correct attributes', () => { + const { baseElement } = render(ImageThumbnail, { + url: '/test-thumbnail.jpg', + altText: 'Test image', + widthStyle: '200px', + }); + const img = baseElement.querySelector('img'); + expect(img).not.toBeNull(); + expect(img!.getAttribute('src')).toBe('/test-thumbnail.jpg'); + expect(img!.getAttribute('alt')).toBe(''); + }); + + it('shows BrokenAsset on error', async () => { + const { baseElement } = render(ImageThumbnail, { + url: '/test-thumbnail.jpg', + altText: 'Test image', + widthStyle: '200px', + }); + const img = baseElement.querySelector('img')!; + await fireEvent.error(img); + + expect(baseElement.querySelector('img')).toBeNull(); + expect(baseElement.querySelector('span')?.textContent).toEqual('error_loading_image'); + }); + + it('calls onComplete with false on successful load', async () => { + const onComplete = vi.fn(); + const { baseElement } = render(ImageThumbnail, { + url: '/test-thumbnail.jpg', + altText: 'Test image', + widthStyle: '200px', + onComplete, + }); + const img = baseElement.querySelector('img')!; + await fireEvent.load(img); + expect(onComplete).toHaveBeenCalledWith(false); + }); + + it('calls onComplete with true on error', async () => { + const onComplete = vi.fn(); + const { baseElement } = render(ImageThumbnail, { + url: '/test-thumbnail.jpg', + altText: 'Test image', + widthStyle: '200px', + onComplete, + }); + const img = baseElement.querySelector('img')!; + await fireEvent.error(img); + expect(onComplete).toHaveBeenCalledWith(true); + }); + + it('applies hidden styles when hidden is true', () => { + const { baseElement } = render(ImageThumbnail, { + url: '/test-thumbnail.jpg', + altText: 'Test image', + widthStyle: '200px', + hidden: true, + }); + const img = baseElement.querySelector('img')!; + const style = img.getAttribute('style') ?? ''; + expect(style).toContain('grayscale'); + expect(style).toContain('opacity'); + }); + + it('sets alt text after loading', async () => { + const { baseElement } = render(ImageThumbnail, { + url: '/test-thumbnail.jpg', + altText: 'Test image', + widthStyle: '200px', + }); + const img = baseElement.querySelector('img')!; + expect(img.getAttribute('alt')).toBe(''); + + await fireEvent.load(img); + expect(img.getAttribute('alt')).toBe('Test image'); + }); +}); diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte index a1dd22f44f..a54ad911fd 100644 --- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte @@ -1,9 +1,8 @@ {#if errored} - + {:else} - {loaded {/if} diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index 5604e6f59d..2b5e9cdf93 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -196,13 +196,19 @@ document.removeEventListener('pointermove', moveHandler, true); }; }); + const backgroundColorClass = $derived.by(() => { + if (loaded && !selected) { + return 'bg-transparent'; + } + if (disabled) { + return 'bg-gray-300'; + } + return 'dark:bg-neutral-700 bg-neutral-200'; + });
- -
-
- -
- - {#if !usingMobileDevice && !disabled} -
- {/if} - - - {#if dimmed && !mouseOver} -
- {/if} - - - {#if !authManager.isSharedLink && asset.isFavorite} -
- -
- {/if} - - {#if !!assetOwner} -
-

- {assetOwner.name} -

-
- {/if} - - {#if !authManager.isSharedLink && showArchiveIcon && asset.visibility === AssetVisibility.Archive} -
- -
- {/if} - - {#if asset.isImage && asset.projectionType === ProjectionType.EQUIRECTANGULAR} -
- - - -
- {/if} - - {#if asset.isImage && asset.duration && !asset.duration.includes('0:00:00.000')} -
- - - -
- {/if} - - - {#if asset.stack && showStackedIcon} -
- -

{asset.stack.assetCount.toLocaleString($locale)}

- -
-
- {/if} -
- - - {#if !usingMobileDevice && mouseOver && !disableLinkMouseOver} - evt.preventDefault()} - tabindex={-1} - aria-label="Thumbnail URL" - > - - {/if} - ((loaded = true), (thumbError = errored))} /> {#if asset.isVideo} -
+
{:else if asset.isImage && asset.livePhotoVideoId} -
+
{/if} + + +
+ + {#if !usingMobileDevice && !disabled} +
+ {/if} + + + {#if dimmed && !mouseOver} +
+ {/if} + + + {#if !authManager.isSharedLink && asset.isFavorite} +
+ +
+ {/if} + + {#if !!assetOwner} +
+

+ {assetOwner.name} +

+
+ {/if} + + {#if !authManager.isSharedLink && showArchiveIcon && asset.visibility === AssetVisibility.Archive} +
+ +
+ {/if} + + {#if asset.isImage && asset.projectionType === ProjectionType.EQUIRECTANGULAR} +
+ + + +
+ {/if} + + {#if asset.isImage && asset.duration && !asset.duration.includes('0:00:00.000')} +
+ + + +
+ {/if} + + + {#if asset.stack && showStackedIcon} +
+ +

{asset.stack.assetCount.toLocaleString($locale)}

+ +
+
+ {/if} +
+ + + {#if !usingMobileDevice && mouseOver && !disableLinkMouseOver} + evt.preventDefault()} + tabindex={-1} + aria-label="Thumbnail URL" + > + + {/if}
{#if selectionCandidate}
@@ -411,7 +418,7 @@
- - diff --git a/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte index 222fa7a8ec..28b7ef62ff 100644 --- a/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte @@ -2,6 +2,7 @@ import { Icon, LoadingSpinner } from '@immich/ui'; import { mdiAlertCircleOutline, mdiPauseCircleOutline, mdiPlayCircleOutline } from '@mdi/js'; import { Duration } from 'luxon'; + import type { ClassValue } from 'svelte/elements'; interface Props { url: string; @@ -12,6 +13,7 @@ curve?: boolean; playIcon?: string; pauseIcon?: string; + class?: ClassValue; } let { @@ -23,6 +25,7 @@ curve = false, playIcon = mdiPlayCircleOutline, pauseIcon = mdiPauseCircleOutline, + class: className = undefined, }: Props = $props(); let remainingSeconds = $state(durationInSeconds); @@ -57,7 +60,7 @@ {#if enablePlayback}