diff --git a/.devcontainer/server/container-common.sh b/.devcontainer/server/container-common.sh index e1e81a9b52..544674e169 100755 --- a/.devcontainer/server/container-common.sh +++ b/.devcontainer/server/container-common.sh @@ -74,7 +74,7 @@ install_dependencies() { ( cd "${IMMICH_WORKSPACE}" || exit 1 export CI=1 FROZEN=1 OFFLINE=1 - run_cmd make setup-dev + run_cmd make setup-web-dev setup-server-dev ) log "" } diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index a048536b2f..ca24e33b52 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -122,17 +122,17 @@ jobs: IS_MAIN: ${{ github.ref == 'refs/heads/main' }} run: | if [[ $IS_MAIN == 'true' ]]; then - flutter build apk --release - flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64 + flutter build apk --release --flavor production + flutter build apk --release --flavor production --split-per-abi --target-platform android-arm,android-arm64,android-x64 else - flutter build apk --debug --split-per-abi --target-platform android-arm64 + flutter build apk --debug --flavor production --split-per-abi --target-platform android-arm64 fi - name: Publish Android Artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: release-apk-signed - path: mobile/build/app/outputs/flutter-apk/*.apk + path: mobile/build/app/outputs/flutter-apk/**/*.apk - name: Save Gradle Cache id: cache-gradle-save diff --git a/.github/workflows/org-checks.yml b/.github/workflows/org-checks.yml index 55c5dfad5b..9781dc3b83 100644 --- a/.github/workflows/org-checks.yml +++ b/.github/workflows/org-checks.yml @@ -2,6 +2,7 @@ name: Org Checks on: pull_request_review: + pull_request: jobs: check-approvals: diff --git a/.vscode/launch.json b/.vscode/launch.json index ed3da9f667..8682376cda 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,6 +18,25 @@ "name": "Immich Workers", "remoteRoot": "/usr/src/app", "localRoot": "${workspaceFolder}/server" + }, + { + "name": "Flavor - Production", + "request": "launch", + "type": "dart", + "codeLens": { + "for": [ + "run-test", + "run-test-file", + "run-file", + "debug-test", + "debug-test-file", + "debug-file", + ], + "title": "${debugType}", + }, + "args": [ + "--flavor", "production" + ], } ] } diff --git a/Makefile b/Makefile index 207665d31c..815d1a153b 100644 --- a/Makefile +++ b/Makefile @@ -89,7 +89,7 @@ test-medium-dev: docker exec -it immich_server /bin/sh -c "npm run test:medium" build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ; -install-all: $(foreach M,$(MODULES),install-$M) ; +install-all: $(foreach M,$(MODULES),install-$M) ; ci-all: $(foreach M,$(filter-out .github,$(MODULES)),ci-$M) ; check-all: $(foreach M,$(filter-out sdk cli docs .github,$(MODULES)),check-$M) ; lint-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),lint-$M) ; @@ -106,4 +106,5 @@ clean: command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true -setup-dev: install-server install-sdk build-sdk install-web \ No newline at end of file +setup-server-dev: install-server +setup-web-dev: install-sdk build-sdk install-web diff --git a/cli/package-lock.json b/cli/package-lock.json index 0bb42db17c..d5884841cd 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -42,7 +42,7 @@ "prettier-plugin-organize-imports": "^4.0.0", "typescript": "^5.3.3", "typescript-eslint": "^8.28.0", - "vite": "^6.0.0", + "vite": "^7.0.0", "vite-tsconfig-paths": "^5.0.0", "vitest": "^3.0.0", "vitest-fetch-mock": "^0.4.0", @@ -3466,9 +3466,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -3486,7 +3486,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4196,24 +4196,24 @@ } }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz", + "integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.4", + "fdir": "^6.4.6", "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -4222,14 +4222,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", - "less": "*", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -4314,9 +4314,9 @@ } }, "node_modules/vite/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { diff --git a/cli/package.json b/cli/package.json index ca4ada2f65..e1f49475c0 100644 --- a/cli/package.json +++ b/cli/package.json @@ -36,7 +36,7 @@ "prettier-plugin-organize-imports": "^4.0.0", "typescript": "^5.3.3", "typescript-eslint": "^8.28.0", - "vite": "^6.0.0", + "vite": "^7.0.0", "vite-tsconfig-paths": "^5.0.0", "vitest": "^3.0.0", "vitest-fetch-mock": "^0.4.0", diff --git a/docs/docs/administration/img/admin-nightly-tasks.webp b/docs/docs/administration/img/admin-nightly-tasks.webp new file mode 100644 index 0000000000..b3d8f13cb6 Binary files /dev/null and b/docs/docs/administration/img/admin-nightly-tasks.webp differ diff --git a/docs/docs/administration/jobs-workers.md b/docs/docs/administration/jobs-workers.md index 75f97de982..4634151b9a 100644 --- a/docs/docs/administration/jobs-workers.md +++ b/docs/docs/administration/jobs-workers.md @@ -46,6 +46,12 @@ services: When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page. -Additionally, some jobs run on a schedule, which is every night at midnight. This schedule, with the exception of [External Libraries](/docs/features/libraries) scanning, cannot be changed. - + +Additionally, some jobs (such as memories generation) run on a schedule, which is every night at midnight by default. To change when they run or enable/disable a job navigate to System Settings -> [Nightly Tasks Settings](https://my.immich.app/admin/system-settings?isOpen=nightly-tasks). + + + +:::note +Some jobs ([External Libraries](/docs/features/libraries) scanning, Database Dump) are configured in their own sections in System Settings. +::: diff --git a/docs/docs/guides/external-library.md b/docs/docs/guides/external-library.md index 2ac917f930..3ad1679423 100644 --- a/docs/docs/guides/external-library.md +++ b/docs/docs/guides/external-library.md @@ -41,7 +41,7 @@ In the Immich web UI: - Click Add path -- Enter **/usr/src/app/external** as the path and click Add +- Enter **/home/user/photos1** as the path and click Add - Save the new path diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 2987223fe0..781d9c28a0 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -14,6 +14,7 @@ "@immich/cli": "file:../cli", "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", + "@socket.io/component-emitter": "^3.1.2", "@types/luxon": "^3.4.2", "@types/node": "^22.15.33", "@types/oidc-provider": "^9.0.0", diff --git a/e2e/package.json b/e2e/package.json index 4669ca0a1a..24b97bb4b7 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -24,6 +24,7 @@ "@immich/cli": "file:../cli", "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", + "@socket.io/component-emitter": "^3.1.2", "@types/luxon": "^3.4.2", "@types/node": "^22.15.33", "@types/oidc-provider": "^9.0.0", diff --git a/e2e/src/api/specs/activity.e2e-spec.ts b/e2e/src/api/specs/activity.e2e-spec.ts index 70c32313f1..9ce9b4b916 100644 --- a/e2e/src/api/specs/activity.e2e-spec.ts +++ b/e2e/src/api/specs/activity.e2e-spec.ts @@ -7,6 +7,7 @@ import { ReactionType, createActivity as create, createAlbum, + removeAssetFromAlbum, } from '@immich/sdk'; import { createUserDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; @@ -342,5 +343,36 @@ describe('/activities', () => { expect(status).toBe(204); }); + + it('should return empty list when asset is removed', async () => { + const album3 = await createAlbum( + { + createAlbumDto: { + albumName: 'Album 3', + assetIds: [asset.id], + }, + }, + { headers: asBearerAuth(admin.accessToken) }, + ); + + await createActivity({ albumId: album3.id, assetId: asset.id, type: ReactionType.Like }); + + await removeAssetFromAlbum( + { + id: album3.id, + bulkIdsDto: { + ids: [asset.id], + }, + }, + { headers: asBearerAuth(admin.accessToken) }, + ); + + const { status, body } = await request(app) + .get('/activities') + .query({ albumId: album.id }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toEqual(200); + expect(body).toEqual([]); + }); }); }); diff --git a/e2e/src/api/specs/api-key.e2e-spec.ts b/e2e/src/api/specs/api-key.e2e-spec.ts index ad03571869..28d134a664 100644 --- a/e2e/src/api/specs/api-key.e2e-spec.ts +++ b/e2e/src/api/specs/api-key.e2e-spec.ts @@ -20,7 +20,7 @@ describe('/api-keys', () => { }); beforeEach(async () => { - await utils.resetDatabase(['api_keys']); + await utils.resetDatabase(['api_key']); }); describe('POST /api-keys', () => { diff --git a/e2e/src/api/specs/system-config.e2e-spec.ts b/e2e/src/api/specs/system-config.e2e-spec.ts index 060163d7c9..1bd7bdc489 100644 --- a/e2e/src/api/specs/system-config.e2e-spec.ts +++ b/e2e/src/api/specs/system-config.e2e-spec.ts @@ -15,12 +15,6 @@ describe('/system-config', () => { }); describe('PUT /system-config', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put('/system-config'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should always return the new config', async () => { const config = await getSystemConfig(admin.accessToken); diff --git a/e2e/src/api/specs/tag.e2e-spec.ts b/e2e/src/api/specs/tag.e2e-spec.ts index a4cbc99ed3..7b645f8bd4 100644 --- a/e2e/src/api/specs/tag.e2e-spec.ts +++ b/e2e/src/api/specs/tag.e2e-spec.ts @@ -37,7 +37,7 @@ describe('/tags', () => { beforeEach(async () => { // tagging assets eventually triggers metadata extraction which can impact other tests await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); - await utils.resetDatabase(['tags']); + await utils.resetDatabase(['tag']); }); describe('POST /tags', () => { diff --git a/e2e/src/cli/specs/upload.e2e-spec.ts b/e2e/src/cli/specs/upload.e2e-spec.ts index c901a299ab..8249b9b360 100644 --- a/e2e/src/cli/specs/upload.e2e-spec.ts +++ b/e2e/src/cli/specs/upload.e2e-spec.ts @@ -97,7 +97,7 @@ describe(`immich upload`, () => { }); beforeEach(async () => { - await utils.resetDatabase(['assets', 'albums']); + await utils.resetDatabase(['asset', 'album']); }); describe(`immich upload /path/to/file.jpg`, () => { diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts index bb6d17a248..b14aedf895 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -116,6 +116,7 @@ export const deviceDto = { createdAt: expect.any(String), updatedAt: expect.any(String), current: true, + isPendingSyncReset: false, deviceOS: '', deviceType: '', }, diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index cf9eafbf23..3fcc4ab552 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -154,19 +154,19 @@ export const utils = { tables = tables || [ // TODO e2e test for deleting a stack, since it is quite complex - 'asset_stack', - 'libraries', - 'shared_links', + 'stack', + 'library', + 'shared_link', 'person', - 'albums', - 'assets', - 'asset_faces', + 'album', + 'asset', + 'asset_face', 'activity', - 'api_keys', - 'sessions', - 'users', + 'api_key', + 'session', + 'user', 'system_metadata', - 'tags', + 'tag', ]; const sql: string[] = []; @@ -175,7 +175,7 @@ export const utils = { if (table === 'system_metadata') { sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`); } else { - sql.push(`DELETE FROM ${table} CASCADE;`); + sql.push(`DELETE FROM "${table}" CASCADE;`); } } @@ -451,7 +451,7 @@ export const utils = { return; } - await client.query('INSERT INTO asset_faces ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]); + await client.query('INSERT INTO asset_face ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]); }, setPersonThumbnail: async (personId: string) => { diff --git a/i18n/en.json b/i18n/en.json index 64e9fce58b..7f7dae833d 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -166,6 +166,20 @@ "metadata_settings_description": "Manage metadata settings", "migration_job": "Migration", "migration_job_description": "Migrate thumbnails for assets and faces to the latest folder structure", + "nightly_tasks_cluster_faces_setting_description": "Run facial recognition on newly detected faces", + "nightly_tasks_cluster_new_faces_setting": "Cluster new faces", + "nightly_tasks_database_cleanup_setting": "Database cleanup tasks", + "nightly_tasks_database_cleanup_setting_description": "Clean up old, expired data from the database", + "nightly_tasks_generate_memories_setting": "Generate memories", + "nightly_tasks_generate_memories_setting_description": "Create new memories from assets", + "nightly_tasks_missing_thumbnails_setting": "Generate missing thumbnails", + "nightly_tasks_missing_thumbnails_setting_description": "Queue assets without thumbnails for thumbnail generation", + "nightly_tasks_settings": "Nightly Tasks Settings", + "nightly_tasks_settings_description": "Manage nightly tasks", + "nightly_tasks_start_time_setting": "Start time", + "nightly_tasks_start_time_setting_description": "The time at which the server starts running the nightly tasks", + "nightly_tasks_sync_quota_usage_setting": "Sync quota usage", + "nightly_tasks_sync_quota_usage_setting_description": "Update user storage quota, based on current usage", "no_paths_added": "No paths added", "no_pattern_added": "No pattern added", "note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the", @@ -1502,6 +1516,7 @@ "remove_custom_date_range": "Remove custom date range", "remove_deleted_assets": "Remove Deleted Assets", "remove_from_album": "Remove from album", + "remove_from_album_action_prompt": "{count} removed from the album", "remove_from_favorites": "Remove from favorites", "remove_from_lock_folder_action_prompt": "{count} removed from the locked folder", "remove_from_locked_folder": "Remove from locked folder", diff --git a/mobile/.fvmrc b/mobile/.fvmrc index b987073ac6..7d2a608dfa 100644 --- a/mobile/.fvmrc +++ b/mobile/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.29.3" + "flutter": "3.32.6" } \ No newline at end of file diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json index 9c5244f098..c1b3fc9ce3 100644 --- a/mobile/.vscode/settings.json +++ b/mobile/.vscode/settings.json @@ -1,5 +1,5 @@ { - "dart.flutterSdkPath": ".fvm/versions/3.29.3", + "dart.flutterSdkPath": ".fvm/versions/3.32.6", "search.exclude": { "**/.fvm": true }, diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 0d07228252..870e424461 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -66,6 +66,20 @@ android { } } + flavorDimensions "default" + productFlavors { + production { + dimension "default" + applicationId "app.alextran.immich" + } + + beta { + dimension "default" + applicationId "app.alextran.immich.beta" + versionNameSuffix "-BETA" + } + } + buildTypes { debug { applicationIdSuffix '.debug' diff --git a/mobile/android/app/src/beta/AndroidManifest.xml b/mobile/android/app/src/beta/AndroidManifest.xml new file mode 100644 index 0000000000..d4f7b5bc80 --- /dev/null +++ b/mobile/android/app/src/beta/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 6a96aa68e4..cf3b7ee719 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -100,24 +100,24 @@ - - - + + + - + - - - - + + + + diff --git a/mobile/immich_lint/pubspec.lock b/mobile/immich_lint/pubspec.lock index e05cbb3db3..0e4b08be87 100644 --- a/mobile/immich_lint/pubspec.lock +++ b/mobile/immich_lint/pubspec.lock @@ -5,34 +5,34 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 + sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f url: "https://pub.dev" source: hosted - version: "80.0.0" + version: "82.0.0" analyzer: dependency: "direct main" description: name: analyzer - sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" + sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.4.5" analyzer_plugin: dependency: "direct main" description: name: analyzer_plugin - sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4 + sha256: ee188b6df6c85f1441497c7171c84f1392affadc0384f71089cb10a3bc508cef url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.13.1" args: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" async: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" ci: dependency: transitive description: @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: custom_lint_visitor - sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8" + sha256: cba5b6d7a6217312472bf4468cdf68c949488aed7ffb0eab792cd0b6c435054d url: "https://pub.dev" source: hosted - version: "1.0.0+7.3.0" + version: "1.0.0+7.4.5" dart_style: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.0" glob: dependency: "direct main" description: @@ -213,18 +213,18 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" package_config: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" path: dependency: transitive description: @@ -317,10 +317,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" typed_data: dependency: transitive description: @@ -341,18 +341,18 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "15.0.2" watcher: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" yaml: dependency: transitive description: @@ -362,4 +362,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0-0 <4.0.0" + dart: ">=3.8.0 <4.0.0" diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index fb0908e8b6..1a39f98db3 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ @@ -117,8 +117,6 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); path = Sync; sourceTree = ""; }; @@ -473,10 +471,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -505,10 +507,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; diff --git a/mobile/ios/WidgetExtension/Info.plist b/mobile/ios/WidgetExtension/Info.plist index 0f118fb75e..d4e598ee31 100644 --- a/mobile/ios/WidgetExtension/Info.plist +++ b/mobile/ios/WidgetExtension/Info.plist @@ -2,6 +2,11 @@ + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSExtension NSExtensionPointIdentifier diff --git a/mobile/lib/domain/models/album/album.model.dart b/mobile/lib/domain/models/album/album.model.dart index 29f75f29ef..7cafca9116 100644 --- a/mobile/lib/domain/models/album/album.model.dart +++ b/mobile/lib/domain/models/album/album.model.dart @@ -11,7 +11,7 @@ enum AlbumUserRole { } // Model for an album stored in the server -class Album { +class RemoteAlbum { final String id; final String name; final String ownerId; @@ -24,7 +24,7 @@ class Album { final int assetCount; final String ownerName; - const Album({ + const RemoteAlbum({ required this.id, required this.name, required this.ownerId, @@ -57,7 +57,7 @@ class Album { @override bool operator ==(Object other) { - if (other is! Album) return false; + if (other is! RemoteAlbum) return false; if (identical(this, other)) return true; return id == other.id && name == other.name && diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index f3106470d2..ae9e8b5336 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -1,33 +1,35 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; +import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/utils/remote_album.utils.dart'; class RemoteAlbumService { final DriftRemoteAlbumRepository _repository; + final DriftAlbumApiRepository _albumApiRepository; - const RemoteAlbumService(this._repository); + const RemoteAlbumService(this._repository, this._albumApiRepository); - Future> getAll() { + Future> getAll() { return _repository.getAll(); } - List sortAlbums( - List albums, + List sortAlbums( + List albums, RemoteAlbumSortMode sortMode, { bool isReverse = false, }) { return sortMode.sortFn(albums, isReverse); } - List searchAlbums( - List albums, + List searchAlbums( + List albums, String query, String? userId, [ QuickFilterMode filterMode = QuickFilterMode.all, ]) { final lowerQuery = query.toLowerCase(); - List filtered = albums; + List filtered = albums; // Apply text search filter if (query.isNotEmpty) { @@ -57,4 +59,20 @@ class RemoteAlbumService { return filtered; } + + Future createAlbum({ + required String title, + required List assetIds, + String? description, + }) async { + final album = await _albumApiRepository.createDriftAlbum( + title, + description: description, + assetIds: assetIds, + ); + + await _repository.create(album, assetIds); + + return album; + } } diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index ee0ec6c44f..29066195f2 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; +import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; @@ -23,7 +24,12 @@ class SyncStreamService { bool get isCancelled => _cancelChecker?.call() ?? false; - Future sync() => _syncApiRepository.streamChanges(_handleEvents); + Future sync() { + _logger.info("Remote sync request for userr"); + DLog.log("Remote sync request for user"); + // Start the sync stream and handle events + return _syncApiRepository.streamChanges(_handleEvents); + } Future _handleEvents(List events, Function() abort) async { List items = []; diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index b3d6b51402..8204572547 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -18,6 +18,11 @@ typedef TimelineAssetSource = Future> Function( typedef TimelineBucketSource = Stream> Function(); +typedef TimelineQuery = ({ + TimelineAssetSource assetSource, + TimelineBucketSource bucketSource, +}); + class TimelineFactory { final DriftTimelineRepository _timelineRepository; final SettingsService _settingsService; @@ -31,78 +36,32 @@ class TimelineFactory { GroupAssetsBy get groupBy => GroupAssetsBy.values[_settingsService.get(Setting.groupAssetsBy)]; - TimelineService main(List timelineUsers) => TimelineService( - assetSource: (offset, count) => _timelineRepository - .getMainBucketAssets(timelineUsers, offset: offset, count: count), - bucketSource: () => _timelineRepository.watchMainBucket( - timelineUsers, - groupBy: groupBy, - ), - ); + TimelineService main(List timelineUsers) => + TimelineService(_timelineRepository.main(timelineUsers, groupBy)); - TimelineService localAlbum({required String albumId}) => TimelineService( - assetSource: (offset, count) => _timelineRepository - .getLocalAlbumBucketAssets(albumId, offset: offset, count: count), - bucketSource: () => _timelineRepository.watchLocalAlbumBucket( - albumId, - groupBy: groupBy, - ), - ); + TimelineService localAlbum({required String albumId}) => + TimelineService(_timelineRepository.localAlbum(albumId, groupBy)); - TimelineService remoteAlbum({required String albumId}) => TimelineService( - assetSource: (offset, count) => _timelineRepository - .getRemoteAlbumBucketAssets(albumId, offset: offset, count: count), - bucketSource: () => _timelineRepository.watchRemoteAlbumBucket( - albumId, - groupBy: groupBy, - ), - ); + TimelineService remoteAlbum({required String albumId}) => + TimelineService(_timelineRepository.remoteAlbum(albumId, groupBy)); - TimelineService remoteAssets(String ownerId) => TimelineService( - assetSource: (offset, count) => _timelineRepository - .getRemoteBucketAssets(ownerId, offset: offset, count: count), - bucketSource: () => _timelineRepository.watchRemoteBucket( - ownerId, - groupBy: GroupAssetsBy.month, - ), - ); + TimelineService remoteAssets(String userId) => + TimelineService(_timelineRepository.remote(userId, groupBy)); - TimelineService favorite(String userId) => TimelineService( - assetSource: (offset, count) => _timelineRepository - .getFavoriteBucketAssets(userId, offset: offset, count: count), - bucketSource: () => - _timelineRepository.watchFavoriteBucket(userId, groupBy: groupBy), - ); + TimelineService favorite(String userId) => + TimelineService(_timelineRepository.favorite(userId, groupBy)); - TimelineService trash(String userId) => TimelineService( - assetSource: (offset, count) => _timelineRepository - .getTrashBucketAssets(userId, offset: offset, count: count), - bucketSource: () => - _timelineRepository.watchTrashBucket(userId, groupBy: groupBy), - ); + TimelineService trash(String userId) => + TimelineService(_timelineRepository.trash(userId, groupBy)); - TimelineService archive(String userId) => TimelineService( - assetSource: (offset, count) => _timelineRepository - .getArchiveBucketAssets(userId, offset: offset, count: count), - bucketSource: () => - _timelineRepository.watchArchiveBucket(userId, groupBy: groupBy), - ); + TimelineService archive(String userId) => + TimelineService(_timelineRepository.archived(userId, groupBy)); - TimelineService lockedFolder(String userId) => TimelineService( - assetSource: (offset, count) => _timelineRepository - .getLockedFolderBucketAssets(userId, offset: offset, count: count), - bucketSource: () => _timelineRepository.watchLockedFolderBucket( - userId, - groupBy: groupBy, - ), - ); + TimelineService lockedFolder(String userId) => + TimelineService(_timelineRepository.locked(userId, groupBy)); - TimelineService video(String userId) => TimelineService( - assetSource: (offset, count) => _timelineRepository - .getVideoBucketAssets(userId, offset: offset, count: count), - bucketSource: () => - _timelineRepository.watchVideoBucket(userId, groupBy: groupBy), - ); + TimelineService video(String userId) => + TimelineService(_timelineRepository.video(userId, groupBy)); } class TimelineService { @@ -116,7 +75,13 @@ class TimelineService { int _totalAssets = 0; int get totalAssets => _totalAssets; - TimelineService({ + TimelineService(TimelineQuery query) + : this._( + assetSource: query.assetSource, + bucketSource: query.bucketSource, + ); + + TimelineService._({ required TimelineAssetSource assetSource, required TimelineBucketSource bucketSource, }) : _assetSource = assetSource, @@ -210,6 +175,9 @@ class TimelineService { Future preCacheAssets(int index) => _mutex.run(() => _loadAssets(index, math.min(5, _totalAssets - index))); + BaseAsset getRandomAsset() => + _buffer.elementAt(math.Random().nextInt(_buffer.length)); + BaseAsset getAsset(int index) { if (!hasRange(index, 1)) { throw RangeError( diff --git a/mobile/lib/extensions/scroll_extensions.dart b/mobile/lib/extensions/scroll_extensions.dart index 5bbd73163a..2752d0b77a 100644 --- a/mobile/lib/extensions/scroll_extensions.dart +++ b/mobile/lib/extensions/scroll_extensions.dart @@ -11,9 +11,9 @@ class FastScrollPhysics extends ScrollPhysics { @override SpringDescription get spring => const SpringDescription( - mass: 40, - stiffness: 100, - damping: 1, + mass: 1, + stiffness: 402.49984375, + damping: 40, ); } @@ -31,8 +31,8 @@ class FastClampingScrollPhysics extends ClampingScrollPhysics { // can briefly be seen and cause a flicker effect if the video begins to initialize // before the animation finishes - probably a bug in PhotoViewGallery's animation handling // Making the animation faster is not just stylistic, but also helps to avoid this flicker - mass: 80, - stiffness: 100, - damping: 1, + mass: 1, + stiffness: 1601.2499609375, + damping: 80, ); } diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index b1b73b4cdc..b77184bce0 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -1,15 +1,18 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/album/album.model.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/repositories/db.repository.dart'; -enum SortRemoteAlbumsBy { id } +enum SortRemoteAlbumsBy { id, updatedAt } class DriftRemoteAlbumRepository extends DriftDatabaseRepository { final Drift _db; const DriftRemoteAlbumRepository(this._db) : super(_db); - Future> getAll({Set sortBy = const {}}) { + Future> getAll({ + Set sortBy = const {SortRemoteAlbumsBy.updatedAt}, + }) { final assetCount = _db.remoteAlbumAssetEntity.assetId.count(); final query = _db.remoteAlbumEntity.select().join([ @@ -41,6 +44,8 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { orderings.add( switch (sort) { SortRemoteAlbumsBy.id => OrderingTerm.asc(_db.remoteAlbumEntity.id), + SortRemoteAlbumsBy.updatedAt => + OrderingTerm.desc(_db.remoteAlbumEntity.updatedAt), }, ); } @@ -56,11 +61,54 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { ) .get(); } + + Future create( + RemoteAlbum album, + List assetIds, + ) async { + await _db.transaction(() async { + final entity = RemoteAlbumEntityCompanion( + id: Value(album.id), + name: Value(album.name), + ownerId: Value(album.ownerId), + createdAt: Value(album.createdAt), + updatedAt: Value(album.updatedAt), + description: Value(album.description), + thumbnailAssetId: Value(album.thumbnailAssetId), + isActivityEnabled: Value(album.isActivityEnabled), + order: Value(album.order), + ); + + await _db.remoteAlbumEntity.insertOne(entity); + + if (assetIds.isNotEmpty) { + final albumAssets = assetIds.map( + (assetId) => RemoteAlbumAssetEntityCompanion( + albumId: Value(album.id), + assetId: Value(assetId), + ), + ); + + await _db.batch((batch) { + batch.insertAll( + _db.remoteAlbumAssetEntity, + albumAssets, + ); + }); + } + }); + } + + Future removeAssets(String albumId, List assetIds) { + return _db.remoteAlbumAssetEntity.deleteWhere( + (tbl) => tbl.albumId.equals(albumId) & tbl.assetId.isIn(assetIds), + ); + } } extension on RemoteAlbumEntityData { - Album toDto({int assetCount = 0, required String ownerName}) { - return Album( + RemoteAlbum toDto({int assetCount = 0, required String ownerName}) { + return RemoteAlbum( id: id, name: name, ownerId: ownerId, diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 569df0aed8..76ad9bad8f 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -5,8 +5,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -30,19 +32,19 @@ class DriftTimelineRepository extends DriftDatabaseRepository { .map((users) => users..add(userId)); } - List _generateBuckets(int count) { - final numBuckets = (count / kTimelineNoneSegmentSize).floor(); - final buckets = List.generate( - numBuckets, - (_) => const Bucket(assetCount: kTimelineNoneSegmentSize), - ); - if (count % kTimelineNoneSegmentSize != 0) { - buckets.add(Bucket(assetCount: count % kTimelineNoneSegmentSize)); - } - return buckets; - } + TimelineQuery main(List userIds, GroupAssetsBy groupBy) => ( + bucketSource: () => _watchMainBucket( + userIds, + groupBy: groupBy, + ), + assetSource: (offset, count) => _getMainBucketAssets( + userIds, + offset: offset, + count: count, + ), + ); - Stream> watchMainBucket( + Stream> _watchMainBucket( List userIds, { GroupAssetsBy groupBy = GroupAssetsBy.day, }) { @@ -62,7 +64,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { .throttle(const Duration(seconds: 3), trailing: true); } - Future> getMainBucketAssets( + Future> _getMainBucketAssets( List userIds, { required int offset, required int count, @@ -70,42 +72,53 @@ class DriftTimelineRepository extends DriftDatabaseRepository { return _db.mergedAssetDrift .mergedAsset(userIds, limit: Limit(count, offset)) .map( - (row) { - return row.remoteId != null && row.ownerId != null - ? RemoteAsset( - id: row.remoteId!, - localId: row.localId, - name: row.name, - ownerId: row.ownerId!, - checksum: row.checksum, - type: row.type, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - thumbHash: row.thumbHash, - width: row.width, - height: row.height, - isFavorite: row.isFavorite, - durationInSeconds: row.durationInSeconds, - ) - : LocalAsset( - id: row.localId!, - remoteId: row.remoteId, - name: row.name, - checksum: row.checksum, - type: row.type, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - width: row.width, - height: row.height, - isFavorite: row.isFavorite, - durationInSeconds: row.durationInSeconds, - orientation: row.orientation, - ); - }, - ).get(); + (row) => row.remoteId != null && row.ownerId != null + ? RemoteAsset( + id: row.remoteId!, + localId: row.localId, + name: row.name, + ownerId: row.ownerId!, + checksum: row.checksum, + type: row.type, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + thumbHash: row.thumbHash, + width: row.width, + height: row.height, + isFavorite: row.isFavorite, + durationInSeconds: row.durationInSeconds, + ) + : LocalAsset( + id: row.localId!, + remoteId: row.remoteId, + name: row.name, + checksum: row.checksum, + type: row.type, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + width: row.width, + height: row.height, + isFavorite: row.isFavorite, + durationInSeconds: row.durationInSeconds, + orientation: row.orientation, + ), + ) + .get(); } - Stream> watchLocalAlbumBucket( + TimelineQuery localAlbum(String albumId, GroupAssetsBy groupBy) => ( + bucketSource: () => _watchLocalAlbumBucket( + albumId, + groupBy: groupBy, + ), + assetSource: (offset, count) => _getLocalAlbumBucketAssets( + albumId, + offset: offset, + count: count, + ), + ); + + Stream> _watchLocalAlbumBucket( String albumId, { GroupAssetsBy groupBy = GroupAssetsBy.day, }) { @@ -119,15 +132,14 @@ class DriftTimelineRepository extends DriftDatabaseRepository { final assetCountExp = _db.localAssetEntity.id.count(); final dateExp = _db.localAssetEntity.createdAt.dateFmt(groupBy); - final query = _db.localAssetEntity.selectOnly() + final query = _db.localAssetEntity.selectOnly().join([ + innerJoin( + _db.localAlbumAssetEntity, + _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), + useColumns: false, + ), + ]) ..addColumns([assetCountExp, dateExp]) - ..join([ - innerJoin( - _db.localAlbumAssetEntity, - _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), - useColumns: false, - ), - ]) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) ..groupBy([dateExp]) ..orderBy([OrderingTerm.desc(dateExp)]); @@ -139,7 +151,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { }).watch(); } - Future> getLocalAlbumBucketAssets( + Future> _getLocalAlbumBucketAssets( String albumId, { required int offset, required int count, @@ -156,12 +168,25 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) ..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)]) ..limit(count, offset: offset); + return query .map((row) => row.readTable(_db.localAssetEntity).toDto()) .get(); } - Stream> watchRemoteAlbumBucket( + TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => ( + bucketSource: () => _watchRemoteAlbumBucket( + albumId, + groupBy: groupBy, + ), + assetSource: (offset, count) => _getRemoteAlbumBucketAssets( + albumId, + offset: offset, + count: count, + ), + ); + + Stream> _watchRemoteAlbumBucket( String albumId, { GroupAssetsBy groupBy = GroupAssetsBy.day, }) { @@ -199,7 +224,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { }).watch(); } - Future> getRemoteAlbumBucketAssets( + Future> _getRemoteAlbumBucketAssets( String albumId, { required int offset, required int count, @@ -225,325 +250,101 @@ class DriftTimelineRepository extends DriftDatabaseRepository { .get(); } - Stream> watchRemoteBucket( - String ownerId, { - GroupAssetsBy groupBy = GroupAssetsBy.day, - }) { - if (groupBy == GroupAssetsBy.none) { - return _db.remoteAssetEntity - .count( - where: (row) => - row.deletedAt.isNull() & - row.visibility.equalsValue(AssetVisibility.timeline) & - row.ownerId.equals(ownerId), - ) - .map(_generateBuckets) - .watchSingle(); - } - - final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); - - final query = _db.remoteAssetEntity.selectOnly() - ..addColumns([assetCountExp, dateExp]) - ..where( - _db.remoteAssetEntity.deletedAt.isNull() & - _db.remoteAssetEntity.visibility - .equalsValue(AssetVisibility.timeline) & - _db.remoteAssetEntity.ownerId.equals(ownerId), - ) - ..groupBy([dateExp]) - ..orderBy([OrderingTerm.desc(dateExp)]); - - return query.map((row) { - final timeline = row.read(dateExp)!.dateFmt(groupBy); - final assetCount = row.read(assetCountExp)!; - return TimeBucket(date: timeline, assetCount: assetCount); - }).watch(); - } - - Future> getRemoteBucketAssets( - String ownerId, { - required int offset, - required int count, - }) { - final query = _db.remoteAssetEntity.select() - ..where( - (row) => + TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) => + _remoteQueryBuilder( + filter: (row) => row.deletedAt.isNull() & row.visibility.equalsValue(AssetVisibility.timeline) & row.ownerId.equals(ownerId), - ) - ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) - ..limit(count, offset: offset); + groupBy: groupBy, + ); - return query.map((row) => row.toDto()).get(); - } - - Stream> watchFavoriteBucket( - String userId, { - GroupAssetsBy groupBy = GroupAssetsBy.day, - }) { - if (groupBy == GroupAssetsBy.none) { - return _db.remoteAssetEntity - .count( - where: (row) => - row.deletedAt.isNull() & - row.isFavorite.equals(true) & - row.ownerId.equals(userId), - ) - .map(_generateBuckets) - .watchSingle(); - } - - final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); - - final query = _db.remoteAssetEntity.selectOnly() - ..addColumns([assetCountExp, dateExp]) - ..where( - _db.remoteAssetEntity.deletedAt.isNull() & - _db.remoteAssetEntity.ownerId.equals(userId) & - _db.remoteAssetEntity.isFavorite.equals(true), - ) - ..groupBy([dateExp]) - ..orderBy([OrderingTerm.desc(dateExp)]); - - return query.map((row) { - final timeline = row.read(dateExp)!.dateFmt(groupBy); - final assetCount = row.read(assetCountExp)!; - return TimeBucket(date: timeline, assetCount: assetCount); - }).watch(); - } - - Future> getFavoriteBucketAssets( - String userId, { - required int offset, - required int count, - }) { - final query = _db.remoteAssetEntity.select() - ..where( - (row) => + TimelineQuery favorite(String userId, GroupAssetsBy groupBy) => + _remoteQueryBuilder( + filter: (row) => row.deletedAt.isNull() & row.isFavorite.equals(true) & row.ownerId.equals(userId), - ) - ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) - ..limit(count, offset: offset); + groupBy: groupBy, + ); - return query.map((row) => row.toDto()).get(); - } + TimelineQuery trash(String userId, GroupAssetsBy groupBy) => + _remoteQueryBuilder( + filter: (row) => row.deletedAt.isNotNull() & row.ownerId.equals(userId), + groupBy: groupBy, + ); - Stream> watchTrashBucket( - String userId, { - GroupAssetsBy groupBy = GroupAssetsBy.day, - }) { - if (groupBy == GroupAssetsBy.none) { - return _db.remoteAssetEntity - .count( - where: (row) => - row.deletedAt.isNotNull() & row.ownerId.equals(userId), - ) - .map(_generateBuckets) - .watchSingle(); - } - - final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); - - final query = _db.remoteAssetEntity.selectOnly() - ..addColumns([assetCountExp, dateExp]) - ..where( - _db.remoteAssetEntity.ownerId.equals(userId) & - _db.remoteAssetEntity.deletedAt.isNotNull(), - ) - ..groupBy([dateExp]) - ..orderBy([OrderingTerm.desc(dateExp)]); - - return query.map((row) { - final timeline = row.read(dateExp)!.dateFmt(groupBy); - final assetCount = row.read(assetCountExp)!; - return TimeBucket(date: timeline, assetCount: assetCount); - }).watch(); - } - - Future> getTrashBucketAssets( - String userId, { - required int offset, - required int count, - }) { - final query = _db.remoteAssetEntity.select() - ..where( - (row) => row.deletedAt.isNotNull() & row.ownerId.equals(userId), - ) - ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) - ..limit(count, offset: offset); - - return query.map((row) => row.toDto()).get(); - } - - Stream> watchArchiveBucket( - String userId, { - GroupAssetsBy groupBy = GroupAssetsBy.day, - }) { - if (groupBy == GroupAssetsBy.none) { - return _db.remoteAssetEntity - .count( - where: (row) => - row.deletedAt.isNull() & - row.visibility.equalsValue(AssetVisibility.archive) & - row.ownerId.equals(userId), - ) - .map(_generateBuckets) - .watchSingle(); - } - - final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); - - final query = _db.remoteAssetEntity.selectOnly() - ..addColumns([assetCountExp, dateExp]) - ..where( - _db.remoteAssetEntity.deletedAt.isNull() & - _db.remoteAssetEntity.ownerId.equals(userId) & - _db.remoteAssetEntity.visibility - .equalsValue(AssetVisibility.archive), - ) - ..groupBy([dateExp]) - ..orderBy([OrderingTerm.desc(dateExp)]); - - return query.map((row) { - final timeline = row.read(dateExp)!.dateFmt(groupBy); - final assetCount = row.read(assetCountExp)!; - return TimeBucket(date: timeline, assetCount: assetCount); - }).watch(); - } - - Future> getArchiveBucketAssets( - String userId, { - required int offset, - required int count, - }) { - final query = _db.remoteAssetEntity.select() - ..where( - (row) => + TimelineQuery archived(String userId, GroupAssetsBy groupBy) => + _remoteQueryBuilder( + filter: (row) => row.deletedAt.isNull() & row.ownerId.equals(userId) & row.visibility.equalsValue(AssetVisibility.archive), - ) - ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) - ..limit(count, offset: offset); + groupBy: groupBy, + ); - return query.map((row) => row.toDto()).get(); - } - - Stream> watchLockedFolderBucket( - String userId, { - GroupAssetsBy groupBy = GroupAssetsBy.day, - }) { - if (groupBy == GroupAssetsBy.none) { - return _db.remoteAssetEntity - .count( - where: (row) => - row.deletedAt.isNull() & - row.visibility.equalsValue(AssetVisibility.locked) & - row.ownerId.equals(userId), - ) - .map(_generateBuckets) - .watchSingle(); - } - - final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); - - final query = _db.remoteAssetEntity.selectOnly() - ..addColumns([assetCountExp, dateExp]) - ..where( - _db.remoteAssetEntity.deletedAt.isNull() & - _db.remoteAssetEntity.ownerId.equals(userId) & - _db.remoteAssetEntity.visibility - .equalsValue(AssetVisibility.locked), - ) - ..groupBy([dateExp]) - ..orderBy([OrderingTerm.desc(dateExp)]); - - return query.map((row) { - final timeline = row.read(dateExp)!.dateFmt(groupBy); - final assetCount = row.read(assetCountExp)!; - return TimeBucket(date: timeline, assetCount: assetCount); - }).watch(); - } - - Future> getLockedFolderBucketAssets( - String userId, { - required int offset, - required int count, - }) { - final query = _db.remoteAssetEntity.select() - ..where( - (row) => + TimelineQuery locked(String userId, GroupAssetsBy groupBy) => + _remoteQueryBuilder( + filter: (row) => row.deletedAt.isNull() & row.visibility.equalsValue(AssetVisibility.locked) & row.ownerId.equals(userId), - ) - ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) - ..limit(count, offset: offset); + groupBy: groupBy, + ); - return query.map((row) => row.toDto()).get(); - } - - Stream> watchVideoBucket( - String userId, { - GroupAssetsBy groupBy = GroupAssetsBy.day, - }) { - if (groupBy == GroupAssetsBy.none) { - return _db.remoteAssetEntity - .count( - where: (row) => - row.deletedAt.isNull() & - row.type.equalsValue(AssetType.video) & - row.visibility.equalsValue(AssetVisibility.timeline) & - row.ownerId.equals(userId), - ) - .map(_generateBuckets) - .watchSingle(); - } - - final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); - - final query = _db.remoteAssetEntity.selectOnly() - ..addColumns([assetCountExp, dateExp]) - ..where( - _db.remoteAssetEntity.deletedAt.isNull() & - _db.remoteAssetEntity.ownerId.equals(userId) & - _db.remoteAssetEntity.type.equalsValue(AssetType.video) & - _db.remoteAssetEntity.visibility - .equalsValue(AssetVisibility.timeline), - ) - ..groupBy([dateExp]) - ..orderBy([OrderingTerm.desc(dateExp)]); - - return query.map((row) { - final timeline = row.read(dateExp)!.dateFmt(groupBy); - final assetCount = row.read(assetCountExp)!; - return TimeBucket(date: timeline, assetCount: assetCount); - }).watch(); - } - - Future> getVideoBucketAssets( - String userId, { - required int offset, - required int count, - }) { - final query = _db.remoteAssetEntity.select() - ..where( - (row) => + TimelineQuery video(String userId, GroupAssetsBy groupBy) => + _remoteQueryBuilder( + filter: (row) => row.deletedAt.isNull() & row.type.equalsValue(AssetType.video) & row.visibility.equalsValue(AssetVisibility.timeline) & row.ownerId.equals(userId), - ) + groupBy: groupBy, + ); + + TimelineQuery _remoteQueryBuilder({ + required Expression Function($RemoteAssetEntityTable row) filter, + GroupAssetsBy groupBy = GroupAssetsBy.day, + }) { + return ( + bucketSource: () => _watchRemoteBucket(filter: filter, groupBy: groupBy), + assetSource: (offset, count) => + _getRemoteAssets(filter: filter, offset: offset, count: count), + ); + } + + Stream> _watchRemoteBucket({ + required Expression Function($RemoteAssetEntityTable row) filter, + GroupAssetsBy groupBy = GroupAssetsBy.day, + }) { + if (groupBy == GroupAssetsBy.none) { + final query = _db.remoteAssetEntity.count(where: filter); + return query.map(_generateBuckets).watchSingle(); + } + + final assetCountExp = _db.remoteAssetEntity.id.count(); + final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); + + final query = _db.remoteAssetEntity.selectOnly() + ..addColumns([assetCountExp, dateExp]) + ..where(filter(_db.remoteAssetEntity)) + ..groupBy([dateExp]) + ..orderBy([OrderingTerm.desc(dateExp)]); + + return query.map((row) { + final timeline = row.read(dateExp)!.dateFmt(groupBy); + final assetCount = row.read(assetCountExp)!; + return TimeBucket(date: timeline, assetCount: assetCount); + }).watch(); + } + + Future> _getRemoteAssets({ + required Expression Function($RemoteAssetEntityTable row) filter, + required int offset, + required int count, + }) { + final query = _db.remoteAssetEntity.select() + ..where(filter) ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) ..limit(count, offset: offset); @@ -551,6 +352,17 @@ class DriftTimelineRepository extends DriftDatabaseRepository { } } +List _generateBuckets(int count) { + final buckets = List.generate( + (count / kTimelineNoneSegmentSize).floor(), + (_) => const Bucket(assetCount: kTimelineNoneSegmentSize), + ); + if (count % kTimelineNoneSegmentSize != 0) { + buckets.add(Bucket(assetCount: count % kTimelineNoneSegmentSize)); + } + return buckets; +} + extension on Expression { Expression dateFmt(GroupAssetsBy groupBy) { // DateTimes are stored in UTC, so we need to convert them to local time inside the query before formatting diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 1f18ecdf5a..bf79c28361 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -23,12 +23,12 @@ import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/routing/app_navigation_observer.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/background.service.dart'; +import 'package:immich_mobile/services/deep_link.service.dart'; import 'package:immich_mobile/services/local_notification.service.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/cache/widgets_binding.dart'; -import 'package:immich_mobile/services/deep_link.service.dart'; import 'package:immich_mobile/utils/download.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:immich_mobile/utils/migration.dart'; @@ -176,10 +176,13 @@ class ImmichAppState extends ConsumerState final deepLinkHandler = ref.read(deepLinkServiceProvider); final currentRouteName = ref.read(currentRouteNameProvider.notifier).state; + final isColdStart = + currentRouteName == null || currentRouteName == SplashScreenRoute.name; + if (deepLink.uri.scheme == "immich") { final proposedRoute = await deepLinkHandler.handleScheme( deepLink, - currentRouteName == SplashScreenRoute.name, + isColdStart, ); return proposedRoute; @@ -188,7 +191,7 @@ class ImmichAppState extends ConsumerState if (deepLink.uri.host == "my.immich.app") { final proposedRoute = await deepLinkHandler.handleMyImmichApp( deepLink, - currentRouteName == SplashScreenRoute.name, + isColdStart, ); return proposedRoute; @@ -250,7 +253,8 @@ class ImmichAppState extends ConsumerState ), routerConfig: router.config( deepLinkBuilder: _deepLinkBuilder, - navigatorObservers: () => [AppNavigationObserver(ref: ref)], + navigatorObservers: () => + [AppNavigationObserver(ref: ref), HeroController()], ), ), ); diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart index 0188d953dc..e713b3f8da 100644 --- a/mobile/lib/pages/common/tab_controller.page.dart +++ b/mobile/lib/pages/common/tab_controller.page.dart @@ -4,13 +4,13 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; +import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/multiselect.provider.dart'; import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/tab.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; @RoutePage() class TabControllerPage extends HookConsumerWidget { @@ -158,10 +158,6 @@ class TabControllerPage extends HookConsumerWidget { ), builder: (context, child) { final tabsRouter = AutoTabsRouter.of(context); - final heroedChild = HeroControllerScope( - controller: HeroController(), - child: child, - ); return PopScope( canPop: tabsRouter.activeIndex == 0, onPopInvokedWithResult: (didPop, _) => @@ -173,10 +169,10 @@ class TabControllerPage extends HookConsumerWidget { children: [ navigationRail(tabsRouter), const VerticalDivider(), - Expanded(child: heroedChild), + Expanded(child: child), ], ) - : heroedChild, + : child, bottomNavigationBar: multiselectEnabled || isScreenLandscape ? null : bottomNavigationBar(tabsRouter), diff --git a/mobile/lib/pages/common/tab_shell.page.dart b/mobile/lib/pages/common/tab_shell.page.dart index d5d53360b0..edaec6d336 100644 --- a/mobile/lib/pages/common/tab_shell.page.dart +++ b/mobile/lib/pages/common/tab_shell.page.dart @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; import 'package:immich_mobile/providers/tab.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; @@ -127,10 +128,6 @@ class TabShellPage extends ConsumerWidget { ), builder: (context, child) { final tabsRouter = AutoTabsRouter.of(context); - final heroedChild = HeroControllerScope( - controller: HeroController(), - child: child, - ); return PopScope( canPop: tabsRouter.activeIndex == 0, onPopInvokedWithResult: (didPop, _) => @@ -142,10 +139,10 @@ class TabShellPage extends ConsumerWidget { children: [ navigationRail(tabsRouter), const VerticalDivider(), - Expanded(child: heroedChild), + Expanded(child: child), ], ) - : heroedChild, + : child, bottomNavigationBar: _BottomNavigationBar( tabsRouter: tabsRouter, destinations: navigationDestinations, @@ -168,6 +165,11 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) { ref.read(searchInputFocusProvider).requestFocus(); } + // Album page + if (index == 2) { + ref.read(remoteAlbumProvider.notifier).getAll(); + } + ref.read(hapticFeedbackProvider.notifier).selectionClick(); router.setActiveIndex(index); ref.read(tabProvider.notifier).state = TabEnum.values[index]; diff --git a/mobile/lib/presentation/pages/dev/drift_partner_detail.page.dart b/mobile/lib/presentation/pages/dev/drift_partner_detail.page.dart deleted file mode 100644 index 4b62387cb4..0000000000 --- a/mobile/lib/presentation/pages/dev/drift_partner_detail.page.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/widgets.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; - -@RoutePage() -class DriftPartnerDetailPage extends StatelessWidget { - final String partnerId; - - const DriftPartnerDetailPage({super.key, required this.partnerId}); - - @override - Widget build(BuildContext context) { - return ProviderScope( - overrides: [ - timelineServiceProvider.overrideWith( - (ref) { - final timelineService = - ref.watch(timelineFactoryProvider).remoteAssets(partnerId); - ref.onDispose(timelineService.dispose); - return timelineService; - }, - ), - ], - child: const Timeline(), - ); - } -} diff --git a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart index 566c10c70a..bdb426fe22 100644 --- a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -17,6 +17,21 @@ import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; final _features = [ + _Feature( + name: 'Main Timeline', + icon: Icons.timeline_rounded, + onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()), + ), + _Feature( + name: 'Video', + icon: Icons.video_collection_outlined, + onTap: (ctx, _) => ctx.pushRoute(const DriftVideoRoute()), + ), + _Feature( + name: 'Recently Taken', + icon: Icons.schedule_outlined, + onTap: (ctx, _) => ctx.pushRoute(const DriftRecentlyTakenRoute()), + ), _Feature( name: 'Selection Mode Timeline', icon: Icons.developer_mode_rounded, @@ -42,23 +57,28 @@ final _features = [ return Future.value(); }, ), + _Feature( + name: '', + icon: Icons.vertical_align_center_sharp, + onTap: (_, __) => Future.value(), + ), _Feature( name: 'Sync Local', icon: Icons.photo_album_rounded, onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(), ), _Feature( - name: 'Sync Local Full', + name: 'Sync Local Full (1)', icon: Icons.photo_library_rounded, onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(full: true), ), _Feature( - name: 'Hash Local Assets', + name: 'Hash Local Assets (2)', icon: Icons.numbers_outlined, onTap: (_, ref) => ref.read(backgroundSyncProvider).hashAssets(), ), _Feature( - name: 'Sync Remote', + name: 'Sync Remote (3)', icon: Icons.refresh_rounded, onTap: (_, ref) => ref.read(backgroundSyncProvider).syncRemote(), ), @@ -69,6 +89,11 @@ final _features = [ .read(driftProvider) .customStatement("pragma wal_checkpoint(truncate)"), ), + _Feature( + name: '', + icon: Icons.vertical_align_center_sharp, + onTap: (_, __) => Future.value(), + ), _Feature( name: 'Clear Delta Checkpoint', icon: Icons.delete_rounded, @@ -122,21 +147,6 @@ final _features = [ } }, ), - _Feature( - name: 'Main Timeline', - icon: Icons.timeline_rounded, - onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()), - ), - _Feature( - name: 'Video', - icon: Icons.video_collection_outlined, - onTap: (ctx, _) => ctx.pushRoute(const DriftVideoRoute()), - ), - _Feature( - name: 'Recently Taken', - icon: Icons.schedule_outlined, - onTap: (ctx, _) => ctx.pushRoute(const DriftRecentlyTakenRoute()), - ), ]; @RoutePage() diff --git a/mobile/lib/presentation/pages/dev/local_timeline.page.dart b/mobile/lib/presentation/pages/dev/local_timeline.page.dart deleted file mode 100644 index 3a98a81e9e..0000000000 --- a/mobile/lib/presentation/pages/dev/local_timeline.page.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/widgets.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; - -@RoutePage() -class LocalTimelinePage extends StatelessWidget { - final String albumId; - - const LocalTimelinePage({super.key, required this.albumId}); - - @override - Widget build(BuildContext context) { - return ProviderScope( - overrides: [ - timelineServiceProvider.overrideWith( - (ref) { - final timelineService = - ref.watch(timelineFactoryProvider).localAlbum(albumId: albumId); - ref.onDispose(timelineService.dispose); - return timelineService; - }, - ), - ], - child: const Timeline(), - ); - } -} diff --git a/mobile/lib/presentation/pages/dev/main_timeline.page.dart b/mobile/lib/presentation/pages/dev/main_timeline.page.dart index 9ec8002463..7216b638e1 100644 --- a/mobile/lib/presentation/pages/dev/main_timeline.page.dart +++ b/mobile/lib/presentation/pages/dev/main_timeline.page.dart @@ -16,17 +16,18 @@ class MainTimelinePage extends ConsumerWidget { return memoryLaneProvider.when( data: (memories) { return memories.isEmpty - ? const Timeline() + ? const Timeline(showStorageIndicator: true) : Timeline( topSliverWidget: SliverToBoxAdapter( key: Key('memory-lane-${memories.first.assets.first.id}'), child: DriftMemoryLane(memories: memories), ), topSliverWidgetHeight: 200, + showStorageIndicator: true, ); }, - loading: () => const Timeline(), - error: (error, stackTrace) => const Timeline(), + loading: () => const Timeline(showStorageIndicator: true), + error: (error, stackTrace) => const Timeline(showStorageIndicator: true), ); } } diff --git a/mobile/lib/presentation/pages/dev/media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart index e5745fa629..e61dcdf90d 100644 --- a/mobile/lib/presentation/pages/dev/media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -125,7 +125,7 @@ class LocalMediaSummaryPage extends StatelessWidget { name: album.name, countFuture: countFuture, onTap: () => context.router.push( - LocalTimelineRoute(albumId: album.id), + LocalTimelineRoute(album: album), ), ); }, @@ -226,7 +226,7 @@ class RemoteMediaSummaryPage extends StatelessWidget { name: album.name, countFuture: countFuture, onTap: () => context.router.push( - RemoteTimelineRoute(albumId: album.id), + RemoteAlbumRoute(album: album), ), ); }, diff --git a/mobile/lib/presentation/pages/drift_album.page.dart b/mobile/lib/presentation/pages/drift_album.page.dart index 3298f12b65..e6d3d796a4 100644 --- a/mobile/lib/presentation/pages/drift_album.page.dart +++ b/mobile/lib/presentation/pages/drift_album.page.dart @@ -97,7 +97,20 @@ class _DriftAlbumsPageState extends ConsumerState { onRefresh: onRefresh, child: CustomScrollView( slivers: [ - const ImmichSliverAppBar(), + ImmichSliverAppBar( + actions: [ + IconButton( + icon: const Icon( + Icons.add_rounded, + size: 28, + ), + onPressed: () => context.pushRoute( + const DriftCreateAlbumRoute(), + ), + ), + ], + showUploadButton: false, + ), _SearchBar( searchController: searchController, searchFocusNode: searchFocusNode, @@ -475,7 +488,7 @@ class _AlbumList extends StatelessWidget { final bool isLoading; final String? error; - final List albums; + final List albums; final String? userId; @override @@ -555,7 +568,7 @@ class _AlbumList extends StatelessWidget { ), ), onTap: () => context.router.push( - RemoteTimelineRoute(albumId: album.id), + RemoteAlbumRoute(album: album), ), leadingPadding: const EdgeInsets.only( right: 16, @@ -573,13 +586,24 @@ class _AlbumList extends StatelessWidget { ), ), ) - : const SizedBox( + : SizedBox( width: 80, height: 80, - child: Icon( - Icons.photo_album_rounded, - size: 40, - color: Colors.grey, + child: Container( + decoration: BoxDecoration( + color: context.colorScheme.surfaceContainer, + borderRadius: + const BorderRadius.all(Radius.circular(16)), + border: Border.all( + color: context.colorScheme.outline.withAlpha(50), + width: 1, + ), + ), + child: const Icon( + Icons.photo_album_rounded, + size: 24, + color: Colors.grey, + ), ), ), ), @@ -599,7 +623,7 @@ class _AlbumGrid extends StatelessWidget { required this.error, }); - final List albums; + final List albums; final String? userId; final bool isLoading; final String? error; @@ -674,14 +698,14 @@ class _GridAlbumCard extends StatelessWidget { required this.userId, }); - final Album album; + final RemoteAlbum album; final String? userId; @override Widget build(BuildContext context) { return GestureDetector( onTap: () => context.router.push( - RemoteTimelineRoute(albumId: album.id), + RemoteAlbumRoute(album: album), ), child: Card( elevation: 0, diff --git a/mobile/lib/presentation/pages/dev/drift_archive.page.dart b/mobile/lib/presentation/pages/drift_archive.page.dart similarity index 66% rename from mobile/lib/presentation/pages/dev/drift_archive.page.dart rename to mobile/lib/presentation/pages/drift_archive.page.dart index 14657f7149..90b8dcb646 100644 --- a/mobile/lib/presentation/pages/dev/drift_archive.page.dart +++ b/mobile/lib/presentation/pages/drift_archive.page.dart @@ -1,9 +1,12 @@ import 'package:auto_route/auto_route.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() class DriftArchivePage extends StatelessWidget { @@ -27,7 +30,13 @@ class DriftArchivePage extends StatelessWidget { }, ), ], - child: const Timeline(), + child: Timeline( + appBar: MesmerizingSliverAppBar( + title: 'archive'.t(context: context), + icon: Icons.archive_outlined, + ), + bottomSheet: const ArchiveBottomSheet(), + ), ); } } diff --git a/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart b/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart index df6211c338..7ac378e4f5 100644 --- a/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart +++ b/mobile/lib/presentation/pages/drift_asset_selection_timeline.page.dart @@ -33,7 +33,7 @@ class DriftAssetSelectionTimelinePage extends ConsumerWidget { final user = ref.watch(currentUserProvider); if (user == null) { throw Exception( - 'User must be logged in to access recently taken', + 'User must be logged in to access asset selection timeline', ); } diff --git a/mobile/lib/presentation/pages/drift_create_album.page.dart b/mobile/lib/presentation/pages/drift_create_album.page.dart new file mode 100644 index 0000000000..e06321413e --- /dev/null +++ b/mobile/lib/presentation/pages/drift_create_album.page.dart @@ -0,0 +1,500 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; + +@RoutePage() +class DriftCreateAlbumPage extends ConsumerStatefulWidget { + const DriftCreateAlbumPage({super.key}); + + @override + ConsumerState createState() => + _DriftCreateAlbumPageState(); +} + +class _DriftCreateAlbumPageState extends ConsumerState { + TextEditingController albumTitleController = TextEditingController(); + TextEditingController albumDescriptionController = TextEditingController(); + FocusNode albumTitleTextFieldFocusNode = FocusNode(); + FocusNode albumDescriptionTextFieldFocusNode = FocusNode(); + bool isAlbumTitleTextFieldFocus = false; + Set selectedAssets = {}; + + @override + void dispose() { + albumTitleController.dispose(); + albumDescriptionController.dispose(); + albumTitleTextFieldFocusNode.dispose(); + albumDescriptionTextFieldFocusNode.dispose(); + super.dispose(); + } + + bool get _canCreateAlbum => albumTitleController.text.isNotEmpty; + + String _getEffectiveTitle() { + return albumTitleController.text.isNotEmpty + ? albumTitleController.text + : 'create_album_page_untitled'.t(context: context); + } + + Widget _buildSliverAppBar() { + return SliverAppBar( + backgroundColor: context.scaffoldBackgroundColor, + elevation: 0, + automaticallyImplyLeading: false, + pinned: true, + snap: false, + floating: false, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(200.0), + child: SizedBox( + height: 200, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + buildTitleInputField(), + buildDescriptionInputField(), + if (selectedAssets.isNotEmpty) buildControlButton(), + ], + ), + ), + ), + ); + } + + Widget _buildContent() { + if (selectedAssets.isEmpty) { + return SliverList( + delegate: SliverChildListDelegate([ + _buildEmptyState(), + _buildSelectPhotosButton(), + ]), + ); + } else { + return _buildSelectedImageGrid(); + } + } + + Widget _buildEmptyState() { + return Padding( + padding: const EdgeInsets.only(top: 0, left: 18), + child: Text( + 'create_shared_album_page_share_add_assets', + style: context.textTheme.labelLarge, + ).t(), + ); + } + + Widget _buildSelectPhotosButton() { + return Padding( + padding: const EdgeInsets.all(16.0), + child: FilledButton.icon( + style: FilledButton.styleFrom( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + vertical: 24.0, + horizontal: 16.0, + ), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10.0)), + ), + backgroundColor: context.colorScheme.surfaceContainerHigh, + ), + onPressed: onSelectPhotos, + icon: Icon(Icons.add_rounded, color: context.primaryColor), + label: Padding( + padding: const EdgeInsets.only( + left: 8.0, + ), + child: Text( + 'create_shared_album_page_share_select_photos', + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ).t(), + ), + ), + ); + } + + Widget _buildSelectedImageGrid() { + return SliverPadding( + padding: const EdgeInsets.only(top: 16.0), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + crossAxisSpacing: 1.0, + mainAxisSpacing: 1.0, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + final asset = selectedAssets.elementAt(index); + return GestureDetector( + onTap: onBackgroundTapped, + child: Thumbnail(asset: asset), + ); + }, + childCount: selectedAssets.length, + ), + ), + ); + } + + void onBackgroundTapped() { + albumTitleTextFieldFocusNode.unfocus(); + albumDescriptionTextFieldFocusNode.unfocus(); + setState(() { + isAlbumTitleTextFieldFocus = false; + }); + + if (albumTitleController.text.isEmpty) { + final untitledText = 'create_album_page_untitled'.t(); + albumTitleController.text = untitledText; + } + } + + Future onSelectPhotos() async { + final assets = await context.pushRoute>( + DriftAssetSelectionTimelineRoute( + lockedSelectionAssets: selectedAssets, + ), + ); + + if (assets == null || assets.isEmpty) { + return; + } + + setState(() { + selectedAssets = selectedAssets.union(assets); + }); + } + + Future createAlbum() async { + onBackgroundTapped(); + + final title = _getEffectiveTitle().trim(); + if (title.isEmpty) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('create_album_title_required'.t()), + backgroundColor: context.colorScheme.error, + ), + ); + } + return; + } + + final album = await ref.watch(remoteAlbumProvider.notifier).createAlbum( + title: title, + description: albumDescriptionController.text.trim(), + assetIds: selectedAssets.map((asset) { + final remoteAsset = asset as RemoteAsset; + return remoteAsset.id; + }).toList(), + ); + + if (album != null) { + context.replaceRoute( + RemoteAlbumRoute(album: album), + ); + } + } + + Widget buildTitleInputField() { + return Padding( + padding: const EdgeInsets.only( + right: 10.0, + left: 10.0, + ), + child: _AlbumTitleTextField( + focusNode: albumTitleTextFieldFocusNode, + textController: albumTitleController, + isFocus: isAlbumTitleTextFieldFocus, + onFocusChanged: (focus) { + setState(() { + isAlbumTitleTextFieldFocus = focus; + }); + }, + ), + ); + } + + Widget buildDescriptionInputField() { + return Padding( + padding: const EdgeInsets.only( + right: 10.0, + left: 10.0, + top: 8, + ), + child: _AlbumViewerEditableDescription( + textController: albumDescriptionController, + focusNode: albumDescriptionTextFieldFocusNode, + ), + ); + } + + Widget buildControlButton() { + return Padding( + padding: const EdgeInsets.only( + left: 12.0, + top: 8.0, + bottom: 8.0, + ), + child: SizedBox( + height: 42.0, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + AlbumActionFilledButton( + iconData: Icons.add_photo_alternate_outlined, + onPressed: onSelectPhotos, + labelText: "add_photos".t(), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0, + centerTitle: false, + backgroundColor: context.scaffoldBackgroundColor, + leading: IconButton( + onPressed: () => context.maybePop(), + icon: const Icon(Icons.close_rounded), + ), + title: const Text('create_album').t(), + actions: [ + TextButton( + onPressed: _canCreateAlbum ? createAlbum : null, + child: Text( + 'create'.t(), + style: TextStyle( + fontWeight: FontWeight.bold, + color: _canCreateAlbum + ? context.primaryColor + : context.themeData.disabledColor, + ), + ), + ), + ], + ), + body: GestureDetector( + onTap: onBackgroundTapped, + child: CustomScrollView( + slivers: [ + _buildSliverAppBar(), + _buildContent(), + ], + ), + ), + ); + } +} + +class _AlbumTitleTextField extends StatefulWidget { + const _AlbumTitleTextField({ + required this.focusNode, + required this.textController, + required this.isFocus, + required this.onFocusChanged, + }); + + final FocusNode focusNode; + final TextEditingController textController; + final bool isFocus; + final ValueChanged onFocusChanged; + + @override + State<_AlbumTitleTextField> createState() => _AlbumTitleTextFieldState(); +} + +class _AlbumTitleTextFieldState extends State<_AlbumTitleTextField> { + @override + void initState() { + super.initState(); + widget.focusNode.addListener(_onFocusChange); + } + + @override + void dispose() { + widget.focusNode.removeListener(_onFocusChange); + super.dispose(); + } + + void _onFocusChange() { + widget.onFocusChanged(widget.focusNode.hasFocus); + } + + @override + Widget build(BuildContext context) { + return TextField( + focusNode: widget.focusNode, + style: TextStyle( + fontSize: 28.0, + color: context.colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + controller: widget.textController, + onTap: () { + if (widget.textController.text == + 'create_album_page_untitled'.t(context: context)) { + widget.textController.clear(); + } + }, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 16.0, + ), + suffixIcon: widget.textController.text.isNotEmpty && widget.isFocus + ? IconButton( + onPressed: () { + widget.textController.clear(); + }, + icon: Icon( + Icons.cancel_rounded, + color: context.primaryColor, + ), + splashRadius: 10.0, + ) + : null, + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.all( + Radius.circular(16.0), + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: context.primaryColor.withValues(alpha: 0.3), + ), + borderRadius: const BorderRadius.all( + Radius.circular(16.0), + ), + ), + hintText: 'add_a_title'.t(), + hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( + fontSize: 28.0, + fontWeight: FontWeight.bold, + height: 1.2, + ), + focusColor: Colors.grey[300], + fillColor: context.colorScheme.surfaceContainerHigh, + filled: true, + ), + ); + } +} + +class _AlbumViewerEditableDescription extends StatefulWidget { + const _AlbumViewerEditableDescription({ + required this.textController, + required this.focusNode, + }); + + final TextEditingController textController; + final FocusNode focusNode; + + @override + State<_AlbumViewerEditableDescription> createState() => + _AlbumViewerEditableDescriptionState(); +} + +class _AlbumViewerEditableDescriptionState + extends State<_AlbumViewerEditableDescription> { + @override + void initState() { + super.initState(); + widget.focusNode.addListener(_onFocusModeChange); + widget.textController.addListener(_onTextChange); + } + + @override + void dispose() { + widget.focusNode.removeListener(_onFocusModeChange); + widget.textController.removeListener(_onTextChange); + super.dispose(); + } + + void _onFocusModeChange() { + setState(() { + if (!widget.focusNode.hasFocus && widget.textController.text.isEmpty) { + widget.textController.clear(); + } + }); + } + + void _onTextChange() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: TextField( + focusNode: widget.focusNode, + style: context.textTheme.bodyLarge, + maxLines: 3, + minLines: 1, + controller: widget.textController, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 16.0, + ), + suffixIcon: + widget.focusNode.hasFocus && widget.textController.text.isNotEmpty + ? IconButton( + onPressed: () { + widget.textController.clear(); + }, + icon: Icon( + Icons.cancel_rounded, + color: context.primaryColor, + ), + splashRadius: 10.0, + ) + : null, + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: context.colorScheme.outline.withValues(alpha: 0.3), + ), + borderRadius: const BorderRadius.all( + Radius.circular(16.0), + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: context.primaryColor.withValues(alpha: 0.3), + ), + borderRadius: const BorderRadius.all( + Radius.circular(16.0), + ), + ), + hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( + fontSize: 16.0, + color: context.colorScheme.onSurface.withValues(alpha: 0.6), + ), + focusColor: Colors.grey[300], + fillColor: context.scaffoldBackgroundColor, + filled: widget.focusNode.hasFocus, + hintText: 'add_a_description'.t(), + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/dev/drift_favorite.page.dart b/mobile/lib/presentation/pages/drift_favorite.page.dart similarity index 66% rename from mobile/lib/presentation/pages/dev/drift_favorite.page.dart rename to mobile/lib/presentation/pages/drift_favorite.page.dart index 4055ad863b..90a273f93b 100644 --- a/mobile/lib/presentation/pages/dev/drift_favorite.page.dart +++ b/mobile/lib/presentation/pages/drift_favorite.page.dart @@ -1,9 +1,12 @@ import 'package:auto_route/auto_route.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() class DriftFavoritePage extends StatelessWidget { @@ -27,7 +30,13 @@ class DriftFavoritePage extends StatelessWidget { }, ), ], - child: const Timeline(), + child: Timeline( + appBar: MesmerizingSliverAppBar( + title: 'favorites'.t(context: context), + icon: Icons.favorite_outline, + ), + bottomSheet: const FavoriteBottomSheet(), + ), ); } } diff --git a/mobile/lib/presentation/pages/drift_library.page.dart b/mobile/lib/presentation/pages/drift_library.page.dart index 0efa8040e7..1e8975dcfa 100644 --- a/mobile/lib/presentation/pages/drift_library.page.dart +++ b/mobile/lib/presentation/pages/drift_library.page.dart @@ -27,7 +27,12 @@ class DriftLibraryPage extends ConsumerWidget { return const Scaffold( body: CustomScrollView( slivers: [ - ImmichSliverAppBar(), + ImmichSliverAppBar( + snap: false, + floating: false, + pinned: true, + showUploadButton: false, + ), _ActionButtonGrid(), _CollectionCards(), _QuickAccessButtonList(), @@ -507,8 +512,9 @@ class _PartnerList extends StatelessWidget { fontWeight: FontWeight.w500, ), ).t(context: context, args: {'user': partner.name}), - onTap: () => - context.pushRoute(DriftPartnerDetailRoute(partnerId: partner.id)), + onTap: () => context.pushRoute( + DriftPartnerDetailRoute(partner: partner), + ), ); }, ); diff --git a/mobile/lib/presentation/pages/dev/drift_local_album.page.dart b/mobile/lib/presentation/pages/drift_local_album.page.dart similarity index 99% rename from mobile/lib/presentation/pages/dev/drift_local_album.page.dart rename to mobile/lib/presentation/pages/drift_local_album.page.dart index f47811b6da..0ad9abd2fa 100644 --- a/mobile/lib/presentation/pages/dev/drift_local_album.page.dart +++ b/mobile/lib/presentation/pages/drift_local_album.page.dart @@ -103,7 +103,7 @@ class _AlbumList extends ConsumerWidget { ), ), onTap: () => - context.pushRoute(LocalTimelineRoute(albumId: album.id)), + context.pushRoute(LocalTimelineRoute(album: album)), ), ); }, diff --git a/mobile/lib/presentation/pages/dev/drift_locked_folder.page.dart b/mobile/lib/presentation/pages/drift_locked_folder.page.dart similarity index 70% rename from mobile/lib/presentation/pages/dev/drift_locked_folder.page.dart rename to mobile/lib/presentation/pages/drift_locked_folder.page.dart index 5ab7c71347..9b42cdb103 100644 --- a/mobile/lib/presentation/pages/dev/drift_locked_folder.page.dart +++ b/mobile/lib/presentation/pages/drift_locked_folder.page.dart @@ -1,9 +1,12 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() class DriftLockedFolderPage extends StatelessWidget { @@ -27,7 +30,12 @@ class DriftLockedFolderPage extends StatelessWidget { }, ), ], - child: const Timeline(), + child: Timeline( + appBar: MesmerizingSliverAppBar( + title: 'locked_folder'.t(context: context), + ), + bottomSheet: const LockedFolderBottomSheet(), + ), ); } } diff --git a/mobile/lib/presentation/pages/drift_partner_detail.page.dart b/mobile/lib/presentation/pages/drift_partner_detail.page.dart new file mode 100644 index 0000000000..baae893d39 --- /dev/null +++ b/mobile/lib/presentation/pages/drift_partner_detail.page.dart @@ -0,0 +1,109 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; + +@RoutePage() +class DriftPartnerDetailPage extends StatelessWidget { + final UserDto partner; + + const DriftPartnerDetailPage({ + super.key, + required this.partner, + }); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final timelineService = + ref.watch(timelineFactoryProvider).remoteAssets(partner.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: Timeline( + appBar: MesmerizingSliverAppBar( + title: partner.name, + icon: Icons.person_outline, + ), + topSliverWidget: _InfoBox( + onTap: () => { + // TODO: Create DriftUserProvider/DriftUserService to handle this action + }, + inTimeline: partner.inTimeline, + ), + topSliverWidgetHeight: 110, + bottomSheet: const PartnerDetailBottomSheet(), + ), + ); + } +} + +class _InfoBox extends StatelessWidget { + final VoidCallback onTap; + final bool inTimeline; + + const _InfoBox({ + required this.onTap, + required this.inTimeline, + }); + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: SizedBox( + height: 110, + child: Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0), + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: context.colorScheme.onSurface.withAlpha(10), + width: 1, + ), + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), + gradient: LinearGradient( + colors: [ + context.colorScheme.primary.withAlpha(10), + context.colorScheme.primary.withAlpha(15), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ListTile( + title: Text( + "Show in timeline", + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.primary, + ), + ), + subtitle: Text( + "Show photos and videos from this user in your timeline", + style: context.textTheme.bodyMedium, + ), + trailing: Switch( + value: inTimeline, + onChanged: (_) => onTap(), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/dev/drift_recently_taken.page.dart b/mobile/lib/presentation/pages/drift_recently_taken.page.dart similarity index 100% rename from mobile/lib/presentation/pages/dev/drift_recently_taken.page.dart rename to mobile/lib/presentation/pages/drift_recently_taken.page.dart diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart new file mode 100644 index 0000000000..bbfe6ddc74 --- /dev/null +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -0,0 +1,41 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; + +@RoutePage() +class RemoteAlbumPage extends StatelessWidget { + final RemoteAlbum album; + + const RemoteAlbumPage({super.key, required this.album}); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final timelineService = ref + .watch(timelineFactoryProvider) + .remoteAlbum(albumId: album.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: Timeline( + appBar: MesmerizingSliverAppBar( + title: album.name, + icon: Icons.photo_album_outlined, + ), + bottomSheet: RemoteAlbumBottomSheet( + album: album, + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/dev/drift_trash.page.dart b/mobile/lib/presentation/pages/drift_trash.page.dart similarity index 73% rename from mobile/lib/presentation/pages/dev/drift_trash.page.dart rename to mobile/lib/presentation/pages/drift_trash.page.dart index cbcfe50112..9cd2fac760 100644 --- a/mobile/lib/presentation/pages/dev/drift_trash.page.dart +++ b/mobile/lib/presentation/pages/drift_trash.page.dart @@ -1,6 +1,7 @@ import 'package:auto_route/auto_route.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -27,7 +28,16 @@ class DriftTrashPage extends StatelessWidget { }, ), ], - child: const Timeline(), + child: Timeline( + appBar: SliverAppBar( + title: Text('trash'.t(context: context)), + floating: true, + snap: true, + pinned: true, + centerTitle: true, + elevation: 0, + ), + ), ); } } diff --git a/mobile/lib/presentation/pages/dev/drift_video.page.dart b/mobile/lib/presentation/pages/drift_video.page.dart similarity index 100% rename from mobile/lib/presentation/pages/dev/drift_video.page.dart rename to mobile/lib/presentation/pages/drift_video.page.dart diff --git a/mobile/lib/presentation/pages/dev/remote_timeline.page.dart b/mobile/lib/presentation/pages/local_timeline.page.dart similarity index 54% rename from mobile/lib/presentation/pages/dev/remote_timeline.page.dart rename to mobile/lib/presentation/pages/local_timeline.page.dart index 6568f0f74f..b4df0f64e2 100644 --- a/mobile/lib/presentation/pages/dev/remote_timeline.page.dart +++ b/mobile/lib/presentation/pages/local_timeline.page.dart @@ -1,14 +1,17 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() -class RemoteTimelinePage extends StatelessWidget { - final String albumId; +class LocalTimelinePage extends StatelessWidget { + final LocalAlbum album; - const RemoteTimelinePage({super.key, required this.albumId}); + const LocalTimelinePage({super.key, required this.album}); @override Widget build(BuildContext context) { @@ -18,13 +21,16 @@ class RemoteTimelinePage extends StatelessWidget { (ref) { final timelineService = ref .watch(timelineFactoryProvider) - .remoteAlbum(albumId: albumId); + .localAlbum(albumId: album.id); ref.onDispose(timelineService.dispose); return timelineService; }, ), ], - child: const Timeline(), + child: Timeline( + appBar: MesmerizingSliverAppBar(title: album.name), + bottomSheet: const LocalAlbumBottomSheet(), + ), ); } } diff --git a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart index b18e10ebba..8228e27cc1 100644 --- a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart @@ -1,16 +1,56 @@ import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; class RemoveFromAlbumActionButton extends ConsumerWidget { - const RemoveFromAlbumActionButton({super.key}); + final String albumId; + final ActionSource source; + + const RemoveFromAlbumActionButton({ + super.key, + required this.albumId, + required this.source, + }); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref + .read(actionProvider.notifier) + .removeFromAlbum(source, albumId); + ref.read(multiSelectProvider.notifier).reset(); + + final successMessage = 'remove_from_album_action_prompt'.t( + context: context, + args: {'count': result.count.toString()}, + ); + + if (context.mounted) { + ImmichToast.show( + context: context, + msg: result.success + ? successMessage + : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); + } + } @override Widget build(BuildContext context, WidgetRef ref) { return BaseActionButton( iconData: Icons.remove_circle_outline, label: "remove_from_album".t(context: context), + onPressed: () => _onTap(context, ref), ); } } diff --git a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart index a58d9f1ee1..e44500144c 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart @@ -8,10 +8,10 @@ import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; -class UnarchiveActionButton extends ConsumerWidget { +class UnArchiveActionButton extends ConsumerWidget { final ActionSource source; - const UnarchiveActionButton({super.key, required this.source}); + const UnArchiveActionButton({super.key, required this.source}); void _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index aea821d945..b41d6ba2c7 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -71,6 +71,7 @@ class _AssetViewerState extends ConsumerState { StreamSubscription? reloadSubscription; late Platform platform; + late final int heroOffset; late PhotoViewControllerValue initialPhotoViewState; bool? hasDraggedDown; bool isSnapping = false; @@ -98,6 +99,7 @@ class _AssetViewerState extends ConsumerState { _onAssetChanged(widget.initialIndex); }); reloadSubscription = EventStream.shared.listen(_onEvent); + heroOffset = TabsRouterScope.of(context)?.controller.activeIndex ?? 0; } @override @@ -335,7 +337,7 @@ class _AssetViewerState extends ConsumerState { final isDraggingDown = currentExtent < previousExtent; previousExtent = currentExtent; // Closes the bottom sheet if the user is dragging down - if (isDraggingDown && delta.extent < 0.5) { + if (isDraggingDown && delta.extent < 0.55) { if (dragInProgress) { blockGestures = true; } @@ -400,7 +402,7 @@ class _AssetViewerState extends ConsumerState { previousExtent = _kBottomSheetMinimumExtent; sheetCloseController = showBottomSheet( context: ctx, - sheetAnimationStyle: AnimationStyle( + sheetAnimationStyle: const AnimationStyle( duration: Durations.short4, reverseDuration: Durations.short2, ), @@ -487,7 +489,8 @@ class _AssetViewerState extends ConsumerState { return PhotoViewGalleryPageOptions( key: ValueKey(asset.heroTag), imageProvider: getFullImageProvider(asset, size: size), - heroAttributes: PhotoViewHeroAttributes(tag: asset.heroTag), + heroAttributes: + PhotoViewHeroAttributes(tag: '${asset.heroTag}_$heroOffset'), filterQuality: FilterQuality.high, tightMode: true, initialScale: PhotoViewComputedScale.contained * 0.999, @@ -521,7 +524,8 @@ class _AssetViewerState extends ConsumerState { onDragUpdate: _onDragUpdate, onDragEnd: _onDragEnd, onTapDown: _onTapDown, - heroAttributes: PhotoViewHeroAttributes(tag: asset.heroTag), + heroAttributes: + PhotoViewHeroAttributes(tag: '${asset.heroTag}_$heroOffset'), filterQuality: FilterQuality.high, initialScale: PhotoViewComputedScale.contained * 0.99, maxScale: 1.0, diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index d0bdc28d10..a7a2a57ce5 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -16,7 +16,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart'; -import 'package:immich_mobile/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; diff --git a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart new file mode 100644 index 0000000000..c23f268465 --- /dev/null +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; + +class ArchiveBottomSheet extends ConsumerWidget { + const ArchiveBottomSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final multiselect = ref.watch(multiSelectProvider); + final isTrashEnable = ref.watch( + serverInfoProvider.select((state) => state.serverFeatures.trash), + ); + + return BaseBottomSheet( + initialChildSize: 0.25, + maxChildSize: 0.4, + shouldCloseOnMinExtent: false, + actions: [ + const ShareActionButton(), + if (multiselect.hasRemote) ...[ + const ShareLinkActionButton(source: ActionSource.timeline), + const UnArchiveActionButton(source: ActionSource.timeline), + const FavoriteActionButton(source: ActionSource.timeline), + const DownloadActionButton(), + isTrashEnable + ? const TrashActionButton(source: ActionSource.timeline) + : const DeletePermanentActionButton( + source: ActionSource.timeline, + ), + const EditDateTimeActionButton(), + const EditLocationActionButton(source: ActionSource.timeline), + const MoveToLockFolderActionButton( + source: ActionSource.timeline, + ), + const StackActionButton(), + ], + if (multiselect.hasLocal) ...[ + const DeleteLocalActionButton(), + const UploadActionButton(), + ], + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart similarity index 100% rename from mobile/lib/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart rename to mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart new file mode 100644 index 0000000000..2f8208a80b --- /dev/null +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; + +class FavoriteBottomSheet extends ConsumerWidget { + const FavoriteBottomSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final multiselect = ref.watch(multiSelectProvider); + final isTrashEnable = ref.watch( + serverInfoProvider.select((state) => state.serverFeatures.trash), + ); + + return BaseBottomSheet( + initialChildSize: 0.25, + maxChildSize: 0.4, + shouldCloseOnMinExtent: false, + actions: [ + const ShareActionButton(), + if (multiselect.hasRemote) ...[ + const ShareLinkActionButton(source: ActionSource.timeline), + const UnFavoriteActionButton(source: ActionSource.timeline), + const ArchiveActionButton(source: ActionSource.timeline), + const DownloadActionButton(), + isTrashEnable + ? const TrashActionButton(source: ActionSource.timeline) + : const DeletePermanentActionButton( + source: ActionSource.timeline, + ), + const EditDateTimeActionButton(), + const EditLocationActionButton(source: ActionSource.timeline), + const MoveToLockFolderActionButton( + source: ActionSource.timeline, + ), + const StackActionButton(), + ], + if (multiselect.hasLocal) ...[ + const DeleteLocalActionButton(), + const UploadActionButton(), + ], + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/bottom_app_bar/home_bottom_app_bar.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart similarity index 92% rename from mobile/lib/presentation/widgets/bottom_app_bar/home_bottom_app_bar.widget.dart rename to mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index f5ba759130..900adefd0b 100644 --- a/mobile/lib/presentation/widgets/bottom_app_bar/home_bottom_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -14,12 +14,12 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -class HomeBottomAppBar extends ConsumerWidget { - const HomeBottomAppBar({super.key}); +class GeneralBottomSheet extends ConsumerWidget { + const GeneralBottomSheet({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -30,9 +30,10 @@ class HomeBottomAppBar extends ConsumerWidget { return BaseBottomSheet( initialChildSize: 0.25, + maxChildSize: 0.4, shouldCloseOnMinExtent: false, actions: [ - if (multiselect.isEnabled) const ShareActionButton(), + const ShareActionButton(), if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), const ArchiveActionButton(source: ActionSource.timeline), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart new file mode 100644 index 0000000000..3fd717f516 --- /dev/null +++ b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; + +class LocalAlbumBottomSheet extends ConsumerWidget { + const LocalAlbumBottomSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return const BaseBottomSheet( + initialChildSize: 0.25, + maxChildSize: 0.4, + shouldCloseOnMinExtent: false, + actions: [ + ShareActionButton(), + DeleteLocalActionButton(), + UploadActionButton(), + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart new file mode 100644 index 0000000000..97b2646f32 --- /dev/null +++ b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; + +class LockedFolderBottomSheet extends ConsumerWidget { + const LockedFolderBottomSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return const BaseBottomSheet( + initialChildSize: 0.25, + maxChildSize: 0.4, + shouldCloseOnMinExtent: false, + actions: [ + ShareActionButton(), + DownloadActionButton(), + DeletePermanentActionButton(source: ActionSource.timeline), + RemoveFromLockFolderActionButton(source: ActionSource.timeline), + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart new file mode 100644 index 0000000000..7af8ab7c86 --- /dev/null +++ b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; + +class PartnerDetailBottomSheet extends ConsumerWidget { + const PartnerDetailBottomSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return const BaseBottomSheet( + initialChildSize: 0.25, + maxChildSize: 0.4, + shouldCloseOnMinExtent: false, + actions: [ + ShareActionButton(), + DownloadActionButton(), + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart new file mode 100644 index 0000000000..0268a2b386 --- /dev/null +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; + +class RemoteAlbumBottomSheet extends ConsumerWidget { + final RemoteAlbum album; + const RemoteAlbumBottomSheet({super.key, required this.album}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final multiselect = ref.watch(multiSelectProvider); + final isTrashEnable = ref.watch( + serverInfoProvider.select((state) => state.serverFeatures.trash), + ); + + return BaseBottomSheet( + initialChildSize: 0.25, + maxChildSize: 0.4, + shouldCloseOnMinExtent: false, + actions: [ + const ShareActionButton(), + if (multiselect.hasRemote) ...[ + const ShareLinkActionButton(source: ActionSource.timeline), + const ArchiveActionButton(source: ActionSource.timeline), + const FavoriteActionButton(source: ActionSource.timeline), + const DownloadActionButton(), + isTrashEnable + ? const TrashActionButton(source: ActionSource.timeline) + : const DeletePermanentActionButton( + source: ActionSource.timeline, + ), + const EditDateTimeActionButton(), + const EditLocationActionButton(source: ActionSource.timeline), + const MoveToLockFolderActionButton( + source: ActionSource.timeline, + ), + const StackActionButton(), + ], + if (multiselect.hasLocal) ...[ + const DeleteLocalActionButton(), + const UploadActionButton(), + ], + RemoveFromAlbumActionButton( + source: ActionSource.timeline, + albumId: album.id, + ), + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart index d7261c927e..7e3776adb2 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -25,6 +26,8 @@ class ThumbnailTile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final heroOffset = TabsRouterScope.of(context)?.controller.activeIndex ?? 0; + final assetContainerColor = context.isDarkTheme ? context.primaryColor.darken(amount: 0.4) : context.primaryColor.lighten(amount: 0.75); @@ -64,7 +67,7 @@ class ThumbnailTile extends ConsumerWidget { children: [ Positioned.fill( child: Hero( - tag: asset.heroTag, + tag: '${asset.heroTag}_$heroOffset', child: Thumbnail( asset: asset, fit: fit, diff --git a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart index 14b1a4616d..d12f82d27d 100644 --- a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart +++ b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart @@ -159,17 +159,18 @@ class _AssetTileWidget extends ConsumerWidget { required this.assetIndex, }); - void _handleOnTap( + Future _handleOnTap( BuildContext ctx, WidgetRef ref, int assetIndex, BaseAsset asset, - ) { + ) async { final multiSelectState = ref.read(multiSelectProvider); if (multiSelectState.forceEnable || multiSelectState.isEnabled) { ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset); } else { + await ref.read(timelineServiceProvider).loadAssets(assetIndex, 1); ctx.pushRoute( AssetViewerRoute( initialIndex: assetIndex, @@ -206,6 +207,9 @@ class _AssetTileWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final lockSelection = _getLockSelectionStatus(ref); + final showStorageIndicator = ref.watch( + timelineArgsProvider.select((args) => args.showStorageIndicator), + ); return RepaintBoundary( child: GestureDetector( @@ -217,6 +221,7 @@ class _AssetTileWidget extends ConsumerWidget { child: ThumbnailTile( asset, lockSelection: lockSelection, + showStorageIndicator: showStorageIndicator, ), ), ); diff --git a/mobile/lib/presentation/widgets/timeline/timeline.state.dart b/mobile/lib/presentation/widgets/timeline/timeline.state.dart index 629fac7831..30a4088ce2 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.state.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.state.dart @@ -14,12 +14,14 @@ class TimelineArgs { final double maxHeight; final double spacing; final int columnCount; + final bool showStorageIndicator; const TimelineArgs({ required this.maxWidth, required this.maxHeight, this.spacing = kTimelineSpacing, this.columnCount = kTimelineColumnCount, + this.showStorageIndicator = false, }); @override @@ -27,7 +29,8 @@ class TimelineArgs { return spacing == other.spacing && maxWidth == other.maxWidth && maxHeight == other.maxHeight && - columnCount == other.columnCount; + columnCount == other.columnCount && + showStorageIndicator == other.showStorageIndicator; } @override @@ -35,7 +38,8 @@ class TimelineArgs { maxWidth.hashCode ^ maxHeight.hashCode ^ spacing.hashCode ^ - columnCount.hashCode; + columnCount.hashCode ^ + showStorageIndicator.hashCode; } class TimelineState { diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index 9fb164e2dc..490f2bcff2 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/bottom_app_bar/home_bottom_app_bar.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'; @@ -18,6 +18,7 @@ import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart'; +import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart'; class Timeline extends StatelessWidget { @@ -25,11 +26,16 @@ class Timeline extends StatelessWidget { super.key, this.topSliverWidget, this.topSliverWidgetHeight, + this.showStorageIndicator = false, + this.appBar, + this.bottomSheet = const GeneralBottomSheet(), }); final Widget? topSliverWidget; final double? topSliverWidgetHeight; - + final bool showStorageIndicator; + final Widget? appBar; + final Widget? bottomSheet; @override Widget build(BuildContext context) { return Scaffold( @@ -43,12 +49,15 @@ class Timeline extends StatelessWidget { columnCount: ref.watch( settingsProvider.select((s) => s.get(Setting.tilesPerRow)), ), + showStorageIndicator: showStorageIndicator, ), ), ], child: _SliverTimeline( topSliverWidget: topSliverWidget, topSliverWidgetHeight: topSliverWidgetHeight, + appBar: appBar, + bottomSheet: bottomSheet, ), ), ), @@ -60,10 +69,14 @@ class _SliverTimeline extends ConsumerStatefulWidget { const _SliverTimeline({ this.topSliverWidget, this.topSliverWidgetHeight, + this.appBar, + this.bottomSheet, }); final Widget? topSliverWidget; final double? topSliverWidgetHeight; + final Widget? appBar; + final Widget? bottomSheet; @override ConsumerState createState() => _SliverTimelineState(); @@ -100,6 +113,10 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { onData: (segments) { final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1; final statusBarHeight = context.padding.top; + final double appBarExpandedHeight = + widget.appBar != null && widget.appBar is MesmerizingSliverAppBar + ? 200 + : 0; final totalAppBarHeight = statusBarHeight + kToolbarHeight; const scrubberBottomPadding = 100.0; @@ -112,7 +129,8 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { timelineHeight: maxHeight, topPadding: totalAppBarHeight + 10, bottomPadding: context.padding.bottom + scrubberBottomPadding, - monthSegmentSnappingOffset: widget.topSliverWidgetHeight, + monthSegmentSnappingOffset: + widget.topSliverWidgetHeight ?? 0 + appBarExpandedHeight, child: CustomScrollView( primary: true, cacheExtent: maxHeight * 2, @@ -120,11 +138,12 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { if (isSelectionMode) const SelectionSliverAppBar() else - const ImmichSliverAppBar( - floating: true, - pinned: false, - snap: false, - ), + widget.appBar ?? + const ImmichSliverAppBar( + floating: true, + pinned: false, + snap: false, + ), if (widget.topSliverWidget != null) widget.topSliverWidget!, _SliverSegmentedList( segments: segments, @@ -182,7 +201,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { } return const SizedBox.shrink(); }, - child: const HomeBottomAppBar(), + child: widget.bottomSheet, ), ], ], diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 0c197ca683..49605e918a 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -228,6 +228,24 @@ class ActionNotifier extends Notifier { ); } } + + Future removeFromAlbum( + ActionSource source, + String albumId, + ) async { + final ids = _getRemoteIdsForSource(source); + try { + final removedCount = await _service.removeFromAlbum(ids, albumId); + return ActionResult(count: removedCount, success: true); + } catch (error, stack) { + _logger.severe('Failed to remove assets from album', error, stack); + return ActionResult( + count: ids.length, + success: false, + error: error.toString(), + ); + } + } } extension on Iterable { diff --git a/mobile/lib/providers/infrastructure/album.provider.dart b/mobile/lib/providers/infrastructure/album.provider.dart index 4a6db50697..4ec3453d16 100644 --- a/mobile/lib/providers/infrastructure/album.provider.dart +++ b/mobile/lib/providers/infrastructure/album.provider.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart'; +import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; final localAlbumRepository = Provider( (ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)), @@ -30,7 +31,10 @@ final remoteAlbumRepository = Provider( ); final remoteAlbumServiceProvider = Provider( - (ref) => RemoteAlbumService(ref.watch(remoteAlbumRepository)), + (ref) => RemoteAlbumService( + ref.watch(remoteAlbumRepository), + ref.watch(driftAlbumApiRepositoryProvider), + ), dependencies: [remoteAlbumRepository], ); diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index 2e5a475b9c..84db53ab9f 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -8,21 +8,21 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'album.provider.dart'; class RemoteAlbumState { - final List albums; - final List filteredAlbums; + final List albums; + final List filteredAlbums; final bool isLoading; final String? error; const RemoteAlbumState({ required this.albums, - List? filteredAlbums, + List? filteredAlbums, this.isLoading = false, this.error, }) : filteredAlbums = filteredAlbums ?? albums; RemoteAlbumState copyWith({ - List? albums, - List? filteredAlbums, + List? albums, + List? filteredAlbums, bool? isLoading, String? error, }) { @@ -66,7 +66,7 @@ class RemoteAlbumNotifier extends Notifier { return const RemoteAlbumState(albums: [], filteredAlbums: []); } - Future> getAll() async { + Future> getAll() async { state = state.copyWith(isLoading: true, error: null); try { @@ -118,4 +118,31 @@ class RemoteAlbumNotifier extends Notifier { .sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse); state = state.copyWith(filteredAlbums: sortedAlbums); } + + Future createAlbum({ + required String title, + String? description, + List assetIds = const [], + }) async { + state = state.copyWith(isLoading: true, error: null); + + try { + final album = await _remoteAlbumService.createAlbum( + title: title, + description: description, + assetIds: assetIds, + ); + + state = state.copyWith( + albums: [...state.albums, album], + filteredAlbums: [...state.filteredAlbums, album], + ); + + state = state.copyWith(isLoading: false); + return album; + } catch (e) { + state = state.copyWith(isLoading: false, error: e.toString()); + rethrow; + } + } } diff --git a/mobile/lib/repositories/album_api.repository.dart b/mobile/lib/repositories/album_api.repository.dart index 019e4dc63c..20365534c2 100644 --- a/mobile/lib/repositories/album_api.repository.dart +++ b/mobile/lib/repositories/album_api.repository.dart @@ -1,5 +1,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart' + show AlbumAssetOrder, RemoteAlbum; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart' @@ -50,6 +52,25 @@ class AlbumApiRepository extends ApiRepository { return _toAlbum(responseDto); } + // TODO: Change name after removing old method + Future createDriftAlbum( + String name, { + required Iterable assetIds, + String? description, + }) async { + final responseDto = await checkNull( + _api.createAlbum( + CreateAlbumDto( + albumName: name, + description: description, + assetIds: assetIds.toList(), + ), + ), + ); + + return _toRemoteAlbum(responseDto); + } + Future update( String albumId, { String? name, @@ -170,4 +191,22 @@ class AlbumApiRepository extends ApiRepository { return album; } + + static RemoteAlbum _toRemoteAlbum(AlbumResponseDto dto) { + return RemoteAlbum( + id: dto.id, + name: dto.albumName, + ownerId: dto.owner.id, + description: dto.description, + createdAt: dto.createdAt, + updatedAt: dto.updatedAt, + thumbnailAssetId: dto.albumThumbnailAssetId, + isActivityEnabled: dto.isActivityEnabled, + order: dto.order == AssetOrder.asc + ? AlbumAssetOrder.asc + : AlbumAssetOrder.desc, + assetCount: dto.assetCount, + ownerName: dto.owner.name, + ); + } } diff --git a/mobile/lib/repositories/drift_album_api_repository.dart b/mobile/lib/repositories/drift_album_api_repository.dart new file mode 100644 index 0000000000..7ef24f1e7c --- /dev/null +++ b/mobile/lib/repositories/drift_album_api_repository.dart @@ -0,0 +1,74 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/api.repository.dart'; +// ignore: import_rule_openapi +import 'package:openapi/api.dart'; + +final driftAlbumApiRepositoryProvider = Provider( + (ref) => DriftAlbumApiRepository(ref.watch(apiServiceProvider).albumsApi), +); + +class DriftAlbumApiRepository extends ApiRepository { + final AlbumsApi _api; + + DriftAlbumApiRepository(this._api); + + Future createDriftAlbum( + String name, { + required Iterable assetIds, + String? description, + }) async { + final responseDto = await checkNull( + _api.createAlbum( + CreateAlbumDto( + albumName: name, + description: description, + assetIds: assetIds.toList(), + ), + ), + ); + + return responseDto.toRemoteAlbum(); + } + + Future<({List removed, List failed})> removeAssets( + String albumId, + Iterable assetIds, + ) async { + final response = await checkNull( + _api.removeAssetFromAlbum( + albumId, + BulkIdsDto(ids: assetIds.toList()), + ), + ); + final List removed = [], failed = []; + for (final dto in response) { + if (dto.success) { + removed.add(dto.id); + } else { + failed.add(dto.id); + } + } + return (removed: removed, failed: failed); + } +} + +extension on AlbumResponseDto { + RemoteAlbum toRemoteAlbum() { + return RemoteAlbum( + id: id, + name: albumName, + ownerId: owner.id, + description: description, + createdAt: createdAt, + updatedAt: updatedAt, + thumbnailAssetId: albumThumbnailAssetId, + isActivityEnabled: isActivityEnabled, + order: + order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc, + assetCount: assetCount, + ownerName: owner.name, + ); + } +} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 18aa937a9d..5fcd060b0c 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -1,6 +1,8 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.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/log.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; @@ -67,22 +69,23 @@ import 'package:immich_mobile/pages/search/person_result.page.dart'; import 'package:immich_mobile/pages/search/recently_taken.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/drift_favorite.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/drift_partner_detail.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/drift_local_album.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/drift_recently_taken.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/drift_video.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/drift_trash.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/drift_archive.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/drift_locked_folder.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_favorite.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_partner_detail.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_local_album.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_video.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_trash.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_archive.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_locked_folder.page.dart'; import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/local_timeline.page.dart'; +import 'package:immich_mobile/presentation/pages/local_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/remote_timeline.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_library.page.dart'; import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_memory.page.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; @@ -387,7 +390,7 @@ class AppRouter extends RootStackRouter { guards: [_authGuard, _duplicateGuard], ), AutoRoute( - page: RemoteTimelineRoute.page, + page: RemoteAlbumRoute.page, guards: [_authGuard, _duplicateGuard], ), AutoRoute( @@ -446,6 +449,11 @@ class AppRouter extends RootStackRouter { page: DriftLocalAlbumsRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: DriftCreateAlbumRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + // required to handle all deeplinks in deep_link.service.dart // auto_route_library#1722 RedirectRoute(path: '*', redirectTo: '/'), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index b13320b1c0..bd2b148455 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -683,6 +683,22 @@ class DriftAssetSelectionTimelineRouteArgs { } } +/// generated route for +/// [DriftCreateAlbumPage] +class DriftCreateAlbumRoute extends PageRouteInfo { + const DriftCreateAlbumRoute({List? children}) + : super(DriftCreateAlbumRoute.name, initialChildren: children); + + static const String name = 'DriftCreateAlbumRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftCreateAlbumPage(); + }, + ); +} + /// generated route for /// [DriftFavoritePage] class DriftFavoriteRoute extends PageRouteInfo { @@ -805,11 +821,11 @@ class DriftPartnerDetailRoute extends PageRouteInfo { DriftPartnerDetailRoute({ Key? key, - required String partnerId, + required UserDto partner, List? children, }) : super( DriftPartnerDetailRoute.name, - args: DriftPartnerDetailRouteArgs(key: key, partnerId: partnerId), + args: DriftPartnerDetailRouteArgs(key: key, partner: partner), initialChildren: children, ); @@ -819,21 +835,21 @@ class DriftPartnerDetailRoute name, builder: (data) { final args = data.argsAs(); - return DriftPartnerDetailPage(key: args.key, partnerId: args.partnerId); + return DriftPartnerDetailPage(key: args.key, partner: args.partner); }, ); } class DriftPartnerDetailRouteArgs { - const DriftPartnerDetailRouteArgs({this.key, required this.partnerId}); + const DriftPartnerDetailRouteArgs({this.key, required this.partner}); final Key? key; - final String partnerId; + final UserDto partner; @override String toString() { - return 'DriftPartnerDetailRouteArgs{key: $key, partnerId: $partnerId}'; + return 'DriftPartnerDetailRouteArgs{key: $key, partner: $partner}'; } } @@ -1211,11 +1227,11 @@ class LocalMediaSummaryRoute extends PageRouteInfo { class LocalTimelineRoute extends PageRouteInfo { LocalTimelineRoute({ Key? key, - required String albumId, + required LocalAlbum album, List? children, }) : super( LocalTimelineRoute.name, - args: LocalTimelineRouteArgs(key: key, albumId: albumId), + args: LocalTimelineRouteArgs(key: key, album: album), initialChildren: children, ); @@ -1225,21 +1241,21 @@ class LocalTimelineRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return LocalTimelinePage(key: args.key, albumId: args.albumId); + return LocalTimelinePage(key: args.key, album: args.album); }, ); } class LocalTimelineRouteArgs { - const LocalTimelineRouteArgs({this.key, required this.albumId}); + const LocalTimelineRouteArgs({this.key, required this.album}); final Key? key; - final String albumId; + final LocalAlbum album; @override String toString() { - return 'LocalTimelineRouteArgs{key: $key, albumId: $albumId}'; + return 'LocalTimelineRouteArgs{key: $key, album: $album}'; } } @@ -1744,6 +1760,43 @@ class RecentlyTakenRoute extends PageRouteInfo { ); } +/// generated route for +/// [RemoteAlbumPage] +class RemoteAlbumRoute extends PageRouteInfo { + RemoteAlbumRoute({ + Key? key, + required RemoteAlbum album, + List? children, + }) : super( + RemoteAlbumRoute.name, + args: RemoteAlbumRouteArgs(key: key, album: album), + initialChildren: children, + ); + + static const String name = 'RemoteAlbumRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return RemoteAlbumPage(key: args.key, album: args.album); + }, + ); +} + +class RemoteAlbumRouteArgs { + const RemoteAlbumRouteArgs({this.key, required this.album}); + + final Key? key; + + final RemoteAlbum album; + + @override + String toString() { + return 'RemoteAlbumRouteArgs{key: $key, album: $album}'; + } +} + /// generated route for /// [RemoteMediaSummaryPage] class RemoteMediaSummaryRoute extends PageRouteInfo { @@ -1760,43 +1813,6 @@ class RemoteMediaSummaryRoute extends PageRouteInfo { ); } -/// generated route for -/// [RemoteTimelinePage] -class RemoteTimelineRoute extends PageRouteInfo { - RemoteTimelineRoute({ - Key? key, - required String albumId, - List? children, - }) : super( - RemoteTimelineRoute.name, - args: RemoteTimelineRouteArgs(key: key, albumId: albumId), - initialChildren: children, - ); - - static const String name = 'RemoteTimelineRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return RemoteTimelinePage(key: args.key, albumId: args.albumId); - }, - ); -} - -class RemoteTimelineRouteArgs { - const RemoteTimelineRouteArgs({this.key, required this.albumId}); - - final Key? key; - - final String albumId; - - @override - String toString() { - return 'RemoteTimelineRouteArgs{key: $key, albumId: $albumId}'; - } -} - /// generated route for /// [SearchPage] class SearchRoute extends PageRouteInfo { diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 2f4c8cc926..d7c625b981 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -2,9 +2,12 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/location_picker.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -14,16 +17,22 @@ final actionServiceProvider = Provider( (ref) => ActionService( ref.watch(assetApiRepositoryProvider), ref.watch(remoteAssetRepositoryProvider), + ref.watch(driftAlbumApiRepositoryProvider), + ref.watch(remoteAlbumRepository), ), ); class ActionService { final AssetApiRepository _assetApiRepository; final RemoteAssetRepository _remoteAssetRepository; + final DriftAlbumApiRepository _albumApiRepository; + final DriftRemoteAlbumRepository _remoteAlbumRepository; const ActionService( this._assetApiRepository, this._remoteAssetRepository, + this._albumApiRepository, + this._remoteAlbumRepository, ); Future shareLink(List remoteIds, BuildContext context) async { @@ -131,4 +140,16 @@ class ActionService { return true; } + + Future removeFromAlbum(List remoteIds, String albumId) async { + int removedCount = 0; + final result = await _albumApiRepository.removeAssets(albumId, remoteIds); + + if (result.removed.isNotEmpty) { + removedCount = + await _remoteAlbumRepository.removeAssets(albumId, result.removed); + } + + return removedCount; + } } diff --git a/mobile/lib/utils/remote_album.utils.dart b/mobile/lib/utils/remote_album.utils.dart index 4fc7ba5f74..04184ee367 100644 --- a/mobile/lib/utils/remote_album.utils.dart +++ b/mobile/lib/utils/remote_album.utils.dart @@ -1,38 +1,56 @@ import 'package:collection/collection.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; -typedef AlbumSortFn = List Function(List albums, bool isReverse); +typedef AlbumSortFn = List Function( + List albums, + bool isReverse, +); class _RemoteAlbumSortHandlers { const _RemoteAlbumSortHandlers._(); static const AlbumSortFn created = _sortByCreated; - static List _sortByCreated(List albums, bool isReverse) { + static List _sortByCreated( + List albums, + bool isReverse, + ) { final sorted = albums.sortedBy((album) => album.createdAt); return (isReverse ? sorted.reversed : sorted).toList(); } static const AlbumSortFn title = _sortByTitle; - static List _sortByTitle(List albums, bool isReverse) { + static List _sortByTitle( + List albums, + bool isReverse, + ) { final sorted = albums.sortedBy((album) => album.name); return (isReverse ? sorted.reversed : sorted).toList(); } static const AlbumSortFn lastModified = _sortByLastModified; - static List _sortByLastModified(List albums, bool isReverse) { + static List _sortByLastModified( + List albums, + bool isReverse, + ) { final sorted = albums.sortedBy((album) => album.updatedAt); return (isReverse ? sorted.reversed : sorted).toList(); } static const AlbumSortFn assetCount = _sortByAssetCount; - static List _sortByAssetCount(List albums, bool isReverse) { + static List _sortByAssetCount( + List albums, + bool isReverse, + ) { final sorted = albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount)); return (isReverse ? sorted.reversed : sorted).toList(); } static const AlbumSortFn mostRecent = _sortByMostRecent; - static List _sortByMostRecent(List albums, bool isReverse) { + static List _sortByMostRecent( + List albums, + bool isReverse, + ) { final sorted = albums.sorted((a, b) { // For most recent, we sort by updatedAt in descending order return b.updatedAt.compareTo(a.updatedAt); @@ -41,7 +59,10 @@ class _RemoteAlbumSortHandlers { } static const AlbumSortFn mostOldest = _sortByMostOldest; - static List _sortByMostOldest(List albums, bool isReverse) { + static List _sortByMostOldest( + List albums, + bool isReverse, + ) { final sorted = albums.sorted((a, b) { // For oldest, we sort by createdAt in ascending order return a.createdAt.compareTo(b.createdAt); diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index dbf088c112..799cf17f3f 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -185,7 +186,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { child: action, ), ), - if (kDebugMode || kProfileMode) + if (kDebugMode || kProfileMode || appFlavor == 'beta') IconButton( icon: const Icon(Icons.science_rounded), onPressed: () => context.pushRoute(const FeatInDevRoute()), diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index ff0e88e5d7..c2c5b79753 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -73,7 +73,15 @@ class ImmichSliverAppBar extends ConsumerWidget { onPressed: () => context.pop(), ), IconButton( - onPressed: () => ref.read(backgroundSyncProvider).syncRemote(), + onPressed: () { + ref.read(backgroundSyncProvider).syncLocal(full: true); + ref.read(backgroundSyncProvider).syncRemote(); + + Future.delayed( + const Duration(seconds: 10), + () => ref.read(backgroundSyncProvider).hashAssets(), + ); + }, icon: const Icon( Icons.sync, ), diff --git a/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart new file mode 100644 index 0000000000..36f944dbcd --- /dev/null +++ b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart @@ -0,0 +1,563 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; + +class MesmerizingSliverAppBar extends ConsumerStatefulWidget { + const MesmerizingSliverAppBar({ + super.key, + required this.title, + this.icon = Icons.camera, + }); + + final String title; + final IconData icon; + + @override + ConsumerState createState() => + _MesmerizingSliverAppBarState(); +} + +class _MesmerizingSliverAppBarState + extends ConsumerState { + double _scrollProgress = 0.0; + + double _calculateScrollProgress(FlexibleSpaceBarSettings? settings) { + if (settings?.maxExtent == null || settings?.minExtent == null) { + return 1.0; + } + + final deltaExtent = settings!.maxExtent - settings.minExtent; + if (deltaExtent <= 0.0) { + return 1.0; + } + + return (1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent) + .clamp(0.0, 1.0); + } + + @override + Widget build(BuildContext context) { + final isMultiSelectEnabled = + ref.watch(multiSelectProvider.select((s) => s.isEnabled)); + + return isMultiSelectEnabled + ? SliverToBoxAdapter( + child: switch (_scrollProgress) { + < 0.8 => const SizedBox(height: 120), + _ => const SizedBox(height: 352), + }, + ) + : SliverAppBar( + expandedHeight: 300.0, + floating: false, + pinned: true, + snap: false, + elevation: 0, + leading: IconButton( + icon: Icon( + Platform.isIOS + ? Icons.arrow_back_ios_new_rounded + : Icons.arrow_back, + color: Color.lerp( + Colors.white, + context.primaryColor, + _scrollProgress, + ), + shadows: [ + _scrollProgress < 0.95 + ? Shadow( + offset: const Offset(0, 2), + blurRadius: 5, + color: Colors.black.withValues(alpha: 0.5), + ) + : const Shadow( + offset: Offset(0, 2), + blurRadius: 0, + color: Colors.transparent, + ), + ], + ), + onPressed: () { + context.pop(); + }, + ), + flexibleSpace: Builder( + builder: (context) { + final settings = context.dependOnInheritedWidgetOfExactType< + FlexibleSpaceBarSettings>(); + final scrollProgress = _calculateScrollProgress(settings); + + // Update scroll progress for the leading button + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && _scrollProgress != scrollProgress) { + setState(() { + _scrollProgress = scrollProgress; + }); + } + }); + + return FlexibleSpaceBar( + centerTitle: true, + title: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: scrollProgress > 0.95 + ? Text( + widget.title, + style: TextStyle( + color: context.primaryColor, + fontWeight: FontWeight.w600, + fontSize: 18, + ), + ) + : null, + ), + background: _ExpandedBackground( + scrollProgress: scrollProgress, + title: widget.title, + icon: widget.icon, + ), + ); + }, + ), + ); + } +} + +class _ExpandedBackground extends ConsumerStatefulWidget { + final double scrollProgress; + final String title; + final IconData icon; + + const _ExpandedBackground({ + required this.scrollProgress, + required this.title, + required this.icon, + }); + + @override + ConsumerState<_ExpandedBackground> createState() => + _ExpandedBackgroundState(); +} + +class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> + with SingleTickerProviderStateMixin { + late AnimationController _slideController; + late Animation _slideAnimation; + + @override + void initState() { + super.initState(); + + _slideController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + _slideAnimation = Tween( + begin: const Offset(0, 1.5), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _slideController, + curve: Curves.easeOutCubic, + ), + ); + + Future.delayed(const Duration(milliseconds: 100), () { + if (mounted) { + _slideController.forward(); + } + }); + } + + @override + void dispose() { + _slideController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final timelineService = ref.watch(timelineServiceProvider); + + return Stack( + fit: StackFit.expand, + children: [ + Transform.translate( + offset: Offset(0, widget.scrollProgress * 50), + child: Transform.scale( + scale: 1.4 - (widget.scrollProgress * 0.2), + child: _RandomAssetBackground( + timelineService: timelineService, + icon: widget.icon, + ), + ), + ), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.transparent, + Colors.black.withValues( + alpha: 0.6 + (widget.scrollProgress * 0.2), + ), + ], + stops: const [0.0, 0.65, 1.0], + ), + ), + ), + Positioned( + bottom: 16, + left: 16, + right: 16, + child: SlideTransition( + position: _slideAnimation, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: double.infinity, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Text( + widget.title, + maxLines: 1, + style: const TextStyle( + color: Colors.white, + fontSize: 36, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + shadows: [ + Shadow( + offset: Offset(0, 2), + blurRadius: 12, + color: Colors.black45, + ), + ], + ), + ), + ), + ), + AnimatedContainer( + duration: const Duration(milliseconds: 300), + child: const _ItemCountText(), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _ItemCountText extends ConsumerStatefulWidget { + const _ItemCountText(); + + @override + ConsumerState<_ItemCountText> createState() => _ItemCountTextState(); +} + +class _ItemCountTextState extends ConsumerState<_ItemCountText> { + StreamSubscription? _reloadSubscription; + + @override + void initState() { + super.initState(); + _reloadSubscription = + EventStream.shared.listen((_) => setState(() {})); + } + + @override + void dispose() { + _reloadSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final assetCount = ref.watch( + timelineServiceProvider.select((s) => s.totalAssets), + ); + + return Text( + 'items_count'.t( + context: context, + args: {"count": assetCount}, + ), + style: context.textTheme.labelLarge?.copyWith( + // letterSpacing: 0.2, + fontWeight: FontWeight.bold, + color: Colors.white, + shadows: [ + const Shadow( + offset: Offset(0, 1), + blurRadius: 6, + color: Colors.black45, + ), + ], + ), + ); + } +} + +class _RandomAssetBackground extends StatefulWidget { + final TimelineService timelineService; + final IconData icon; + + const _RandomAssetBackground({ + required this.timelineService, + required this.icon, + }); + + @override + State<_RandomAssetBackground> createState() => _RandomAssetBackgroundState(); +} + +class _RandomAssetBackgroundState extends State<_RandomAssetBackground> + with TickerProviderStateMixin { + late AnimationController _zoomController; + late AnimationController _crossFadeController; + late Animation _zoomAnimation; + late Animation _panAnimation; + late Animation _crossFadeAnimation; + BaseAsset? _currentAsset; + BaseAsset? _nextAsset; + bool _isZoomingIn = true; + + @override + void initState() { + super.initState(); + + _zoomController = AnimationController( + duration: const Duration(seconds: 12), + vsync: this, + ); + + _crossFadeController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + ); + + _zoomAnimation = Tween( + begin: 1.0, + end: 1.2, + ).animate( + CurvedAnimation( + parent: _zoomController, + curve: Curves.easeInOut, + ), + ); + + _panAnimation = Tween( + begin: Offset.zero, + end: const Offset(0.5, -0.5), + ).animate( + CurvedAnimation( + parent: _zoomController, + curve: Curves.easeInOut, + ), + ); + + _crossFadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate( + CurvedAnimation( + parent: _crossFadeController, + curve: Curves.easeInOutCubic, + ), + ); + + Future.delayed( + Durations.medium1, + () => _loadFirstAsset(), + ); + } + + @override + void dispose() { + _zoomController.dispose(); + _crossFadeController.dispose(); + super.dispose(); + } + + void _startAnimationCycle() { + if (_isZoomingIn) { + _zoomController.forward().then((_) { + _loadNextAsset(); + }); + } else { + _zoomController.reverse().then((_) { + _loadNextAsset(); + }); + } + } + + Future _loadFirstAsset() async { + if (!mounted) { + return; + } + + if (widget.timelineService.totalAssets == 0) { + setState(() { + _currentAsset = null; + }); + + return; + } + + setState(() { + _currentAsset = widget.timelineService.getRandomAsset(); + }); + + await _crossFadeController.forward(); + + if (_zoomController.status == AnimationStatus.dismissed) { + if (_isZoomingIn) { + _zoomController.reset(); + } else { + _zoomController.value = 1.0; + } + _startAnimationCycle(); + } + } + + Future _loadNextAsset() async { + if (!mounted) { + return; + } + + try { + if (widget.timelineService.totalAssets > 1) { + // Load next asset while keeping current one visible + final nextAsset = widget.timelineService.getRandomAsset(); + + setState(() { + _nextAsset = nextAsset; + }); + + await _crossFadeController.reverse(); + setState(() { + _currentAsset = _nextAsset; + _nextAsset = null; + }); + + _crossFadeController.value = 1.0; + + _isZoomingIn = !_isZoomingIn; + + _startAnimationCycle(); + } + } catch (e) { + _zoomController.reset(); + _startAnimationCycle(); + } + } + + @override + Widget build(BuildContext context) { + if (widget.timelineService.totalAssets == 0) { + return const SizedBox.shrink(); + } + + return AnimatedBuilder( + animation: Listenable.merge( + [_zoomAnimation, _panAnimation, _crossFadeAnimation], + ), + builder: (context, child) { + return Transform.scale( + scale: _zoomAnimation.value, + filterQuality: FilterQuality.low, + child: Transform.translate( + offset: _panAnimation.value, + filterQuality: FilterQuality.low, + child: Stack( + fit: StackFit.expand, + children: [ + // Current image + if (_currentAsset != null) + Opacity( + opacity: _crossFadeAnimation.value, + child: SizedBox( + width: double.infinity, + height: double.infinity, + child: Image( + alignment: Alignment.topRight, + image: getFullImageProvider(_currentAsset!), + fit: BoxFit.cover, + frameBuilder: + (context, child, frame, wasSynchronouslyLoaded) { + if (wasSynchronouslyLoaded || frame != null) { + return child; + } + return Container(); + }, + errorBuilder: (context, error, stackTrace) { + return SizedBox( + width: double.infinity, + height: double.infinity, + child: Icon( + Icons.error_outline_rounded, + size: 24, + color: Colors.red[300], + ), + ); + }, + ), + ), + ), + + if (_nextAsset != null) + Opacity( + opacity: 1.0 - _crossFadeAnimation.value, + child: SizedBox( + width: double.infinity, + height: double.infinity, + child: Image( + alignment: Alignment.topRight, + image: getFullImageProvider(_nextAsset!), + fit: BoxFit.cover, + frameBuilder: + (context, child, frame, wasSynchronouslyLoaded) { + if (wasSynchronouslyLoaded || frame != null) { + return child; + } + return const SizedBox.shrink(); + }, + errorBuilder: (context, error, stackTrace) { + return SizedBox( + width: double.infinity, + height: double.infinity, + child: Icon( + Icons.error_outline_rounded, + size: 24, + color: Colors.red[300], + ), + ); + }, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/mobile/makefile b/mobile/makefile index 64992ec946..37d33fa817 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -28,4 +28,7 @@ translation: dart run easy_localization:generate -S ../i18n dart run bin/generate_keys.dart dart format lib/generated/codegen_loader.g.dart - dart format lib/generated/intl_keys.g.dart \ No newline at end of file + dart format lib/generated/intl_keys.g.dart + +build-beta: + flutter build apk --flavor beta --release diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 1beaec0ae6..28fa63ba84 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -208,6 +208,7 @@ Class | Method | HTTP request | Description *SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} | *SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions | *SessionsApi* | [**lockSession**](doc//SessionsApi.md#locksession) | **POST** /sessions/{id}/lock | +*SessionsApi* | [**updateSession**](doc//SessionsApi.md#updatesession) | **PUT** /sessions/{id} | *SharedLinksApi* | [**addSharedLinkAssets**](doc//SharedLinksApi.md#addsharedlinkassets) | **PUT** /shared-links/{id}/assets | *SharedLinksApi* | [**createSharedLink**](doc//SharedLinksApi.md#createsharedlink) | **POST** /shared-links | *SharedLinksApi* | [**getAllSharedLinks**](doc//SharedLinksApi.md#getallsharedlinks) | **GET** /shared-links | @@ -449,6 +450,7 @@ Class | Method | HTTP request | Description - [SessionCreateResponseDto](doc//SessionCreateResponseDto.md) - [SessionResponseDto](doc//SessionResponseDto.md) - [SessionUnlockDto](doc//SessionUnlockDto.md) + - [SessionUpdateDto](doc//SessionUpdateDto.md) - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) - [SharedLinkEditDto](doc//SharedLinkEditDto.md) - [SharedLinkResponseDto](doc//SharedLinkResponseDto.md) @@ -481,11 +483,15 @@ Class | Method | HTTP request | Description - [SyncMemoryV1](doc//SyncMemoryV1.md) - [SyncPartnerDeleteV1](doc//SyncPartnerDeleteV1.md) - [SyncPartnerV1](doc//SyncPartnerV1.md) + - [SyncPersonDeleteV1](doc//SyncPersonDeleteV1.md) + - [SyncPersonV1](doc//SyncPersonV1.md) - [SyncRequestType](doc//SyncRequestType.md) - [SyncStackDeleteV1](doc//SyncStackDeleteV1.md) - [SyncStackV1](doc//SyncStackV1.md) - [SyncStreamDto](doc//SyncStreamDto.md) - [SyncUserDeleteV1](doc//SyncUserDeleteV1.md) + - [SyncUserMetadataDeleteV1](doc//SyncUserMetadataDeleteV1.md) + - [SyncUserMetadataV1](doc//SyncUserMetadataV1.md) - [SyncUserV1](doc//SyncUserV1.md) - [SystemConfigBackupsDto](doc//SystemConfigBackupsDto.md) - [SystemConfigDto](doc//SystemConfigDto.md) @@ -503,6 +509,7 @@ Class | Method | HTTP request | Description - [SystemConfigMapDto](doc//SystemConfigMapDto.md) - [SystemConfigMetadataDto](doc//SystemConfigMetadataDto.md) - [SystemConfigNewVersionCheckDto](doc//SystemConfigNewVersionCheckDto.md) + - [SystemConfigNightlyTasksDto](doc//SystemConfigNightlyTasksDto.md) - [SystemConfigNotificationsDto](doc//SystemConfigNotificationsDto.md) - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md) - [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 3998961720..becafa06bf 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -232,6 +232,7 @@ part 'model/session_create_dto.dart'; part 'model/session_create_response_dto.dart'; part 'model/session_response_dto.dart'; part 'model/session_unlock_dto.dart'; +part 'model/session_update_dto.dart'; part 'model/shared_link_create_dto.dart'; part 'model/shared_link_edit_dto.dart'; part 'model/shared_link_response_dto.dart'; @@ -264,11 +265,15 @@ part 'model/sync_memory_delete_v1.dart'; part 'model/sync_memory_v1.dart'; part 'model/sync_partner_delete_v1.dart'; part 'model/sync_partner_v1.dart'; +part 'model/sync_person_delete_v1.dart'; +part 'model/sync_person_v1.dart'; part 'model/sync_request_type.dart'; part 'model/sync_stack_delete_v1.dart'; part 'model/sync_stack_v1.dart'; part 'model/sync_stream_dto.dart'; part 'model/sync_user_delete_v1.dart'; +part 'model/sync_user_metadata_delete_v1.dart'; +part 'model/sync_user_metadata_v1.dart'; part 'model/sync_user_v1.dart'; part 'model/system_config_backups_dto.dart'; part 'model/system_config_dto.dart'; @@ -286,6 +291,7 @@ part 'model/system_config_machine_learning_dto.dart'; part 'model/system_config_map_dto.dart'; part 'model/system_config_metadata_dto.dart'; part 'model/system_config_new_version_check_dto.dart'; +part 'model/system_config_nightly_tasks_dto.dart'; part 'model/system_config_notifications_dto.dart'; part 'model/system_config_o_auth_dto.dart'; part 'model/system_config_password_login_dto.dart'; diff --git a/mobile/openapi/lib/api/sessions_api.dart b/mobile/openapi/lib/api/sessions_api.dart index 3228d31e91..d54f520641 100644 --- a/mobile/openapi/lib/api/sessions_api.dart +++ b/mobile/openapi/lib/api/sessions_api.dart @@ -219,4 +219,56 @@ class SessionsApi { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } } + + /// Performs an HTTP 'PUT /sessions/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [SessionUpdateDto] sessionUpdateDto (required): + Future updateSessionWithHttpInfo(String id, SessionUpdateDto sessionUpdateDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/sessions/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = sessionUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [SessionUpdateDto] sessionUpdateDto (required): + Future updateSession(String id, SessionUpdateDto sessionUpdateDto,) async { + final response = await updateSessionWithHttpInfo(id, sessionUpdateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // 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), 'SessionResponseDto',) as SessionResponseDto; + + } + return null; + } } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 0edc2638bc..603163f00e 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -520,6 +520,8 @@ class ApiClient { return SessionResponseDto.fromJson(value); case 'SessionUnlockDto': return SessionUnlockDto.fromJson(value); + case 'SessionUpdateDto': + return SessionUpdateDto.fromJson(value); case 'SharedLinkCreateDto': return SharedLinkCreateDto.fromJson(value); case 'SharedLinkEditDto': @@ -584,6 +586,10 @@ class ApiClient { return SyncPartnerDeleteV1.fromJson(value); case 'SyncPartnerV1': return SyncPartnerV1.fromJson(value); + case 'SyncPersonDeleteV1': + return SyncPersonDeleteV1.fromJson(value); + case 'SyncPersonV1': + return SyncPersonV1.fromJson(value); case 'SyncRequestType': return SyncRequestTypeTypeTransformer().decode(value); case 'SyncStackDeleteV1': @@ -594,6 +600,10 @@ class ApiClient { return SyncStreamDto.fromJson(value); case 'SyncUserDeleteV1': return SyncUserDeleteV1.fromJson(value); + case 'SyncUserMetadataDeleteV1': + return SyncUserMetadataDeleteV1.fromJson(value); + case 'SyncUserMetadataV1': + return SyncUserMetadataV1.fromJson(value); case 'SyncUserV1': return SyncUserV1.fromJson(value); case 'SystemConfigBackupsDto': @@ -628,6 +638,8 @@ class ApiClient { return SystemConfigMetadataDto.fromJson(value); case 'SystemConfigNewVersionCheckDto': return SystemConfigNewVersionCheckDto.fromJson(value); + case 'SystemConfigNightlyTasksDto': + return SystemConfigNightlyTasksDto.fromJson(value); case 'SystemConfigNotificationsDto': return SystemConfigNotificationsDto.fromJson(value); case 'SystemConfigOAuthDto': diff --git a/mobile/openapi/lib/model/session_create_response_dto.dart b/mobile/openapi/lib/model/session_create_response_dto.dart index ab1c4ca2d8..a4f93e8d9c 100644 --- a/mobile/openapi/lib/model/session_create_response_dto.dart +++ b/mobile/openapi/lib/model/session_create_response_dto.dart @@ -19,6 +19,7 @@ class SessionCreateResponseDto { required this.deviceType, this.expiresAt, required this.id, + required this.isPendingSyncReset, required this.token, required this.updatedAt, }); @@ -41,6 +42,8 @@ class SessionCreateResponseDto { String id; + bool isPendingSyncReset; + String token; String updatedAt; @@ -53,6 +56,7 @@ class SessionCreateResponseDto { other.deviceType == deviceType && other.expiresAt == expiresAt && other.id == id && + other.isPendingSyncReset == isPendingSyncReset && other.token == token && other.updatedAt == updatedAt; @@ -65,11 +69,12 @@ class SessionCreateResponseDto { (deviceType.hashCode) + (expiresAt == null ? 0 : expiresAt!.hashCode) + (id.hashCode) + + (isPendingSyncReset.hashCode) + (token.hashCode) + (updatedAt.hashCode); @override - String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, token=$token, updatedAt=$updatedAt]'; + String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, token=$token, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -83,6 +88,7 @@ class SessionCreateResponseDto { // json[r'expiresAt'] = null; } json[r'id'] = this.id; + json[r'isPendingSyncReset'] = this.isPendingSyncReset; json[r'token'] = this.token; json[r'updatedAt'] = this.updatedAt; return json; @@ -103,6 +109,7 @@ class SessionCreateResponseDto { deviceType: mapValueOfType(json, r'deviceType')!, expiresAt: mapValueOfType(json, r'expiresAt'), id: mapValueOfType(json, r'id')!, + isPendingSyncReset: mapValueOfType(json, r'isPendingSyncReset')!, token: mapValueOfType(json, r'token')!, updatedAt: mapValueOfType(json, r'updatedAt')!, ); @@ -157,6 +164,7 @@ class SessionCreateResponseDto { 'deviceOS', 'deviceType', 'id', + 'isPendingSyncReset', 'token', 'updatedAt', }; diff --git a/mobile/openapi/lib/model/session_response_dto.dart b/mobile/openapi/lib/model/session_response_dto.dart index cf9eb08a78..e76e4d48b4 100644 --- a/mobile/openapi/lib/model/session_response_dto.dart +++ b/mobile/openapi/lib/model/session_response_dto.dart @@ -19,6 +19,7 @@ class SessionResponseDto { required this.deviceType, this.expiresAt, required this.id, + required this.isPendingSyncReset, required this.updatedAt, }); @@ -40,6 +41,8 @@ class SessionResponseDto { String id; + bool isPendingSyncReset; + String updatedAt; @override @@ -50,6 +53,7 @@ class SessionResponseDto { other.deviceType == deviceType && other.expiresAt == expiresAt && other.id == id && + other.isPendingSyncReset == isPendingSyncReset && other.updatedAt == updatedAt; @override @@ -61,10 +65,11 @@ class SessionResponseDto { (deviceType.hashCode) + (expiresAt == null ? 0 : expiresAt!.hashCode) + (id.hashCode) + + (isPendingSyncReset.hashCode) + (updatedAt.hashCode); @override - String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, updatedAt=$updatedAt]'; + String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -78,6 +83,7 @@ class SessionResponseDto { // json[r'expiresAt'] = null; } json[r'id'] = this.id; + json[r'isPendingSyncReset'] = this.isPendingSyncReset; json[r'updatedAt'] = this.updatedAt; return json; } @@ -97,6 +103,7 @@ class SessionResponseDto { deviceType: mapValueOfType(json, r'deviceType')!, expiresAt: mapValueOfType(json, r'expiresAt'), id: mapValueOfType(json, r'id')!, + isPendingSyncReset: mapValueOfType(json, r'isPendingSyncReset')!, updatedAt: mapValueOfType(json, r'updatedAt')!, ); } @@ -150,6 +157,7 @@ class SessionResponseDto { 'deviceOS', 'deviceType', 'id', + 'isPendingSyncReset', 'updatedAt', }; } diff --git a/mobile/openapi/lib/model/session_update_dto.dart b/mobile/openapi/lib/model/session_update_dto.dart new file mode 100644 index 0000000000..cd170b1baa --- /dev/null +++ b/mobile/openapi/lib/model/session_update_dto.dart @@ -0,0 +1,108 @@ +// +// 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 SessionUpdateDto { + /// Returns a new [SessionUpdateDto] instance. + SessionUpdateDto({ + this.isPendingSyncReset, + }); + + /// + /// 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. + /// + bool? isPendingSyncReset; + + @override + bool operator ==(Object other) => identical(this, other) || other is SessionUpdateDto && + other.isPendingSyncReset == isPendingSyncReset; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (isPendingSyncReset == null ? 0 : isPendingSyncReset!.hashCode); + + @override + String toString() => 'SessionUpdateDto[isPendingSyncReset=$isPendingSyncReset]'; + + Map toJson() { + final json = {}; + if (this.isPendingSyncReset != null) { + json[r'isPendingSyncReset'] = this.isPendingSyncReset; + } else { + // json[r'isPendingSyncReset'] = null; + } + return json; + } + + /// Returns a new [SessionUpdateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SessionUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "SessionUpdateDto"); + if (value is Map) { + final json = value.cast(); + + return SessionUpdateDto( + isPendingSyncReset: mapValueOfType(json, r'isPendingSyncReset'), + ); + } + 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 = SessionUpdateDto.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 = SessionUpdateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SessionUpdateDto-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] = SessionUpdateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/sync_asset_v1.dart b/mobile/openapi/lib/model/sync_asset_v1.dart index 1ca6e20cff..4c42d08a5f 100644 --- a/mobile/openapi/lib/model/sync_asset_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_v1.dart @@ -20,9 +20,11 @@ class SyncAssetV1 { required this.fileModifiedAt, required this.id, required this.isFavorite, + required this.livePhotoVideoId, required this.localDateTime, required this.originalFileName, required this.ownerId, + required this.stackId, required this.thumbhash, required this.type, required this.visibility, @@ -42,12 +44,16 @@ class SyncAssetV1 { bool isFavorite; + String? livePhotoVideoId; + DateTime? localDateTime; String originalFileName; String ownerId; + String? stackId; + String? thumbhash; AssetTypeEnum type; @@ -63,9 +69,11 @@ class SyncAssetV1 { other.fileModifiedAt == fileModifiedAt && other.id == id && other.isFavorite == isFavorite && + other.livePhotoVideoId == livePhotoVideoId && other.localDateTime == localDateTime && other.originalFileName == originalFileName && other.ownerId == ownerId && + other.stackId == stackId && other.thumbhash == thumbhash && other.type == type && other.visibility == visibility; @@ -80,15 +88,17 @@ class SyncAssetV1 { (fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) + (id.hashCode) + (isFavorite.hashCode) + + (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) + (localDateTime == null ? 0 : localDateTime!.hashCode) + (originalFileName.hashCode) + (ownerId.hashCode) + + (stackId == null ? 0 : stackId!.hashCode) + (thumbhash == null ? 0 : thumbhash!.hashCode) + (type.hashCode) + (visibility.hashCode); @override - String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, thumbhash=$thumbhash, type=$type, visibility=$visibility]'; + String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility]'; Map toJson() { final json = {}; @@ -115,6 +125,11 @@ class SyncAssetV1 { } json[r'id'] = this.id; json[r'isFavorite'] = this.isFavorite; + if (this.livePhotoVideoId != null) { + json[r'livePhotoVideoId'] = this.livePhotoVideoId; + } else { + // json[r'livePhotoVideoId'] = null; + } if (this.localDateTime != null) { json[r'localDateTime'] = this.localDateTime!.toUtc().toIso8601String(); } else { @@ -122,6 +137,11 @@ class SyncAssetV1 { } json[r'originalFileName'] = this.originalFileName; json[r'ownerId'] = this.ownerId; + if (this.stackId != null) { + json[r'stackId'] = this.stackId; + } else { + // json[r'stackId'] = null; + } if (this.thumbhash != null) { json[r'thumbhash'] = this.thumbhash; } else { @@ -148,9 +168,11 @@ class SyncAssetV1 { fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''), id: mapValueOfType(json, r'id')!, isFavorite: mapValueOfType(json, r'isFavorite')!, + livePhotoVideoId: mapValueOfType(json, r'livePhotoVideoId'), localDateTime: mapDateTime(json, r'localDateTime', r''), originalFileName: mapValueOfType(json, r'originalFileName')!, ownerId: mapValueOfType(json, r'ownerId')!, + stackId: mapValueOfType(json, r'stackId'), thumbhash: mapValueOfType(json, r'thumbhash'), type: AssetTypeEnum.fromJson(json[r'type'])!, visibility: AssetVisibility.fromJson(json[r'visibility'])!, @@ -208,9 +230,11 @@ class SyncAssetV1 { 'fileModifiedAt', 'id', 'isFavorite', + 'livePhotoVideoId', 'localDateTime', 'originalFileName', 'ownerId', + 'stackId', 'thumbhash', 'type', 'visibility', diff --git a/mobile/openapi/lib/model/sync_entity_type.dart b/mobile/openapi/lib/model/sync_entity_type.dart index ecaadc9c31..61f94401c7 100644 --- a/mobile/openapi/lib/model/sync_entity_type.dart +++ b/mobile/openapi/lib/model/sync_entity_type.dart @@ -56,7 +56,12 @@ class SyncEntityType { static const memoryToAssetDeleteV1 = SyncEntityType._(r'MemoryToAssetDeleteV1'); static const stackV1 = SyncEntityType._(r'StackV1'); static const stackDeleteV1 = SyncEntityType._(r'StackDeleteV1'); + static const personV1 = SyncEntityType._(r'PersonV1'); + static const personDeleteV1 = SyncEntityType._(r'PersonDeleteV1'); + static const userMetadataV1 = SyncEntityType._(r'UserMetadataV1'); + static const userMetadataDeleteV1 = SyncEntityType._(r'UserMetadataDeleteV1'); static const syncAckV1 = SyncEntityType._(r'SyncAckV1'); + static const syncResetV1 = SyncEntityType._(r'SyncResetV1'); /// List of all possible values in this [enum][SyncEntityType]. static const values = [ @@ -93,7 +98,12 @@ class SyncEntityType { memoryToAssetDeleteV1, stackV1, stackDeleteV1, + personV1, + personDeleteV1, + userMetadataV1, + userMetadataDeleteV1, syncAckV1, + syncResetV1, ]; static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value); @@ -165,7 +175,12 @@ class SyncEntityTypeTypeTransformer { case r'MemoryToAssetDeleteV1': return SyncEntityType.memoryToAssetDeleteV1; case r'StackV1': return SyncEntityType.stackV1; case r'StackDeleteV1': return SyncEntityType.stackDeleteV1; + case r'PersonV1': return SyncEntityType.personV1; + case r'PersonDeleteV1': return SyncEntityType.personDeleteV1; + case r'UserMetadataV1': return SyncEntityType.userMetadataV1; + case r'UserMetadataDeleteV1': return SyncEntityType.userMetadataDeleteV1; case r'SyncAckV1': return SyncEntityType.syncAckV1; + case r'SyncResetV1': return SyncEntityType.syncResetV1; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/sync_person_delete_v1.dart b/mobile/openapi/lib/model/sync_person_delete_v1.dart new file mode 100644 index 0000000000..002f5c5b83 --- /dev/null +++ b/mobile/openapi/lib/model/sync_person_delete_v1.dart @@ -0,0 +1,99 @@ +// +// 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 SyncPersonDeleteV1 { + /// Returns a new [SyncPersonDeleteV1] instance. + SyncPersonDeleteV1({ + required this.personId, + }); + + String personId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncPersonDeleteV1 && + other.personId == personId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (personId.hashCode); + + @override + String toString() => 'SyncPersonDeleteV1[personId=$personId]'; + + Map toJson() { + final json = {}; + json[r'personId'] = this.personId; + return json; + } + + /// Returns a new [SyncPersonDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncPersonDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncPersonDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncPersonDeleteV1( + personId: mapValueOfType(json, r'personId')!, + ); + } + 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 = SyncPersonDeleteV1.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 = SyncPersonDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncPersonDeleteV1-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] = SyncPersonDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'personId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_person_v1.dart b/mobile/openapi/lib/model/sync_person_v1.dart new file mode 100644 index 0000000000..e86c22f64b --- /dev/null +++ b/mobile/openapi/lib/model/sync_person_v1.dart @@ -0,0 +1,191 @@ +// +// 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 SyncPersonV1 { + /// Returns a new [SyncPersonV1] instance. + SyncPersonV1({ + required this.birthDate, + required this.color, + required this.createdAt, + required this.faceAssetId, + required this.id, + required this.isFavorite, + required this.isHidden, + required this.name, + required this.ownerId, + required this.thumbnailPath, + required this.updatedAt, + }); + + DateTime? birthDate; + + String? color; + + DateTime createdAt; + + String? faceAssetId; + + String id; + + bool isFavorite; + + bool isHidden; + + String name; + + String ownerId; + + String thumbnailPath; + + DateTime updatedAt; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncPersonV1 && + other.birthDate == birthDate && + other.color == color && + other.createdAt == createdAt && + other.faceAssetId == faceAssetId && + other.id == id && + other.isFavorite == isFavorite && + other.isHidden == isHidden && + other.name == name && + other.ownerId == ownerId && + other.thumbnailPath == thumbnailPath && + other.updatedAt == updatedAt; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (birthDate == null ? 0 : birthDate!.hashCode) + + (color == null ? 0 : color!.hashCode) + + (createdAt.hashCode) + + (faceAssetId == null ? 0 : faceAssetId!.hashCode) + + (id.hashCode) + + (isFavorite.hashCode) + + (isHidden.hashCode) + + (name.hashCode) + + (ownerId.hashCode) + + (thumbnailPath.hashCode) + + (updatedAt.hashCode); + + @override + String toString() => 'SyncPersonV1[birthDate=$birthDate, color=$color, createdAt=$createdAt, faceAssetId=$faceAssetId, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, ownerId=$ownerId, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]'; + + Map toJson() { + final json = {}; + if (this.birthDate != null) { + json[r'birthDate'] = this.birthDate!.toUtc().toIso8601String(); + } else { + // json[r'birthDate'] = null; + } + if (this.color != null) { + json[r'color'] = this.color; + } else { + // json[r'color'] = null; + } + json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + if (this.faceAssetId != null) { + json[r'faceAssetId'] = this.faceAssetId; + } else { + // json[r'faceAssetId'] = null; + } + json[r'id'] = this.id; + json[r'isFavorite'] = this.isFavorite; + json[r'isHidden'] = this.isHidden; + json[r'name'] = this.name; + json[r'ownerId'] = this.ownerId; + json[r'thumbnailPath'] = this.thumbnailPath; + json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + return json; + } + + /// Returns a new [SyncPersonV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncPersonV1? fromJson(dynamic value) { + upgradeDto(value, "SyncPersonV1"); + if (value is Map) { + final json = value.cast(); + + return SyncPersonV1( + birthDate: mapDateTime(json, r'birthDate', r''), + color: mapValueOfType(json, r'color'), + createdAt: mapDateTime(json, r'createdAt', r'')!, + faceAssetId: mapValueOfType(json, r'faceAssetId'), + id: mapValueOfType(json, r'id')!, + isFavorite: mapValueOfType(json, r'isFavorite')!, + isHidden: mapValueOfType(json, r'isHidden')!, + name: mapValueOfType(json, r'name')!, + ownerId: mapValueOfType(json, r'ownerId')!, + thumbnailPath: mapValueOfType(json, r'thumbnailPath')!, + updatedAt: mapDateTime(json, r'updatedAt', r'')!, + ); + } + 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 = SyncPersonV1.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 = SyncPersonV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncPersonV1-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] = SyncPersonV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'birthDate', + 'color', + 'createdAt', + 'faceAssetId', + 'id', + 'isFavorite', + 'isHidden', + 'name', + 'ownerId', + 'thumbnailPath', + 'updatedAt', + }; +} + diff --git a/mobile/openapi/lib/model/sync_request_type.dart b/mobile/openapi/lib/model/sync_request_type.dart index c13d791d84..75ce852f9f 100644 --- a/mobile/openapi/lib/model/sync_request_type.dart +++ b/mobile/openapi/lib/model/sync_request_type.dart @@ -38,6 +38,8 @@ class SyncRequestType { static const partnerStacksV1 = SyncRequestType._(r'PartnerStacksV1'); static const stacksV1 = SyncRequestType._(r'StacksV1'); static const usersV1 = SyncRequestType._(r'UsersV1'); + static const peopleV1 = SyncRequestType._(r'PeopleV1'); + static const userMetadataV1 = SyncRequestType._(r'UserMetadataV1'); /// List of all possible values in this [enum][SyncRequestType]. static const values = [ @@ -56,6 +58,8 @@ class SyncRequestType { partnerStacksV1, stacksV1, usersV1, + peopleV1, + userMetadataV1, ]; static SyncRequestType? fromJson(dynamic value) => SyncRequestTypeTypeTransformer().decode(value); @@ -109,6 +113,8 @@ class SyncRequestTypeTypeTransformer { case r'PartnerStacksV1': return SyncRequestType.partnerStacksV1; case r'StacksV1': return SyncRequestType.stacksV1; case r'UsersV1': return SyncRequestType.usersV1; + case r'PeopleV1': return SyncRequestType.peopleV1; + case r'UserMetadataV1': return SyncRequestType.userMetadataV1; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/sync_stream_dto.dart b/mobile/openapi/lib/model/sync_stream_dto.dart index 28fd3dfaee..9884eef342 100644 --- a/mobile/openapi/lib/model/sync_stream_dto.dart +++ b/mobile/openapi/lib/model/sync_stream_dto.dart @@ -13,25 +13,41 @@ part of openapi.api; class SyncStreamDto { /// Returns a new [SyncStreamDto] instance. SyncStreamDto({ + this.reset, this.types = const [], }); + /// + /// 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. + /// + bool? reset; + List types; @override bool operator ==(Object other) => identical(this, other) || other is SyncStreamDto && + other.reset == reset && _deepEquality.equals(other.types, types); @override int get hashCode => // ignore: unnecessary_parenthesis + (reset == null ? 0 : reset!.hashCode) + (types.hashCode); @override - String toString() => 'SyncStreamDto[types=$types]'; + String toString() => 'SyncStreamDto[reset=$reset, types=$types]'; Map toJson() { final json = {}; + if (this.reset != null) { + json[r'reset'] = this.reset; + } else { + // json[r'reset'] = null; + } json[r'types'] = this.types; return json; } @@ -45,6 +61,7 @@ class SyncStreamDto { final json = value.cast(); return SyncStreamDto( + reset: mapValueOfType(json, r'reset'), types: SyncRequestType.listFromJson(json[r'types']), ); } diff --git a/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart b/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart new file mode 100644 index 0000000000..e9dd733295 --- /dev/null +++ b/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart @@ -0,0 +1,107 @@ +// +// 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 SyncUserMetadataDeleteV1 { + /// Returns a new [SyncUserMetadataDeleteV1] instance. + SyncUserMetadataDeleteV1({ + required this.key, + required this.userId, + }); + + String key; + + String userId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncUserMetadataDeleteV1 && + other.key == key && + other.userId == userId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (key.hashCode) + + (userId.hashCode); + + @override + String toString() => 'SyncUserMetadataDeleteV1[key=$key, userId=$userId]'; + + Map toJson() { + final json = {}; + json[r'key'] = this.key; + json[r'userId'] = this.userId; + return json; + } + + /// Returns a new [SyncUserMetadataDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncUserMetadataDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncUserMetadataDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncUserMetadataDeleteV1( + key: mapValueOfType(json, r'key')!, + userId: mapValueOfType(json, r'userId')!, + ); + } + 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 = SyncUserMetadataDeleteV1.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 = SyncUserMetadataDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncUserMetadataDeleteV1-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] = SyncUserMetadataDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'key', + 'userId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_user_metadata_v1.dart b/mobile/openapi/lib/model/sync_user_metadata_v1.dart new file mode 100644 index 0000000000..0b060dc17c --- /dev/null +++ b/mobile/openapi/lib/model/sync_user_metadata_v1.dart @@ -0,0 +1,115 @@ +// +// 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 SyncUserMetadataV1 { + /// Returns a new [SyncUserMetadataV1] instance. + SyncUserMetadataV1({ + required this.key, + required this.userId, + required this.value, + }); + + String key; + + String userId; + + Object value; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncUserMetadataV1 && + other.key == key && + other.userId == userId && + other.value == value; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (key.hashCode) + + (userId.hashCode) + + (value.hashCode); + + @override + String toString() => 'SyncUserMetadataV1[key=$key, userId=$userId, value=$value]'; + + Map toJson() { + final json = {}; + json[r'key'] = this.key; + json[r'userId'] = this.userId; + json[r'value'] = this.value; + return json; + } + + /// Returns a new [SyncUserMetadataV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncUserMetadataV1? fromJson(dynamic value) { + upgradeDto(value, "SyncUserMetadataV1"); + if (value is Map) { + final json = value.cast(); + + return SyncUserMetadataV1( + key: mapValueOfType(json, r'key')!, + userId: mapValueOfType(json, r'userId')!, + value: mapValueOfType(json, r'value')!, + ); + } + 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 = SyncUserMetadataV1.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 = SyncUserMetadataV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncUserMetadataV1-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] = SyncUserMetadataV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'key', + 'userId', + 'value', + }; +} + diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index 59d5f09fc9..38dbb30f0c 100644 --- a/mobile/openapi/lib/model/system_config_dto.dart +++ b/mobile/openapi/lib/model/system_config_dto.dart @@ -23,6 +23,7 @@ class SystemConfigDto { required this.map, required this.metadata, required this.newVersionCheck, + required this.nightlyTasks, required this.notifications, required this.oauth, required this.passwordLogin, @@ -55,6 +56,8 @@ class SystemConfigDto { SystemConfigNewVersionCheckDto newVersionCheck; + SystemConfigNightlyTasksDto nightlyTasks; + SystemConfigNotificationsDto notifications; SystemConfigOAuthDto oauth; @@ -87,6 +90,7 @@ class SystemConfigDto { other.map == map && other.metadata == metadata && other.newVersionCheck == newVersionCheck && + other.nightlyTasks == nightlyTasks && other.notifications == notifications && other.oauth == oauth && other.passwordLogin == passwordLogin && @@ -111,6 +115,7 @@ class SystemConfigDto { (map.hashCode) + (metadata.hashCode) + (newVersionCheck.hashCode) + + (nightlyTasks.hashCode) + (notifications.hashCode) + (oauth.hashCode) + (passwordLogin.hashCode) + @@ -123,7 +128,7 @@ class SystemConfigDto { (user.hashCode); @override - String toString() => 'SystemConfigDto[backup=$backup, ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, metadata=$metadata, newVersionCheck=$newVersionCheck, notifications=$notifications, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, templates=$templates, theme=$theme, trash=$trash, user=$user]'; + String toString() => 'SystemConfigDto[backup=$backup, ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, metadata=$metadata, newVersionCheck=$newVersionCheck, nightlyTasks=$nightlyTasks, notifications=$notifications, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, templates=$templates, theme=$theme, trash=$trash, user=$user]'; Map toJson() { final json = {}; @@ -137,6 +142,7 @@ class SystemConfigDto { json[r'map'] = this.map; json[r'metadata'] = this.metadata; json[r'newVersionCheck'] = this.newVersionCheck; + json[r'nightlyTasks'] = this.nightlyTasks; json[r'notifications'] = this.notifications; json[r'oauth'] = this.oauth; json[r'passwordLogin'] = this.passwordLogin; @@ -169,6 +175,7 @@ class SystemConfigDto { map: SystemConfigMapDto.fromJson(json[r'map'])!, metadata: SystemConfigMetadataDto.fromJson(json[r'metadata'])!, newVersionCheck: SystemConfigNewVersionCheckDto.fromJson(json[r'newVersionCheck'])!, + nightlyTasks: SystemConfigNightlyTasksDto.fromJson(json[r'nightlyTasks'])!, notifications: SystemConfigNotificationsDto.fromJson(json[r'notifications'])!, oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!, passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!, @@ -236,6 +243,7 @@ class SystemConfigDto { 'map', 'metadata', 'newVersionCheck', + 'nightlyTasks', 'notifications', 'oauth', 'passwordLogin', diff --git a/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart b/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart new file mode 100644 index 0000000000..ab7b4b37c2 --- /dev/null +++ b/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart @@ -0,0 +1,139 @@ +// +// 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 SystemConfigNightlyTasksDto { + /// Returns a new [SystemConfigNightlyTasksDto] instance. + SystemConfigNightlyTasksDto({ + required this.clusterNewFaces, + required this.databaseCleanup, + required this.generateMemories, + required this.missingThumbnails, + required this.startTime, + required this.syncQuotaUsage, + }); + + bool clusterNewFaces; + + bool databaseCleanup; + + bool generateMemories; + + bool missingThumbnails; + + String startTime; + + bool syncQuotaUsage; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigNightlyTasksDto && + other.clusterNewFaces == clusterNewFaces && + other.databaseCleanup == databaseCleanup && + other.generateMemories == generateMemories && + other.missingThumbnails == missingThumbnails && + other.startTime == startTime && + other.syncQuotaUsage == syncQuotaUsage; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (clusterNewFaces.hashCode) + + (databaseCleanup.hashCode) + + (generateMemories.hashCode) + + (missingThumbnails.hashCode) + + (startTime.hashCode) + + (syncQuotaUsage.hashCode); + + @override + String toString() => 'SystemConfigNightlyTasksDto[clusterNewFaces=$clusterNewFaces, databaseCleanup=$databaseCleanup, generateMemories=$generateMemories, missingThumbnails=$missingThumbnails, startTime=$startTime, syncQuotaUsage=$syncQuotaUsage]'; + + Map toJson() { + final json = {}; + json[r'clusterNewFaces'] = this.clusterNewFaces; + json[r'databaseCleanup'] = this.databaseCleanup; + json[r'generateMemories'] = this.generateMemories; + json[r'missingThumbnails'] = this.missingThumbnails; + json[r'startTime'] = this.startTime; + json[r'syncQuotaUsage'] = this.syncQuotaUsage; + return json; + } + + /// Returns a new [SystemConfigNightlyTasksDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigNightlyTasksDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigNightlyTasksDto"); + if (value is Map) { + final json = value.cast(); + + return SystemConfigNightlyTasksDto( + clusterNewFaces: mapValueOfType(json, r'clusterNewFaces')!, + databaseCleanup: mapValueOfType(json, r'databaseCleanup')!, + generateMemories: mapValueOfType(json, r'generateMemories')!, + missingThumbnails: mapValueOfType(json, r'missingThumbnails')!, + startTime: mapValueOfType(json, r'startTime')!, + syncQuotaUsage: mapValueOfType(json, r'syncQuotaUsage')!, + ); + } + 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 = SystemConfigNightlyTasksDto.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 = SystemConfigNightlyTasksDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigNightlyTasksDto-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] = SystemConfigNightlyTasksDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'clusterNewFaces', + 'databaseCleanup', + 'generateMemories', + 'missingThumbnails', + 'startTime', + 'syncQuotaUsage', + }; +} + diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 3be12d497c..95aa866ea3 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: "direct main" description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" auto_route: dependency: "direct main" description: @@ -509,10 +509,10 @@ packages: dependency: "direct dev" description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -1007,10 +1007,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: @@ -1064,10 +1064,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -2029,10 +2029,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" wakelock_plus: dependency: "direct main" description: @@ -2085,10 +2085,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.1.0" win32: dependency: transitive description: @@ -2155,4 +2155,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.7.0 <4.0.0" - flutter: ">=3.29.3" + flutter: ">=3.32.6" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 56aa09e1df..f8059d7e3c 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -6,7 +6,7 @@ version: 1.135.3+204 environment: sdk: '>=3.3.0 <4.0.0' - flutter: 3.29.3 + flutter: 3.32.6 isar_version: &isar_version 3.1.8 @@ -43,7 +43,7 @@ dependencies: home_widget: ^0.8.0 http: ^1.3.0 image_picker: ^1.1.2 - intl: ^0.19.0 + intl: ^0.20.0 local_auth: ^2.3.0 logging: ^1.3.0 maplibre_gl: ^0.22.0 diff --git a/mobile/test/modules/map/map_theme_override_test.dart b/mobile/test/modules/map/map_theme_override_test.dart index db133d5c34..59f9b9e246 100644 --- a/mobile/test/modules/map/map_theme_override_test.dart +++ b/mobile/test/modules/map/map_theme_override_test.dart @@ -1,3 +1,4 @@ +@Skip('Flaky test, needs investigation') @Tags(['widget']) library; @@ -23,8 +24,8 @@ void main() { late Isar db; setUpAll(() async { - TestUtils.init(); db = await TestUtils.initIsar(); + TestUtils.init(); }); setUp(() async { diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 35d59ed5fe..11b516e626 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -6078,6 +6078,56 @@ "tags": [ "Sessions" ] + }, + "put": { + "operationId": "updateSession", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Sessions" + ] } }, "/sessions/{id}/lock": { @@ -12844,6 +12894,9 @@ "id": { "type": "string" }, + "isPendingSyncReset": { + "type": "boolean" + }, "token": { "type": "string" }, @@ -12857,6 +12910,7 @@ "deviceOS", "deviceType", "id", + "isPendingSyncReset", "token", "updatedAt" ], @@ -12882,6 +12936,9 @@ "id": { "type": "string" }, + "isPendingSyncReset": { + "type": "boolean" + }, "updatedAt": { "type": "string" } @@ -12892,6 +12949,7 @@ "deviceOS", "deviceType", "id", + "isPendingSyncReset", "updatedAt" ], "type": "object" @@ -12908,6 +12966,14 @@ }, "type": "object" }, + "SessionUpdateDto": { + "properties": { + "isPendingSyncReset": { + "type": "boolean" + } + }, + "type": "object" + }, "SharedLinkCreateDto": { "properties": { "albumId": { @@ -13806,6 +13872,10 @@ "isFavorite": { "type": "boolean" }, + "livePhotoVideoId": { + "nullable": true, + "type": "string" + }, "localDateTime": { "format": "date-time", "nullable": true, @@ -13817,6 +13887,10 @@ "ownerId": { "type": "string" }, + "stackId": { + "nullable": true, + "type": "string" + }, "thumbhash": { "nullable": true, "type": "string" @@ -13844,9 +13918,11 @@ "fileModifiedAt", "id", "isFavorite", + "livePhotoVideoId", "localDateTime", "originalFileName", "ownerId", + "stackId", "thumbhash", "type", "visibility" @@ -13888,7 +13964,12 @@ "MemoryToAssetDeleteV1", "StackV1", "StackDeleteV1", - "SyncAckV1" + "PersonV1", + "PersonDeleteV1", + "UserMetadataV1", + "UserMetadataDeleteV1", + "SyncAckV1", + "SyncResetV1" ], "type": "string" }, @@ -14037,6 +14118,74 @@ ], "type": "object" }, + "SyncPersonDeleteV1": { + "properties": { + "personId": { + "type": "string" + } + }, + "required": [ + "personId" + ], + "type": "object" + }, + "SyncPersonV1": { + "properties": { + "birthDate": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "color": { + "nullable": true, + "type": "string" + }, + "createdAt": { + "format": "date-time", + "type": "string" + }, + "faceAssetId": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "string" + }, + "isFavorite": { + "type": "boolean" + }, + "isHidden": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "ownerId": { + "type": "string" + }, + "thumbnailPath": { + "type": "string" + }, + "updatedAt": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "birthDate", + "color", + "createdAt", + "faceAssetId", + "id", + "isFavorite", + "isHidden", + "name", + "ownerId", + "thumbnailPath", + "updatedAt" + ], + "type": "object" + }, "SyncRequestType": { "enum": [ "AlbumsV1", @@ -14053,10 +14202,16 @@ "PartnerAssetExifsV1", "PartnerStacksV1", "StacksV1", - "UsersV1" + "UsersV1", + "PeopleV1", + "UserMetadataV1" ], "type": "string" }, + "SyncResetV1": { + "properties": {}, + "type": "object" + }, "SyncStackDeleteV1": { "properties": { "stackId": { @@ -14099,6 +14254,9 @@ }, "SyncStreamDto": { "properties": { + "reset": { + "type": "boolean" + }, "types": { "items": { "$ref": "#/components/schemas/SyncRequestType" @@ -14122,6 +14280,40 @@ ], "type": "object" }, + "SyncUserMetadataDeleteV1": { + "properties": { + "key": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "required": [ + "key", + "userId" + ], + "type": "object" + }, + "SyncUserMetadataV1": { + "properties": { + "key": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "value": { + "type": "object" + } + }, + "required": [ + "key", + "userId", + "value" + ], + "type": "object" + }, "SyncUserV1": { "properties": { "deletedAt": { @@ -14190,6 +14382,9 @@ "newVersionCheck": { "$ref": "#/components/schemas/SystemConfigNewVersionCheckDto" }, + "nightlyTasks": { + "$ref": "#/components/schemas/SystemConfigNightlyTasksDto" + }, "notifications": { "$ref": "#/components/schemas/SystemConfigNotificationsDto" }, @@ -14232,6 +14427,7 @@ "map", "metadata", "newVersionCheck", + "nightlyTasks", "notifications", "oauth", "passwordLogin", @@ -14662,6 +14858,37 @@ ], "type": "object" }, + "SystemConfigNightlyTasksDto": { + "properties": { + "clusterNewFaces": { + "type": "boolean" + }, + "databaseCleanup": { + "type": "boolean" + }, + "generateMemories": { + "type": "boolean" + }, + "missingThumbnails": { + "type": "boolean" + }, + "startTime": { + "type": "string" + }, + "syncQuotaUsage": { + "type": "boolean" + } + }, + "required": [ + "clusterNewFaces", + "databaseCleanup", + "generateMemories", + "missingThumbnails", + "startTime", + "syncQuotaUsage" + ], + "type": "object" + }, "SystemConfigNotificationsDto": { "properties": { "smtp": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index a2205de0c8..0d5daed14a 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1164,6 +1164,7 @@ export type SessionResponseDto = { deviceType: string; expiresAt?: string; id: string; + isPendingSyncReset: boolean; updatedAt: string; }; export type SessionCreateDto = { @@ -1179,9 +1180,13 @@ export type SessionCreateResponseDto = { deviceType: string; expiresAt?: string; id: string; + isPendingSyncReset: boolean; token: string; updatedAt: string; }; +export type SessionUpdateDto = { + isPendingSyncReset?: boolean; +}; export type SharedLinkResponseDto = { album?: AlbumResponseDto; allowDownload: boolean; @@ -1264,6 +1269,7 @@ export type AssetFullSyncDto = { userId?: string; }; export type SyncStreamDto = { + reset?: boolean; types: SyncRequestType[]; }; export type DatabaseBackupConfig = { @@ -1383,6 +1389,14 @@ export type SystemConfigMetadataDto = { export type SystemConfigNewVersionCheckDto = { enabled: boolean; }; +export type SystemConfigNightlyTasksDto = { + clusterNewFaces: boolean; + databaseCleanup: boolean; + generateMemories: boolean; + missingThumbnails: boolean; + startTime: string; + syncQuotaUsage: boolean; +}; export type SystemConfigNotificationsDto = { smtp: SystemConfigSmtpDto; }; @@ -1451,6 +1465,7 @@ export type SystemConfigDto = { map: SystemConfigMapDto; metadata: SystemConfigMetadataDto; newVersionCheck: SystemConfigNewVersionCheckDto; + nightlyTasks: SystemConfigNightlyTasksDto; notifications: SystemConfigNotificationsDto; oauth: SystemConfigOAuthDto; passwordLogin: SystemConfigPasswordLoginDto; @@ -3184,6 +3199,19 @@ export function deleteSession({ id }: { method: "DELETE" })); } +export function updateSession({ id, sessionUpdateDto }: { + id: string; + sessionUpdateDto: SessionUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SessionResponseDto; + }>(`/sessions/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: sessionUpdateDto + }))); +} export function lockSession({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { @@ -4109,7 +4137,12 @@ export enum SyncEntityType { MemoryToAssetDeleteV1 = "MemoryToAssetDeleteV1", StackV1 = "StackV1", StackDeleteV1 = "StackDeleteV1", - SyncAckV1 = "SyncAckV1" + PersonV1 = "PersonV1", + PersonDeleteV1 = "PersonDeleteV1", + UserMetadataV1 = "UserMetadataV1", + UserMetadataDeleteV1 = "UserMetadataDeleteV1", + SyncAckV1 = "SyncAckV1", + SyncResetV1 = "SyncResetV1" } export enum SyncRequestType { AlbumsV1 = "AlbumsV1", @@ -4126,7 +4159,9 @@ export enum SyncRequestType { PartnerAssetExifsV1 = "PartnerAssetExifsV1", PartnerStacksV1 = "PartnerStacksV1", StacksV1 = "StacksV1", - UsersV1 = "UsersV1" + UsersV1 = "UsersV1", + PeopleV1 = "PeopleV1", + UserMetadataV1 = "UserMetadataV1" } export enum TranscodeHWAccel { Nvenc = "nvenc", diff --git a/server/Dockerfile b/server/Dockerfile index 2f2749ada0..1f5438d070 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -50,7 +50,7 @@ RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \ # Flutter SDK # https://flutter.dev/docs/development/tools/sdk/releases?tab=linux ENV FLUTTER_CHANNEL="stable" -ENV FLUTTER_VERSION="3.29.3" +ENV FLUTTER_VERSION="3.32.6" ENV FLUTTER_HOME=/flutter ENV PATH=${PATH}:${FLUTTER_HOME}/bin diff --git a/server/src/bin/migrations.ts b/server/src/bin/migrations.ts index 81f5c7e0ab..3bdfb3bbc6 100644 --- a/server/src/bin/migrations.ts +++ b/server/src/bin/migrations.ts @@ -107,7 +107,7 @@ const compare = async () => { const { database } = configRepository.getEnv(); const db = postgres(asPostgresConnectionConfig(database.config)); - const source = schemaFromCode({ overrides: true }); + const source = schemaFromCode({ overrides: true, namingStrategy: 'default' }); const target = await schemaFromDatabase(db, {}); console.log(source.warnings.join('\n')); diff --git a/server/src/config.ts b/server/src/config.ts index 1fcc2e9782..90ca2c1529 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -121,6 +121,14 @@ export interface SystemConfig { newVersionCheck: { enabled: boolean; }; + nightlyTasks: { + startTime: string; + databaseCleanup: boolean; + missingThumbnails: boolean; + clusterNewFaces: boolean; + generateMemories: boolean; + syncQuotaUsage: boolean; + }; trash: { enabled: boolean; days: number; @@ -298,6 +306,14 @@ export const defaults = Object.freeze({ newVersionCheck: { enabled: true, }, + nightlyTasks: { + startTime: '00:00', + databaseCleanup: true, + generateMemories: true, + syncQuotaUsage: true, + missingThumbnails: true, + clusterNewFaces: true, + }, trash: { enabled: true, days: 30, diff --git a/server/src/controllers/session.controller.ts b/server/src/controllers/session.controller.ts index 3838d5af80..f5eb10b3dd 100644 --- a/server/src/controllers/session.controller.ts +++ b/server/src/controllers/session.controller.ts @@ -1,7 +1,7 @@ -import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; -import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto } from 'src/dtos/session.dto'; +import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto, SessionUpdateDto } from 'src/dtos/session.dto'; import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { SessionService } from 'src/services/session.service'; @@ -31,6 +31,16 @@ export class SessionController { return this.service.deleteAll(auth); } + @Put(':id') + @Authenticated({ permission: Permission.SESSION_UPDATE }) + updateSession( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Body() dto: SessionUpdateDto, + ): Promise { + return this.service.update(auth, id, dto); + } + @Delete(':id') @Authenticated({ permission: Permission.SESSION_DELETE }) @HttpCode(HttpStatus.NO_CONTENT) diff --git a/server/src/controllers/system-config.controller.spec.ts b/server/src/controllers/system-config.controller.spec.ts new file mode 100644 index 0000000000..48b8c1bcf0 --- /dev/null +++ b/server/src/controllers/system-config.controller.spec.ts @@ -0,0 +1,74 @@ +import _ from 'lodash'; +import { defaults } from 'src/config'; +import { SystemConfigController } from 'src/controllers/system-config.controller'; +import { StorageTemplateService } from 'src/services/storage-template.service'; +import { SystemConfigService } from 'src/services/system-config.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(SystemConfigController.name, () => { + let ctx: ControllerContext; + const systemConfigService = mockBaseService(SystemConfigService); + const templateService = mockBaseService(StorageTemplateService); + + beforeAll(async () => { + ctx = await controllerSetup(SystemConfigController, [ + { provide: SystemConfigService, useValue: systemConfigService }, + { provide: StorageTemplateService, useValue: templateService }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + systemConfigService.resetAllMocks(); + templateService.resetAllMocks(); + ctx.reset(); + }); + + describe('GET /system-config', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/system-config'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /system-config/defaults', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/system-config/defaults'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PUT /system-config', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put('/system-config'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + describe('nightlyTasks', () => { + it('should validate nightly jobs start time', async () => { + const config = _.cloneDeep(defaults); + config.nightlyTasks.startTime = 'invalid'; + const { status, body } = await request(ctx.getHttpServer()).put('/system-config').send(config); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['nightlyTasks.startTime must be in HH:mm format'])); + }); + + it('should accept a valid time', async () => { + const config = _.cloneDeep(defaults); + config.nightlyTasks.startTime = '05:05'; + const { status } = await request(ctx.getHttpServer()).put('/system-config').send(config); + expect(status).toBe(200); + }); + + it('should validate a boolean field', async () => { + const config = _.cloneDeep(defaults); + (config.nightlyTasks.databaseCleanup as any) = 'invalid'; + const { status, body } = await request(ctx.getHttpServer()).put('/system-config').send(config); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['nightlyTasks.databaseCleanup must be a boolean value'])); + }); + }); + }); +}); diff --git a/server/src/database.ts b/server/src/database.ts index acd6980985..d42b2618a4 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -13,7 +13,7 @@ import { UserStatus, } from 'src/enum'; import { AlbumTable } from 'src/schema/tables/album.table'; -import { ExifTable } from 'src/schema/tables/exif.table'; +import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { UserMetadataItem } from 'src/types'; export type AuthUser = { @@ -201,6 +201,7 @@ export type Album = Selectable & { export type AuthSession = { id: string; + isPendingSyncReset: boolean; hasElevatedPermission: boolean; }; @@ -238,9 +239,10 @@ export type Session = { deviceOS: string; deviceType: string; pinExpiresAt: Date | null; + isPendingSyncReset: boolean; }; -export type Exif = Omit, 'updatedAt' | 'updateId'>; +export type Exif = Omit, 'updatedAt' | 'updateId'>; export type Person = { createdAt: Date; @@ -274,52 +276,45 @@ export type AssetFace = { const userColumns = ['id', 'name', 'email', 'avatarColor', 'profileImagePath', 'profileChangedAt'] as const; const userWithPrefixColumns = [ - 'users.id', - 'users.name', - 'users.email', - 'users.avatarColor', - 'users.profileImagePath', - 'users.profileChangedAt', + 'user2.id', + 'user2.name', + 'user2.email', + 'user2.avatarColor', + 'user2.profileImagePath', + 'user2.profileChangedAt', ] as const; export const columns = { asset: [ - 'assets.id', - 'assets.checksum', - 'assets.deviceAssetId', - 'assets.deviceId', - 'assets.fileCreatedAt', - 'assets.fileModifiedAt', - 'assets.isExternal', - 'assets.visibility', - 'assets.libraryId', - 'assets.livePhotoVideoId', - 'assets.localDateTime', - 'assets.originalFileName', - 'assets.originalPath', - 'assets.ownerId', - 'assets.sidecarPath', - 'assets.type', + 'asset.id', + 'asset.checksum', + 'asset.deviceAssetId', + 'asset.deviceId', + 'asset.fileCreatedAt', + 'asset.fileModifiedAt', + 'asset.isExternal', + 'asset.visibility', + 'asset.libraryId', + 'asset.livePhotoVideoId', + 'asset.localDateTime', + 'asset.originalFileName', + 'asset.originalPath', + 'asset.ownerId', + 'asset.sidecarPath', + 'asset.type', ], - assetFiles: ['asset_files.id', 'asset_files.path', 'asset_files.type'], - authUser: [ - 'users.id', - 'users.name', - 'users.email', - 'users.isAdmin', - 'users.quotaUsageInBytes', - 'users.quotaSizeInBytes', - ], - authApiKey: ['api_keys.id', 'api_keys.permissions'], - authSession: ['sessions.id', 'sessions.updatedAt', 'sessions.pinExpiresAt'], + assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'], + authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'], + authApiKey: ['api_key.id', 'api_key.permissions'], + authSession: ['session.id', 'session.isPendingSyncReset', 'session.updatedAt', 'session.pinExpiresAt'], authSharedLink: [ - 'shared_links.id', - 'shared_links.userId', - 'shared_links.expiresAt', - 'shared_links.showExif', - 'shared_links.allowUpload', - 'shared_links.allowDownload', - 'shared_links.password', + 'shared_link.id', + 'shared_link.userId', + 'shared_link.expiresAt', + 'shared_link.showExif', + 'shared_link.allowUpload', + 'shared_link.allowDownload', + 'shared_link.password', ], user: userColumns, userWithPrefix: userWithPrefixColumns, @@ -337,89 +332,85 @@ export const columns = { 'quotaSizeInBytes', 'quotaUsageInBytes', ], - tag: ['tags.id', 'tags.value', 'tags.createdAt', 'tags.updatedAt', 'tags.color', 'tags.parentId'], + tag: ['tag.id', 'tag.value', 'tag.createdAt', 'tag.updatedAt', 'tag.color', 'tag.parentId'], apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'], notification: ['id', 'createdAt', 'level', 'type', 'title', 'description', 'data', 'readAt'], syncAsset: [ - 'assets.id', - 'assets.ownerId', - 'assets.originalFileName', - 'assets.thumbhash', - 'assets.checksum', - 'assets.fileCreatedAt', - 'assets.fileModifiedAt', - 'assets.localDateTime', - 'assets.type', - 'assets.deletedAt', - 'assets.isFavorite', - 'assets.visibility', - 'assets.duration', - ], - syncAlbumUser: ['album_users.albumsId as albumId', 'album_users.usersId as userId', 'album_users.role'], - syncStack: [ - 'asset_stack.id', - 'asset_stack.createdAt', - 'asset_stack.updatedAt', - 'asset_stack.primaryAssetId', - 'asset_stack.ownerId', + 'asset.id', + 'asset.ownerId', + 'asset.originalFileName', + 'asset.thumbhash', + 'asset.checksum', + 'asset.fileCreatedAt', + 'asset.fileModifiedAt', + 'asset.localDateTime', + 'asset.type', + 'asset.deletedAt', + 'asset.isFavorite', + 'asset.visibility', + 'asset.duration', + 'asset.livePhotoVideoId', + 'asset.stackId', ], + syncAlbumUser: ['album_user.albumsId as albumId', 'album_user.usersId as userId', 'album_user.role'], + syncStack: ['stack.id', 'stack.createdAt', 'stack.updatedAt', 'stack.primaryAssetId', 'stack.ownerId'], stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'], syncAssetExif: [ - 'exif.assetId', - 'exif.description', - 'exif.exifImageWidth', - 'exif.exifImageHeight', - 'exif.fileSizeInByte', - 'exif.orientation', - 'exif.dateTimeOriginal', - 'exif.modifyDate', - 'exif.timeZone', - 'exif.latitude', - 'exif.longitude', - 'exif.projectionType', - 'exif.city', - 'exif.state', - 'exif.country', - 'exif.make', - 'exif.model', - 'exif.lensModel', - 'exif.fNumber', - 'exif.focalLength', - 'exif.iso', - 'exif.exposureTime', - 'exif.profileDescription', - 'exif.rating', - 'exif.fps', + 'asset_exif.assetId', + 'asset_exif.description', + 'asset_exif.exifImageWidth', + 'asset_exif.exifImageHeight', + 'asset_exif.fileSizeInByte', + 'asset_exif.orientation', + 'asset_exif.dateTimeOriginal', + 'asset_exif.modifyDate', + 'asset_exif.timeZone', + 'asset_exif.latitude', + 'asset_exif.longitude', + 'asset_exif.projectionType', + 'asset_exif.city', + 'asset_exif.state', + 'asset_exif.country', + 'asset_exif.make', + 'asset_exif.model', + 'asset_exif.lensModel', + 'asset_exif.fNumber', + 'asset_exif.focalLength', + 'asset_exif.iso', + 'asset_exif.exposureTime', + 'asset_exif.profileDescription', + 'asset_exif.rating', + 'asset_exif.fps', ], exif: [ - 'exif.assetId', - 'exif.autoStackId', - 'exif.bitsPerSample', - 'exif.city', - 'exif.colorspace', - 'exif.country', - 'exif.dateTimeOriginal', - 'exif.description', - 'exif.exifImageHeight', - 'exif.exifImageWidth', - 'exif.exposureTime', - 'exif.fileSizeInByte', - 'exif.fNumber', - 'exif.focalLength', - 'exif.fps', - 'exif.iso', - 'exif.latitude', - 'exif.lensModel', - 'exif.livePhotoCID', - 'exif.longitude', - 'exif.make', - 'exif.model', - 'exif.modifyDate', - 'exif.orientation', - 'exif.profileDescription', - 'exif.projectionType', - 'exif.rating', - 'exif.state', - 'exif.timeZone', + 'asset_exif.assetId', + 'asset_exif.autoStackId', + 'asset_exif.bitsPerSample', + 'asset_exif.city', + 'asset_exif.colorspace', + 'asset_exif.country', + 'asset_exif.dateTimeOriginal', + 'asset_exif.description', + 'asset_exif.exifImageHeight', + 'asset_exif.exifImageWidth', + 'asset_exif.exposureTime', + 'asset_exif.fileSizeInByte', + 'asset_exif.fNumber', + 'asset_exif.focalLength', + 'asset_exif.fps', + 'asset_exif.iso', + 'asset_exif.latitude', + 'asset_exif.lensModel', + 'asset_exif.livePhotoCID', + 'asset_exif.longitude', + 'asset_exif.make', + 'asset_exif.model', + 'asset_exif.modifyDate', + 'asset_exif.orientation', + 'asset_exif.profileDescription', + 'asset_exif.projectionType', + 'asset_exif.rating', + 'asset_exif.state', + 'asset_exif.timeZone', ], } as const; diff --git a/server/src/dtos/session.dto.ts b/server/src/dtos/session.dto.ts index f15166fbf5..0babbb9182 100644 --- a/server/src/dtos/session.dto.ts +++ b/server/src/dtos/session.dto.ts @@ -1,6 +1,6 @@ import { IsInt, IsPositive, IsString } from 'class-validator'; import { Session } from 'src/database'; -import { Optional } from 'src/validation'; +import { Optional, ValidateBoolean } from 'src/validation'; export class SessionCreateDto { /** @@ -20,6 +20,11 @@ export class SessionCreateDto { deviceOS?: string; } +export class SessionUpdateDto { + @ValidateBoolean({ optional: true }) + isPendingSyncReset?: boolean; +} + export class SessionResponseDto { id!: string; createdAt!: string; @@ -28,6 +33,7 @@ export class SessionResponseDto { current!: boolean; deviceType!: string; deviceOS!: string; + isPendingSyncReset!: boolean; } export class SessionCreateResponseDto extends SessionResponseDto { @@ -42,4 +48,5 @@ export const mapSession = (entity: Session, currentId?: string): SessionResponse current: currentId === entity.id, deviceOS: entity.deviceOS, deviceType: entity.deviceType, + isPendingSyncReset: entity.isPendingSyncReset, }); diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index db1fd29418..8ba73271e6 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -10,8 +10,10 @@ import { MemoryType, SyncEntityType, SyncRequestType, + UserMetadataKey, } from 'src/enum'; -import { Optional, ValidateDate, ValidateUUID } from 'src/validation'; +import { UserMetadata } from 'src/types'; +import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; export class AssetFullSyncDto { @ValidateUUID({ optional: true }) @@ -94,6 +96,8 @@ export class SyncAssetV1 { isFavorite!: boolean; @ApiProperty({ enumName: 'AssetVisibility', enum: AssetVisibility }) visibility!: AssetVisibility; + livePhotoVideoId!: string | null; + stackId!: string | null; } @ExtraModel() @@ -233,9 +237,45 @@ export class SyncStackDeleteV1 { stackId!: string; } +@ExtraModel() +export class SyncPersonV1 { + id!: string; + createdAt!: Date; + updatedAt!: Date; + ownerId!: string; + name!: string; + birthDate!: Date | null; + thumbnailPath!: string; + isHidden!: boolean; + isFavorite!: boolean; + color!: string | null; + faceAssetId!: string | null; +} + +@ExtraModel() +export class SyncPersonDeleteV1 { + personId!: string; +} + +@ExtraModel() +export class SyncUserMetadataV1 { + userId!: string; + key!: string; + value!: UserMetadata[UserMetadataKey]; +} + +@ExtraModel() +export class SyncUserMetadataDeleteV1 { + userId!: string; + key!: string; +} + @ExtraModel() export class SyncAckV1 {} +@ExtraModel() +export class SyncResetV1 {} + export type SyncItem = { [SyncEntityType.UserV1]: SyncUserV1; [SyncEntityType.UserDeleteV1]: SyncUserDeleteV1; @@ -270,13 +310,21 @@ export type SyncItem = { [SyncEntityType.PartnerStackBackfillV1]: SyncStackV1; [SyncEntityType.PartnerStackDeleteV1]: SyncStackDeleteV1; [SyncEntityType.PartnerStackV1]: SyncStackV1; + [SyncEntityType.PersonV1]: SyncPersonV1; + [SyncEntityType.PersonDeleteV1]: SyncPersonDeleteV1; + [SyncEntityType.UserMetadataV1]: SyncUserMetadataV1; + [SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1; [SyncEntityType.SyncAckV1]: SyncAckV1; + [SyncEntityType.SyncResetV1]: SyncResetV1; }; export class SyncStreamDto { @IsEnum(SyncRequestType, { each: true }) @ApiProperty({ enumName: 'SyncRequestType', enum: SyncRequestType, isArray: true }) types!: SyncRequestType[]; + + @ValidateBoolean({ optional: true }) + reset?: boolean; } export class SyncAckDto { diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index b0385984b4..49c5e5b4e7 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -34,7 +34,7 @@ import { VideoContainer, } from 'src/enum'; import { ConcurrentQueueName } from 'src/types'; -import { IsCronExpression, Optional, ValidateBoolean } from 'src/validation'; +import { IsCronExpression, IsDateStringFormat, Optional, ValidateBoolean } from 'src/validation'; const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled; const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled; @@ -329,6 +329,26 @@ class SystemConfigNewVersionCheckDto { enabled!: boolean; } +class SystemConfigNightlyTasksDto { + @IsDateStringFormat('HH:mm', { message: 'startTime must be in HH:mm format' }) + startTime!: string; + + @ValidateBoolean() + databaseCleanup!: boolean; + + @ValidateBoolean() + missingThumbnails!: boolean; + + @ValidateBoolean() + clusterNewFaces!: boolean; + + @ValidateBoolean() + generateMemories!: boolean; + + @ValidateBoolean() + syncQuotaUsage!: boolean; +} + class SystemConfigOAuthDto { @ValidateBoolean() autoLaunch!: boolean; @@ -638,6 +658,11 @@ export class SystemConfigDto implements SystemConfig { @IsObject() newVersionCheck!: SystemConfigNewVersionCheckDto; + @Type(() => SystemConfigNightlyTasksDto) + @ValidateNested() + @IsObject() + nightlyTasks!: SystemConfigNightlyTasksDto; + @Type(() => SystemConfigOAuthDto) @ValidateNested() @IsObject() diff --git a/server/src/enum.ts b/server/src/enum.ts index d211420ab5..d7c74a71c6 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -567,6 +567,7 @@ export enum DatabaseLock { VersionHistory = 500, CLIPDimSize = 512, Library = 1337, + NightlyJobs = 600, GetSystemConfig = 69, BackupDatabase = 42, MemoryCreation = 777, @@ -588,6 +589,8 @@ export enum SyncRequestType { PartnerStacksV1 = 'PartnerStacksV1', StacksV1 = 'StacksV1', UsersV1 = 'UsersV1', + PeopleV1 = 'PeopleV1', + UserMetadataV1 = 'UserMetadataV1', } export enum SyncEntityType { @@ -635,7 +638,14 @@ export enum SyncEntityType { StackV1 = 'StackV1', StackDeleteV1 = 'StackDeleteV1', + PersonV1 = 'PersonV1', + PersonDeleteV1 = 'PersonDeleteV1', + + UserMetadataV1 = 'UserMetadataV1', + UserMetadataDeleteV1 = 'UserMetadataDeleteV1', + SyncAckV1 = 'SyncAckV1', + SyncResetV1 = 'SyncResetV1', } export enum NotificationLevel { @@ -675,3 +685,8 @@ export enum AssetVisibility { HIDDEN = 'hidden', LOCKED = 'locked', } + +export enum CronJob { + LibraryScan = 'LibraryScan', + NightlyJobs = 'NightlyJobs', +} diff --git a/server/src/queries/access.repository.sql b/server/src/queries/access.repository.sql index 402bbdcfaf..9aecaafb52 100644 --- a/server/src/queries/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -14,161 +14,161 @@ select "activity"."id" from "activity" - left join "albums" on "activity"."albumId" = "albums"."id" - and "albums"."deletedAt" is null + left join "album" on "activity"."albumId" = "album"."id" + and "album"."deletedAt" is null where "activity"."id" in ($1) - and "albums"."ownerId" = $2::uuid + and "album"."ownerId" = $2::uuid -- AccessRepository.activity.checkCreateAccess select - "albums"."id" + "album"."id" from - "albums" - left join "albums_shared_users_users" as "albumUsers" on "albumUsers"."albumsId" = "albums"."id" - left join "users" on "users"."id" = "albumUsers"."usersId" - and "users"."deletedAt" is null + "album" + left join "album_user" as "albumUsers" on "albumUsers"."albumsId" = "album"."id" + left join "user" on "user"."id" = "albumUsers"."usersId" + and "user"."deletedAt" is null where - "albums"."id" in ($1) - and "albums"."isActivityEnabled" = $2 + "album"."id" in ($1) + and "album"."isActivityEnabled" = $2 and ( - "albums"."ownerId" = $3 - or "users"."id" = $4 + "album"."ownerId" = $3 + or "user"."id" = $4 ) - and "albums"."deletedAt" is null + and "album"."deletedAt" is null -- AccessRepository.album.checkOwnerAccess select - "albums"."id" + "album"."id" from - "albums" + "album" where - "albums"."id" in ($1) - and "albums"."ownerId" = $2 - and "albums"."deletedAt" is null + "album"."id" in ($1) + and "album"."ownerId" = $2 + and "album"."deletedAt" is null -- AccessRepository.album.checkSharedAlbumAccess select - "albums"."id" + "album"."id" from - "albums" - left join "albums_shared_users_users" as "albumUsers" on "albumUsers"."albumsId" = "albums"."id" - left join "users" on "users"."id" = "albumUsers"."usersId" - and "users"."deletedAt" is null + "album" + left join "album_user" on "album_user"."albumsId" = "album"."id" + left join "user" on "user"."id" = "album_user"."usersId" + and "user"."deletedAt" is null where - "albums"."id" in ($1) - and "albums"."deletedAt" is null - and "users"."id" = $2 - and "albumUsers"."role" in ($3, $4) + "album"."id" in ($1) + and "album"."deletedAt" is null + and "user"."id" = $2 + and "album_user"."role" in ($3, $4) -- AccessRepository.album.checkSharedLinkAccess select - "shared_links"."albumId" + "shared_link"."albumId" from - "shared_links" + "shared_link" where - "shared_links"."id" = $1 - and "shared_links"."albumId" in ($2) + "shared_link"."id" = $1 + and "shared_link"."albumId" in ($2) -- AccessRepository.asset.checkAlbumAccess select - "assets"."id", - "assets"."livePhotoVideoId" + "asset"."id", + "asset"."livePhotoVideoId" from - "albums" - inner join "albums_assets_assets" as "albumAssets" on "albums"."id" = "albumAssets"."albumsId" - inner join "assets" on "assets"."id" = "albumAssets"."assetsId" - and "assets"."deletedAt" is null - left join "albums_shared_users_users" as "albumUsers" on "albumUsers"."albumsId" = "albums"."id" - left join "users" on "users"."id" = "albumUsers"."usersId" - and "users"."deletedAt" is null + "album" + inner join "album_asset" as "albumAssets" on "album"."id" = "albumAssets"."albumsId" + inner join "asset" on "asset"."id" = "albumAssets"."assetsId" + and "asset"."deletedAt" is null + left join "album_user" as "albumUsers" on "albumUsers"."albumsId" = "album"."id" + left join "user" on "user"."id" = "albumUsers"."usersId" + and "user"."deletedAt" is null where - array["assets"."id", "assets"."livePhotoVideoId"] && array[$1]::uuid[] + array["asset"."id", "asset"."livePhotoVideoId"] && array[$1]::uuid[] and ( - "albums"."ownerId" = $2 - or "users"."id" = $3 + "album"."ownerId" = $2 + or "user"."id" = $3 ) - and "albums"."deletedAt" is null + and "album"."deletedAt" is null -- AccessRepository.asset.checkOwnerAccess select - "assets"."id" + "asset"."id" from - "assets" + "asset" where - "assets"."id" in ($1) - and "assets"."ownerId" = $2 - and "assets"."visibility" != $3 + "asset"."id" in ($1) + and "asset"."ownerId" = $2 + and "asset"."visibility" != $3 -- AccessRepository.asset.checkPartnerAccess select - "assets"."id" + "asset"."id" from - "partners" as "partner" - inner join "users" as "sharedBy" on "sharedBy"."id" = "partner"."sharedById" + "partner" + inner join "user" as "sharedBy" on "sharedBy"."id" = "partner"."sharedById" and "sharedBy"."deletedAt" is null - inner join "assets" on "assets"."ownerId" = "sharedBy"."id" - and "assets"."deletedAt" is null + inner join "asset" on "asset"."ownerId" = "sharedBy"."id" + and "asset"."deletedAt" is null where "partner"."sharedWithId" = $1 and ( - "assets"."visibility" = 'timeline' - or "assets"."visibility" = 'hidden' + "asset"."visibility" = 'timeline' + or "asset"."visibility" = 'hidden' ) - and "assets"."id" in ($2) + and "asset"."id" in ($2) -- AccessRepository.asset.checkSharedLinkAccess select - "assets"."id" as "assetId", - "assets"."livePhotoVideoId" as "assetLivePhotoVideoId", + "asset"."id" as "assetId", + "asset"."livePhotoVideoId" as "assetLivePhotoVideoId", "albumAssets"."id" as "albumAssetId", "albumAssets"."livePhotoVideoId" as "albumAssetLivePhotoVideoId" from - "shared_links" - left join "albums" on "albums"."id" = "shared_links"."albumId" - and "albums"."deletedAt" is null - left join "shared_link__asset" on "shared_link__asset"."sharedLinksId" = "shared_links"."id" - left join "assets" on "assets"."id" = "shared_link__asset"."assetsId" - and "assets"."deletedAt" is null - left join "albums_assets_assets" on "albums_assets_assets"."albumsId" = "albums"."id" - left join "assets" as "albumAssets" on "albumAssets"."id" = "albums_assets_assets"."assetsId" + "shared_link" + left join "album" on "album"."id" = "shared_link"."albumId" + and "album"."deletedAt" is null + left join "shared_link_asset" on "shared_link_asset"."sharedLinksId" = "shared_link"."id" + left join "asset" on "asset"."id" = "shared_link_asset"."assetsId" + and "asset"."deletedAt" is null + left join "album_asset" on "album_asset"."albumsId" = "album"."id" + left join "asset" as "albumAssets" on "albumAssets"."id" = "album_asset"."assetsId" and "albumAssets"."deletedAt" is null where - "shared_links"."id" = $1 + "shared_link"."id" = $1 and array[ - "assets"."id", - "assets"."livePhotoVideoId", + "asset"."id", + "asset"."livePhotoVideoId", "albumAssets"."id", "albumAssets"."livePhotoVideoId" ] && array[$2]::uuid[] -- AccessRepository.authDevice.checkOwnerAccess select - "sessions"."id" + "session"."id" from - "sessions" + "session" where - "sessions"."userId" = $1 - and "sessions"."id" in ($2) + "session"."userId" = $1 + and "session"."id" in ($2) -- AccessRepository.memory.checkOwnerAccess select - "memories"."id" + "memory"."id" from - "memories" + "memory" where - "memories"."id" in ($1) - and "memories"."ownerId" = $2 - and "memories"."deletedAt" is null + "memory"."id" in ($1) + and "memory"."ownerId" = $2 + and "memory"."deletedAt" is null -- AccessRepository.notification.checkOwnerAccess select - "notifications"."id" + "notification"."id" from - "notifications" + "notification" where - "notifications"."id" in ($1) - and "notifications"."userId" = $2 + "notification"."id" in ($1) + and "notification"."userId" = $2 -- AccessRepository.person.checkOwnerAccess select @@ -181,56 +181,56 @@ where -- AccessRepository.person.checkFaceOwnerAccess select - "asset_faces"."id" + "asset_face"."id" from - "asset_faces" - left join "assets" on "assets"."id" = "asset_faces"."assetId" - and "assets"."deletedAt" is null + "asset_face" + left join "asset" on "asset"."id" = "asset_face"."assetId" + and "asset"."deletedAt" is null where - "asset_faces"."id" in ($1) - and "assets"."ownerId" = $2 + "asset_face"."id" in ($1) + and "asset"."ownerId" = $2 -- AccessRepository.partner.checkUpdateAccess select - "partners"."sharedById" + "partner"."sharedById" from - "partners" + "partner" where - "partners"."sharedById" in ($1) - and "partners"."sharedWithId" = $2 + "partner"."sharedById" in ($1) + and "partner"."sharedWithId" = $2 -- AccessRepository.session.checkOwnerAccess select - "sessions"."id" + "session"."id" from - "sessions" + "session" where - "sessions"."id" in ($1) - and "sessions"."userId" = $2 + "session"."id" in ($1) + and "session"."userId" = $2 -- AccessRepository.stack.checkOwnerAccess select - "stacks"."id" + "stack"."id" from - "asset_stack" as "stacks" + "stack" where - "stacks"."id" in ($1) - and "stacks"."ownerId" = $2 + "stack"."id" in ($1) + and "stack"."ownerId" = $2 -- AccessRepository.tag.checkOwnerAccess select - "tags"."id" + "tag"."id" from - "tags" + "tag" where - "tags"."id" in ($1) - and "tags"."userId" = $2 + "tag"."id" in ($1) + and "tag"."userId" = $2 -- AccessRepository.timeline.checkPartnerAccess select - "partners"."sharedById" + "partner"."sharedById" from - "partners" + "partner" where - "partners"."sharedById" in ($1) - and "partners"."sharedWithId" = $2 + "partner"."sharedById" in ($1) + and "partner"."sharedWithId" = $2 diff --git a/server/src/queries/activity.repository.sql b/server/src/queries/activity.repository.sql index 01c4f901cb..228e5cb0ba 100644 --- a/server/src/queries/activity.repository.sql +++ b/server/src/queries/activity.repository.sql @@ -6,26 +6,26 @@ select to_json("user") as "user" from "activity" - inner join "users" on "users"."id" = "activity"."userId" - and "users"."deletedAt" is null + inner join "user" as "user2" on "user2"."id" = "activity"."userId" + and "user2"."deletedAt" is null inner join lateral ( select - "users"."id", - "users"."name", - "users"."email", - "users"."avatarColor", - "users"."profileImagePath", - "users"."profileChangedAt" + "user2"."id", + "user2"."name", + "user2"."email", + "user2"."avatarColor", + "user2"."profileImagePath", + "user2"."profileChangedAt" from ( select 1 ) as "dummy" ) as "user" on true - left join "assets" on "assets"."id" = "activity"."assetId" + left join "asset" on "asset"."id" = "activity"."assetId" where "activity"."albumId" = $1 - and "assets"."deletedAt" is null + and "asset"."deletedAt" is null order by "activity"."createdAt" asc @@ -49,9 +49,9 @@ returning "profileImagePath", "profileChangedAt" from - "users" + "user" where - "users"."id" = "activity"."userId" + "user"."id" = "activity"."userId" ) as obj ) as "user" @@ -72,16 +72,16 @@ select ) as "likes" from "activity" - inner join "users" on "users"."id" = "activity"."userId" - and "users"."deletedAt" is null - left join "assets" on "assets"."id" = "activity"."assetId" + inner join "user" on "user"."id" = "activity"."userId" + and "user"."deletedAt" is null + left join "asset" on "asset"."id" = "activity"."assetId" where "activity"."assetId" = $3 and "activity"."albumId" = $4 and ( ( - "assets"."deletedAt" is null - and "assets"."visibility" != 'locked' + "asset"."deletedAt" is null + and "asset"."visibility" != 'locked' ) - or "assets"."id" is null + or "asset"."id" is null ) diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index 26ddccbe17..36c44414db 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -2,7 +2,7 @@ -- AlbumRepository.getById select - "albums".*, + "album".*, ( select to_json(obj) @@ -16,9 +16,9 @@ select "profileImagePath", "profileChangedAt" from - "users" + "user" where - "users"."id" = "albums"."ownerId" + "user"."id" = "album"."ownerId" ) as obj ) as "owner", ( @@ -27,7 +27,7 @@ select from ( select - "album_users"."role", + "album_user"."role", ( select to_json(obj) @@ -41,15 +41,15 @@ select "profileImagePath", "profileChangedAt" from - "users" + "user" where - "users"."id" = "album_users"."usersId" + "user"."id" = "album_user"."usersId" ) as obj ) as "user" from - "albums_shared_users_users" as "album_users" + "album_user" where - "album_users"."albumsId" = "albums"."id" + "album_user"."albumsId" = "album"."id" ) as agg ) as "albumUsers", ( @@ -60,9 +60,9 @@ select select * from - "shared_links" + "shared_link" where - "shared_links"."albumId" = "albums"."id" + "shared_link"."albumId" = "album"."id" ) as agg ) as "sharedLinks", ( @@ -71,29 +71,29 @@ select from ( select - "assets".*, - "exif" as "exifInfo" + "asset".*, + "asset_exif" as "exifInfo" from - "assets" - left join "exif" on "assets"."id" = "exif"."assetId" - inner join "albums_assets_assets" on "albums_assets_assets"."assetsId" = "assets"."id" + "asset" + left join "asset_exif" on "asset"."id" = "asset_exif"."assetId" + inner join "album_asset" on "album_asset"."assetsId" = "asset"."id" where - "albums_assets_assets"."albumsId" = "albums"."id" - and "assets"."deletedAt" is null - and "assets"."visibility" in ('archive', 'timeline') + "album_asset"."albumsId" = "album"."id" + and "asset"."deletedAt" is null + and "asset"."visibility" in ('archive', 'timeline') order by - "assets"."fileCreatedAt" desc + "asset"."fileCreatedAt" desc ) as "asset" ) as "assets" from - "albums" + "album" where - "albums"."id" = $1 - and "albums"."deletedAt" is null + "album"."id" = $1 + and "album"."deletedAt" is null -- AlbumRepository.getByAssetId select - "albums".*, + "album".*, ( select to_json(obj) @@ -107,9 +107,9 @@ select "profileImagePath", "profileChangedAt" from - "users" + "user" where - "users"."id" = "albums"."ownerId" + "user"."id" = "album"."ownerId" ) as obj ) as "owner", ( @@ -118,7 +118,7 @@ select from ( select - "album_users"."role", + "album_user"."role", ( select to_json(obj) @@ -132,62 +132,62 @@ select "profileImagePath", "profileChangedAt" from - "users" + "user" where - "users"."id" = "album_users"."usersId" + "user"."id" = "album_user"."usersId" ) as obj ) as "user" from - "albums_shared_users_users" as "album_users" + "album_user" where - "album_users"."albumsId" = "albums"."id" + "album_user"."albumsId" = "album"."id" ) as agg ) as "albumUsers" from - "albums" - inner join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id" + "album" + inner join "album_asset" on "album_asset"."albumsId" = "album"."id" where ( - "albums"."ownerId" = $1 + "album"."ownerId" = $1 or exists ( select from - "albums_shared_users_users" as "album_users" + "album_user" where - "album_users"."albumsId" = "albums"."id" - and "album_users"."usersId" = $2 + "album_user"."albumsId" = "album"."id" + and "album_user"."usersId" = $2 ) ) - and "album_assets"."assetsId" = $3 - and "albums"."deletedAt" is null + and "album_asset"."assetsId" = $3 + and "album"."deletedAt" is null order by - "albums"."createdAt" desc, - "albums"."createdAt" desc + "album"."createdAt" desc, + "album"."createdAt" desc -- AlbumRepository.getMetadataForIds select - "album_assets"."albumsId" as "albumId", + "album_asset"."albumsId" as "albumId", min( - ("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date + ("asset"."localDateTime" AT TIME ZONE 'UTC'::text)::date ) as "startDate", max( - ("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date + ("asset"."localDateTime" AT TIME ZONE 'UTC'::text)::date ) as "endDate", - max("assets"."updatedAt") as "lastModifiedAssetTimestamp", - count("assets"."id")::int as "assetCount" + max("asset"."updatedAt") as "lastModifiedAssetTimestamp", + count("asset"."id")::int as "assetCount" from - "assets" - inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "assets"."id" + "asset" + inner join "album_asset" on "album_asset"."assetsId" = "asset"."id" where - "assets"."visibility" in ('archive', 'timeline') - and "album_assets"."albumsId" in ($1) - and "assets"."deletedAt" is null + "asset"."visibility" in ('archive', 'timeline') + and "album_asset"."albumsId" in ($1) + and "asset"."deletedAt" is null group by - "album_assets"."albumsId" + "album_asset"."albumsId" -- AlbumRepository.getOwned select - "albums".*, + "album".*, ( select to_json(obj) @@ -201,9 +201,9 @@ select "profileImagePath", "profileChangedAt" from - "users" + "user" where - "users"."id" = "albums"."ownerId" + "user"."id" = "album"."ownerId" ) as obj ) as "owner", ( @@ -212,7 +212,7 @@ select from ( select - "album_users"."role", + "album_user"."role", ( select to_json(obj) @@ -226,15 +226,15 @@ select "profileImagePath", "profileChangedAt" from - "users" + "user" where - "users"."id" = "album_users"."usersId" + "user"."id" = "album_user"."usersId" ) as obj ) as "user" from - "albums_shared_users_users" as "album_users" + "album_user" where - "album_users"."albumsId" = "albums"."id" + "album_user"."albumsId" = "album"."id" ) as agg ) as "albumUsers", ( @@ -245,29 +245,29 @@ select select * from - "shared_links" + "shared_link" where - "shared_links"."albumId" = "albums"."id" + "shared_link"."albumId" = "album"."id" ) as agg ) as "sharedLinks" from - "albums" + "album" where - "albums"."ownerId" = $1 - and "albums"."deletedAt" is null + "album"."ownerId" = $1 + and "album"."deletedAt" is null order by - "albums"."createdAt" desc + "album"."createdAt" desc -- AlbumRepository.getShared select - "albums".*, + "album".*, ( select coalesce(json_agg(agg), '[]') from ( select - "album_users"."role", + "album_user"."role", ( select to_json(obj) @@ -281,15 +281,15 @@ select "profileImagePath", "profileChangedAt" from - "users" + "user" where - "users"."id" = "album_users"."usersId" + "user"."id" = "album_user"."usersId" ) as obj ) as "user" from - "albums_shared_users_users" as "album_users" + "album_user" where - "album_users"."albumsId" = "albums"."id" + "album_user"."albumsId" = "album"."id" ) as agg ) as "albumUsers", ( @@ -305,9 +305,9 @@ select "profileImagePath", "profileChangedAt" from - "users" + "user" where - "users"."id" = "albums"."ownerId" + "user"."id" = "album"."ownerId" ) as obj ) as "owner", ( @@ -318,42 +318,42 @@ select select * from - "shared_links" + "shared_link" where - "shared_links"."albumId" = "albums"."id" + "shared_link"."albumId" = "album"."id" ) as agg ) as "sharedLinks" from - "albums" + "album" where ( exists ( select from - "albums_shared_users_users" as "album_users" + "album_user" where - "album_users"."albumsId" = "albums"."id" + "album_user"."albumsId" = "album"."id" and ( - "albums"."ownerId" = $1 - or "album_users"."usersId" = $2 + "album"."ownerId" = $1 + or "album_user"."usersId" = $2 ) ) or exists ( select from - "shared_links" + "shared_link" where - "shared_links"."albumId" = "albums"."id" - and "shared_links"."userId" = $3 + "shared_link"."albumId" = "album"."id" + and "shared_link"."userId" = $3 ) ) - and "albums"."deletedAt" is null + and "album"."deletedAt" is null order by - "albums"."createdAt" desc + "album"."createdAt" desc -- AlbumRepository.getNotShared select - "albums".*, + "album".*, ( select to_json(obj) @@ -367,43 +367,43 @@ select "profileImagePath", "profileChangedAt" from - "users" + "user" where - "users"."id" = "albums"."ownerId" + "user"."id" = "album"."ownerId" ) as obj ) as "owner" from - "albums" + "album" where - "albums"."ownerId" = $1 - and "albums"."deletedAt" is null + "album"."ownerId" = $1 + and "album"."deletedAt" is null and not exists ( select from - "albums_shared_users_users" as "album_users" + "album_user" where - "album_users"."albumsId" = "albums"."id" + "album_user"."albumsId" = "album"."id" ) and not exists ( select from - "shared_links" + "shared_link" where - "shared_links"."albumId" = "albums"."id" + "shared_link"."albumId" = "album"."id" ) order by - "albums"."createdAt" desc + "album"."createdAt" desc -- AlbumRepository.removeAssetsFromAll -delete from "albums_assets_assets" +delete from "album_asset" where - "albums_assets_assets"."assetsId" in ($1) + "album_asset"."assetsId" in ($1) -- AlbumRepository.getAssetIds select * from - "albums_assets_assets" + "album_asset" where - "albums_assets_assets"."albumsId" = $1 - and "albums_assets_assets"."assetsId" in ($2) + "album_asset"."albumsId" = $1 + and "album_asset"."assetsId" in ($2) diff --git a/server/src/queries/album.user.repository.sql b/server/src/queries/album.user.repository.sql index 08f337c150..e0fc0e7b74 100644 --- a/server/src/queries/album.user.repository.sql +++ b/server/src/queries/album.user.repository.sql @@ -2,7 +2,7 @@ -- AlbumUserRepository.create insert into - "albums_shared_users_users" ("usersId", "albumsId") + "album_user" ("usersId", "albumsId") values ($1, $2) returning @@ -11,7 +11,7 @@ returning "role" -- AlbumUserRepository.update -update "albums_shared_users_users" +update "album_user" set "role" = $1 where @@ -21,7 +21,7 @@ returning * -- AlbumUserRepository.delete -delete from "albums_shared_users_users" +delete from "album_user" where "usersId" = $1 and "albumsId" = $2 diff --git a/server/src/queries/api.key.repository.sql b/server/src/queries/api.key.repository.sql index 35fd5d2821..43f3155ab8 100644 --- a/server/src/queries/api.key.repository.sql +++ b/server/src/queries/api.key.repository.sql @@ -2,31 +2,31 @@ -- ApiKeyRepository.getKey select - "api_keys"."id", - "api_keys"."permissions", + "api_key"."id", + "api_key"."permissions", ( select to_json(obj) from ( select - "users"."id", - "users"."name", - "users"."email", - "users"."isAdmin", - "users"."quotaUsageInBytes", - "users"."quotaSizeInBytes" + "user"."id", + "user"."name", + "user"."email", + "user"."isAdmin", + "user"."quotaUsageInBytes", + "user"."quotaSizeInBytes" from - "users" + "user" where - "users"."id" = "api_keys"."userId" - and "users"."deletedAt" is null + "user"."id" = "api_key"."userId" + and "user"."deletedAt" is null ) as obj ) as "user" from - "api_keys" + "api_key" where - "api_keys"."key" = $1 + "api_key"."key" = $1 -- ApiKeyRepository.getById select @@ -37,7 +37,7 @@ select "updatedAt", "permissions" from - "api_keys" + "api_key" where "id" = $1::uuid and "userId" = $2 @@ -51,7 +51,7 @@ select "updatedAt", "permissions" from - "api_keys" + "api_key" where "userId" = $1 order by diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index 3a88642c0e..df8163be3e 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -10,10 +10,10 @@ select "visibility", "smart_search"."embedding" from - "assets" - left join "smart_search" on "assets"."id" = "smart_search"."assetId" + "asset" + left join "smart_search" on "asset"."id" = "smart_search"."assetId" where - "assets"."id" = $1::uuid + "asset"."id" = $1::uuid limit $2 @@ -28,227 +28,227 @@ select from ( select - "tags"."value" + "tag"."value" from - "tags" - inner join "tag_asset" on "tags"."id" = "tag_asset"."tagsId" + "tag" + inner join "tag_asset" on "tag"."id" = "tag_asset"."tagsId" where - "assets"."id" = "tag_asset"."assetsId" + "asset"."id" = "tag_asset"."assetsId" ) as agg ) as "tags" from - "assets" + "asset" where - "assets"."id" = $1::uuid + "asset"."id" = $1::uuid limit $2 -- AssetJobRepository.streamForThumbnailJob select - "assets"."id", - "assets"."thumbhash", + "asset"."id", + "asset"."thumbhash", ( select coalesce(json_agg(agg), '[]') from ( select - "asset_files"."id", - "asset_files"."path", - "asset_files"."type" + "asset_file"."id", + "asset_file"."path", + "asset_file"."type" from - "asset_files" + "asset_file" where - "asset_files"."assetId" = "assets"."id" + "asset_file"."assetId" = "asset"."id" ) as agg ) as "files" from - "assets" - inner join "asset_job_status" on "asset_job_status"."assetId" = "assets"."id" + "asset" + inner join "asset_job_status" on "asset_job_status"."assetId" = "asset"."id" where - "assets"."deletedAt" is null - and "assets"."visibility" != $1 + "asset"."deletedAt" is null + and "asset"."visibility" != $1 and ( "asset_job_status"."previewAt" is null or "asset_job_status"."thumbnailAt" is null - or "assets"."thumbhash" is null + or "asset"."thumbhash" is null ) -- AssetJobRepository.getForMigrationJob select - "assets"."id", - "assets"."ownerId", - "assets"."encodedVideoPath", + "asset"."id", + "asset"."ownerId", + "asset"."encodedVideoPath", ( select coalesce(json_agg(agg), '[]') from ( select - "asset_files"."id", - "asset_files"."path", - "asset_files"."type" + "asset_file"."id", + "asset_file"."path", + "asset_file"."type" from - "asset_files" + "asset_file" where - "asset_files"."assetId" = "assets"."id" + "asset_file"."assetId" = "asset"."id" ) as agg ) as "files" from - "assets" + "asset" where - "assets"."id" = $1 + "asset"."id" = $1 -- AssetJobRepository.getForGenerateThumbnailJob select - "assets"."id", - "assets"."visibility", - "assets"."originalFileName", - "assets"."originalPath", - "assets"."ownerId", - "assets"."thumbhash", - "assets"."type", + "asset"."id", + "asset"."visibility", + "asset"."originalFileName", + "asset"."originalPath", + "asset"."ownerId", + "asset"."thumbhash", + "asset"."type", ( select coalesce(json_agg(agg), '[]') from ( select - "asset_files"."id", - "asset_files"."path", - "asset_files"."type" + "asset_file"."id", + "asset_file"."path", + "asset_file"."type" from - "asset_files" + "asset_file" where - "asset_files"."assetId" = "assets"."id" + "asset_file"."assetId" = "asset"."id" ) as agg ) as "files", - to_json("exif") as "exifInfo" + to_json("asset_exif") as "exifInfo" from - "assets" - inner join "exif" on "assets"."id" = "exif"."assetId" + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where - "assets"."id" = $1 + "asset"."id" = $1 -- AssetJobRepository.getForMetadataExtraction select - "assets"."id", - "assets"."checksum", - "assets"."deviceAssetId", - "assets"."deviceId", - "assets"."fileCreatedAt", - "assets"."fileModifiedAt", - "assets"."isExternal", - "assets"."visibility", - "assets"."libraryId", - "assets"."livePhotoVideoId", - "assets"."localDateTime", - "assets"."originalFileName", - "assets"."originalPath", - "assets"."ownerId", - "assets"."sidecarPath", - "assets"."type", + "asset"."id", + "asset"."checksum", + "asset"."deviceAssetId", + "asset"."deviceId", + "asset"."fileCreatedAt", + "asset"."fileModifiedAt", + "asset"."isExternal", + "asset"."visibility", + "asset"."libraryId", + "asset"."livePhotoVideoId", + "asset"."localDateTime", + "asset"."originalFileName", + "asset"."originalPath", + "asset"."ownerId", + "asset"."sidecarPath", + "asset"."type", ( select coalesce(json_agg(agg), '[]') from ( select - "asset_faces".* + "asset_face".* from - "asset_faces" + "asset_face" where - "asset_faces"."assetId" = "assets"."id" - and "asset_faces"."deletedAt" is null + "asset_face"."assetId" = "asset"."id" + and "asset_face"."deletedAt" is null ) as agg ) as "faces" from - "assets" + "asset" where - "assets"."id" = $1 + "asset"."id" = $1 -- AssetJobRepository.getAlbumThumbnailFiles select - "asset_files"."id", - "asset_files"."path", - "asset_files"."type" + "asset_file"."id", + "asset_file"."path", + "asset_file"."type" from - "asset_files" + "asset_file" where - "asset_files"."assetId" = $1 - and "asset_files"."type" = $2 + "asset_file"."assetId" = $1 + and "asset_file"."type" = $2 -- AssetJobRepository.streamForSearchDuplicates select - "assets"."id" + "asset"."id" from - "assets" - inner join "smart_search" on "assets"."id" = "smart_search"."assetId" - inner join "asset_job_status" as "job_status" on "job_status"."assetId" = "assets"."id" + "asset" + inner join "smart_search" on "asset"."id" = "smart_search"."assetId" + inner join "asset_job_status" as "job_status" on "job_status"."assetId" = "asset"."id" where - "assets"."deletedAt" is null - and "assets"."visibility" in ('archive', 'timeline') + "asset"."deletedAt" is null + and "asset"."visibility" in ('archive', 'timeline') and "job_status"."duplicatesDetectedAt" is null -- AssetJobRepository.streamForEncodeClip select - "assets"."id" + "asset"."id" from - "assets" - inner join "asset_job_status" as "job_status" on "assetId" = "assets"."id" + "asset" + inner join "asset_job_status" as "job_status" on "assetId" = "asset"."id" where - "assets"."visibility" != $1 - and "assets"."deletedAt" is null + "asset"."visibility" != $1 + and "asset"."deletedAt" is null and "job_status"."previewAt" is not null and not exists ( select from "smart_search" where - "assetId" = "assets"."id" + "assetId" = "asset"."id" ) -- AssetJobRepository.getForClipEncoding select - "assets"."id", - "assets"."visibility", + "asset"."id", + "asset"."visibility", ( select coalesce(json_agg(agg), '[]') from ( select - "asset_files"."id", - "asset_files"."path", - "asset_files"."type" + "asset_file"."id", + "asset_file"."path", + "asset_file"."type" from - "asset_files" + "asset_file" where - "asset_files"."assetId" = "assets"."id" - and "asset_files"."type" = $1 + "asset_file"."assetId" = "asset"."id" + and "asset_file"."type" = $1 ) as agg ) as "files" from - "assets" + "asset" where - "assets"."id" = $2 + "asset"."id" = $2 -- AssetJobRepository.getForDetectFacesJob select - "assets"."id", - "assets"."visibility", - to_json("exif") as "exifInfo", + "asset"."id", + "asset"."visibility", + to_json("asset_exif") as "exifInfo", ( select coalesce(json_agg(agg), '[]') from ( select - "asset_faces".* + "asset_face".* from - "asset_faces" + "asset_face" where - "asset_faces"."assetId" = "assets"."id" + "asset_face"."assetId" = "asset"."id" ) as agg ) as "faces", ( @@ -257,67 +257,67 @@ select from ( select - "asset_files"."id", - "asset_files"."path", - "asset_files"."type" + "asset_file"."id", + "asset_file"."path", + "asset_file"."type" from - "asset_files" + "asset_file" where - "asset_files"."assetId" = "assets"."id" - and "asset_files"."type" = $1 + "asset_file"."assetId" = "asset"."id" + and "asset_file"."type" = $1 ) as agg ) as "files" from - "assets" - inner join "exif" on "assets"."id" = "exif"."assetId" + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where - "assets"."id" = $2 + "asset"."id" = $2 -- AssetJobRepository.getForSyncAssets select - "assets"."id", - "assets"."isOffline", - "assets"."libraryId", - "assets"."originalPath", - "assets"."status", - "assets"."fileModifiedAt" + "asset"."id", + "asset"."isOffline", + "asset"."libraryId", + "asset"."originalPath", + "asset"."status", + "asset"."fileModifiedAt" from - "assets" + "asset" where - "assets"."id" = any ($1::uuid[]) + "asset"."id" = any ($1::uuid[]) -- AssetJobRepository.getForAssetDeletion select - "assets"."id", - "assets"."visibility", - "assets"."libraryId", - "assets"."ownerId", - "assets"."livePhotoVideoId", - "assets"."sidecarPath", - "assets"."encodedVideoPath", - "assets"."originalPath", - to_json("exif") as "exifInfo", + "asset"."id", + "asset"."visibility", + "asset"."libraryId", + "asset"."ownerId", + "asset"."livePhotoVideoId", + "asset"."sidecarPath", + "asset"."encodedVideoPath", + "asset"."originalPath", + to_json("asset_exif") as "exifInfo", ( select coalesce(json_agg(agg), '[]') from ( select - "asset_faces".*, + "asset_face".*, "person" as "person" from - "asset_faces" + "asset_face" left join lateral ( select "person".* from "person" where - "asset_faces"."personId" = "person"."id" + "asset_face"."personId" = "person"."id" ) as "person" on true where - "asset_faces"."assetId" = "assets"."id" - and "asset_faces"."deletedAt" is null + "asset_face"."assetId" = "asset"."id" + and "asset_face"."deletedAt" is null ) as agg ) as "faces", ( @@ -326,156 +326,156 @@ select from ( select - "asset_files"."id", - "asset_files"."path", - "asset_files"."type" + "asset_file"."id", + "asset_file"."path", + "asset_file"."type" from - "asset_files" + "asset_file" where - "asset_files"."assetId" = "assets"."id" + "asset_file"."assetId" = "asset"."id" ) as agg ) as "files", to_json("stacked_assets") as "stack" from - "assets" - left join "exif" on "assets"."id" = "exif"."assetId" - left join "asset_stack" on "asset_stack"."id" = "assets"."stackId" + "asset" + left join "asset_exif" on "asset"."id" = "asset_exif"."assetId" + left join "stack" on "stack"."id" = "asset"."stackId" left join lateral ( select - "asset_stack"."id", - "asset_stack"."primaryAssetId", + "stack"."id", + "stack"."primaryAssetId", array_agg("stacked") as "assets" from - "assets" as "stacked" + "asset" as "stacked" where "stacked"."deletedAt" is not null and "stacked"."visibility" = $1 - and "stacked"."stackId" = "asset_stack"."id" + and "stacked"."stackId" = "stack"."id" group by - "asset_stack"."id" - ) as "stacked_assets" on "asset_stack"."id" is not null + "stack"."id" + ) as "stacked_assets" on "stack"."id" is not null where - "assets"."id" = $2 + "asset"."id" = $2 -- AssetJobRepository.streamForVideoConversion select - "assets"."id" + "asset"."id" from - "assets" + "asset" where - "assets"."type" = $1 + "asset"."type" = $1 and ( - "assets"."encodedVideoPath" is null - or "assets"."encodedVideoPath" = $2 + "asset"."encodedVideoPath" is null + or "asset"."encodedVideoPath" = $2 ) - and "assets"."visibility" != $3 - and "assets"."deletedAt" is null + and "asset"."visibility" != $3 + and "asset"."deletedAt" is null -- AssetJobRepository.getForVideoConversion select - "assets"."id", - "assets"."ownerId", - "assets"."originalPath", - "assets"."encodedVideoPath" + "asset"."id", + "asset"."ownerId", + "asset"."originalPath", + "asset"."encodedVideoPath" from - "assets" + "asset" where - "assets"."id" = $1 - and "assets"."type" = $2 + "asset"."id" = $1 + and "asset"."type" = $2 -- AssetJobRepository.streamForMetadataExtraction select - "assets"."id" + "asset"."id" from - "assets" - left join "asset_job_status" on "asset_job_status"."assetId" = "assets"."id" + "asset" + left join "asset_job_status" on "asset_job_status"."assetId" = "asset"."id" where ( "asset_job_status"."metadataExtractedAt" is null or "asset_job_status"."assetId" is null ) - and "assets"."deletedAt" is null + and "asset"."deletedAt" is null -- AssetJobRepository.getForStorageTemplateJob select - "assets"."id", - "assets"."ownerId", - "assets"."type", - "assets"."checksum", - "assets"."originalPath", - "assets"."isExternal", - "assets"."sidecarPath", - "assets"."originalFileName", - "assets"."livePhotoVideoId", - "assets"."fileCreatedAt", - "exif"."timeZone", - "exif"."fileSizeInByte" + "asset"."id", + "asset"."ownerId", + "asset"."type", + "asset"."checksum", + "asset"."originalPath", + "asset"."isExternal", + "asset"."sidecarPath", + "asset"."originalFileName", + "asset"."livePhotoVideoId", + "asset"."fileCreatedAt", + "asset_exif"."timeZone", + "asset_exif"."fileSizeInByte" from - "assets" - inner join "exif" on "assets"."id" = "exif"."assetId" + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where - "assets"."deletedAt" is null - and "assets"."id" = $1 + "asset"."deletedAt" is null + and "asset"."id" = $1 -- AssetJobRepository.streamForStorageTemplateJob select - "assets"."id", - "assets"."ownerId", - "assets"."type", - "assets"."checksum", - "assets"."originalPath", - "assets"."isExternal", - "assets"."sidecarPath", - "assets"."originalFileName", - "assets"."livePhotoVideoId", - "assets"."fileCreatedAt", - "exif"."timeZone", - "exif"."fileSizeInByte" + "asset"."id", + "asset"."ownerId", + "asset"."type", + "asset"."checksum", + "asset"."originalPath", + "asset"."isExternal", + "asset"."sidecarPath", + "asset"."originalFileName", + "asset"."livePhotoVideoId", + "asset"."fileCreatedAt", + "asset_exif"."timeZone", + "asset_exif"."fileSizeInByte" from - "assets" - inner join "exif" on "assets"."id" = "exif"."assetId" + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where - "assets"."deletedAt" is null + "asset"."deletedAt" is null -- AssetJobRepository.streamForDeletedJob select "id", "isOffline" from - "assets" + "asset" where - "assets"."deletedAt" <= $1 + "asset"."deletedAt" <= $1 -- AssetJobRepository.streamForSidecar select - "assets"."id" + "asset"."id" from - "assets" + "asset" where ( - "assets"."sidecarPath" = $1 - or "assets"."sidecarPath" is null + "asset"."sidecarPath" = $1 + or "asset"."sidecarPath" is null ) - and "assets"."visibility" != $2 + and "asset"."visibility" != $2 -- AssetJobRepository.streamForDetectFacesJob select - "assets"."id" + "asset"."id" from - "assets" - inner join "asset_job_status" as "job_status" on "assetId" = "assets"."id" + "asset" + inner join "asset_job_status" as "job_status" on "assetId" = "asset"."id" where - "assets"."visibility" != $1 - and "assets"."deletedAt" is null + "asset"."visibility" != $1 + and "asset"."deletedAt" is null and "job_status"."previewAt" is not null and "job_status"."facesRecognizedAt" is null order by - "assets"."createdAt" desc + "asset"."createdAt" desc -- AssetJobRepository.streamForMigrationJob select "id" from - "assets" + "asset" where - "assets"."deletedAt" is null + "asset"."deletedAt" is null diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index a781ea8239..e482a38a9a 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -1,7 +1,7 @@ -- NOTE: This file is auto generated by ./sql-generator -- AssetRepository.updateAllExif -update "exif" +update "asset_exif" set "model" = $1 where @@ -23,42 +23,42 @@ with min(("localDateTime" at time zone 'UTC')::date) )::int from - assets + asset ), date_part('year', current_date)::int - 1 ) as "year" ) select "a".*, - to_json("exif") as "exifInfo" + to_json("asset_exif") as "exifInfo" from "today" inner join lateral ( select - "assets".* + "asset".* from - "assets" - inner join "asset_job_status" on "assets"."id" = "asset_job_status"."assetId" + "asset" + inner join "asset_job_status" on "asset"."id" = "asset_job_status"."assetId" where "asset_job_status"."previewAt" is not null - and (assets."localDateTime" at time zone 'UTC')::date = today.date - and "assets"."ownerId" = any ($3::uuid[]) - and "assets"."visibility" = $4 + and (asset."localDateTime" at time zone 'UTC')::date = today.date + and "asset"."ownerId" = any ($3::uuid[]) + and "asset"."visibility" = $4 and exists ( select from - "asset_files" + "asset_file" where - "assetId" = "assets"."id" - and "asset_files"."type" = $5 + "assetId" = "asset"."id" + and "asset_file"."type" = $5 ) - and "assets"."deletedAt" is null + and "asset"."deletedAt" is null order by - (assets."localDateTime" at time zone 'UTC')::date desc + (asset."localDateTime" at time zone 'UTC')::date desc limit $6 ) as "a" on true - inner join "exif" on "a"."id" = "exif"."assetId" + inner join "asset_exif" on "a"."id" = "asset_exif"."assetId" ) select date_part( @@ -75,36 +75,36 @@ order by -- AssetRepository.getByIds select - "assets".* + "asset".* from - "assets" + "asset" where - "assets"."id" = any ($1::uuid[]) + "asset"."id" = any ($1::uuid[]) -- AssetRepository.getByIdsWithAllRelationsButStacks select - "assets".*, + "asset".*, ( select coalesce(json_agg(agg), '[]') from ( select - "asset_faces".*, + "asset_face".*, "person" as "person" from - "asset_faces" + "asset_face" left join lateral ( select "person".* from "person" where - "asset_faces"."personId" = "person"."id" + "asset_face"."personId" = "person"."id" ) as "person" on true where - "asset_faces"."assetId" = "assets"."id" - and "asset_faces"."deletedAt" is null + "asset_face"."assetId" = "asset"."id" + and "asset_face"."deletedAt" is null ) as agg ) as "faces", ( @@ -113,36 +113,36 @@ select from ( select - "tags"."id", - "tags"."value", - "tags"."createdAt", - "tags"."updatedAt", - "tags"."color", - "tags"."parentId" + "tag"."id", + "tag"."value", + "tag"."createdAt", + "tag"."updatedAt", + "tag"."color", + "tag"."parentId" from - "tags" - inner join "tag_asset" on "tags"."id" = "tag_asset"."tagsId" + "tag" + inner join "tag_asset" on "tag"."id" = "tag_asset"."tagsId" where - "assets"."id" = "tag_asset"."assetsId" + "asset"."id" = "tag_asset"."assetsId" ) as agg ) as "tags", - to_json("exif") as "exifInfo" + to_json("asset_exif") as "exifInfo" from - "assets" - left join "exif" on "assets"."id" = "exif"."assetId" + "asset" + left join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where - "assets"."id" = any ($1::uuid[]) + "asset"."id" = any ($1::uuid[]) -- AssetRepository.deleteAll -delete from "assets" +delete from "asset" where "ownerId" = $1 -- AssetRepository.getByLibraryIdAndOriginalPath select - "assets".* + "asset".* from - "assets" + "asset" where "libraryId" = $1::uuid and "originalPath" = $2 @@ -153,7 +153,7 @@ limit select "deviceAssetId" from - "assets" + "asset" where "ownerId" = $1::uuid and "deviceId" = $2 @@ -164,22 +164,22 @@ where select count(*) as "count" from - "assets" + "asset" where "livePhotoVideoId" = $1::uuid -- AssetRepository.getById select - "assets".* + "asset".* from - "assets" + "asset" where - "assets"."id" = $1::uuid + "asset"."id" = $1::uuid limit $2 -- AssetRepository.updateAll -update "assets" +update "asset" set "deviceId" = $1 where @@ -187,9 +187,9 @@ where -- AssetRepository.getByChecksum select - "assets".* + "asset".* from - "assets" + "asset" where "ownerId" = $1::uuid and "checksum" = $2 @@ -203,7 +203,7 @@ select "checksum", "deletedAt" from - "assets" + "asset" where "ownerId" = $1::uuid and "checksum" in ($2) @@ -212,7 +212,7 @@ where select "id" from - "assets" + "asset" where "ownerId" = $1::uuid and "checksum" = $2 @@ -222,20 +222,20 @@ limit -- AssetRepository.getTimeBuckets with - "assets" as ( + "asset" as ( select date_trunc('MONTH', "localDateTime" AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' as "timeBucket" from - "assets" + "asset" where - "assets"."deletedAt" is null - and "assets"."visibility" in ('archive', 'timeline') + "asset"."deletedAt" is null + and "asset"."visibility" in ('archive', 'timeline') ) select ("timeBucket" AT TIME ZONE 'UTC')::date::text as "timeBucket", count(*) as "count" from - "assets" + "asset" group by "timeBucket" order by @@ -245,37 +245,37 @@ order by with "cte" as ( select - "assets"."duration", - "assets"."id", - "assets"."visibility", - "assets"."isFavorite", - assets.type = 'IMAGE' as "isImage", - assets."deletedAt" is not null as "isTrashed", - "assets"."livePhotoVideoId", + "asset"."duration", + "asset"."id", + "asset"."visibility", + "asset"."isFavorite", + asset.type = 'IMAGE' as "isImage", + asset."deletedAt" is not null as "isTrashed", + "asset"."livePhotoVideoId", extract( epoch from ( - assets."localDateTime" - assets."fileCreatedAt" at time zone 'UTC' + asset."localDateTime" - asset."fileCreatedAt" at time zone 'UTC' ) )::real / 3600 as "localOffsetHours", - "assets"."ownerId", - "assets"."status", - assets."fileCreatedAt" at time zone 'utc' as "fileCreatedAt", - encode("assets"."thumbhash", 'base64') as "thumbhash", - "exif"."city", - "exif"."country", - "exif"."projectionType", + "asset"."ownerId", + "asset"."status", + asset."fileCreatedAt" at time zone 'utc' as "fileCreatedAt", + encode("asset"."thumbhash", 'base64') as "thumbhash", + "asset_exif"."city", + "asset_exif"."country", + "asset_exif"."projectionType", coalesce( case - when exif."exifImageHeight" = 0 - or exif."exifImageWidth" = 0 then 1 - when "exif"."orientation" in ('5', '6', '7', '8', '-90', '90') then round( - exif."exifImageHeight"::numeric / exif."exifImageWidth"::numeric, + when asset_exif."exifImageHeight" = 0 + or asset_exif."exifImageWidth" = 0 then 1 + when "asset_exif"."orientation" in ('5', '6', '7', '8', '-90', '90') then round( + asset_exif."exifImageHeight"::numeric / asset_exif."exifImageWidth"::numeric, 3 ) else round( - exif."exifImageWidth"::numeric / exif."exifImageHeight"::numeric, + asset_exif."exifImageWidth"::numeric / asset_exif."exifImageHeight"::numeric, 3 ) end, @@ -283,34 +283,34 @@ with ) as "ratio", "stack" from - "assets" - inner join "exif" on "assets"."id" = "exif"."assetId" + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" left join lateral ( select array[stacked."stackId"::text, count('stacked')::text] as "stack" from - "assets" as "stacked" + "asset" as "stacked" where - "stacked"."stackId" = "assets"."stackId" + "stacked"."stackId" = "asset"."stackId" and "stacked"."deletedAt" is null and "stacked"."visibility" = $1 group by "stacked"."stackId" ) as "stacked_assets" on true where - "assets"."deletedAt" is null - and "assets"."visibility" in ('archive', 'timeline') + "asset"."deletedAt" is null + and "asset"."visibility" in ('archive', 'timeline') and date_trunc('MONTH', "localDateTime" AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' = $2 and not exists ( select from - "asset_stack" + "stack" where - "asset_stack"."id" = "assets"."stackId" - and "asset_stack"."primaryAssetId" != "assets"."id" + "stack"."id" = "asset"."stackId" + and "stack"."primaryAssetId" != "asset"."id" ) order by - "assets"."fileCreatedAt" desc + "asset"."fileCreatedAt" desc ), "agg" as ( select @@ -345,7 +345,7 @@ with select "city" from - "exif" + "asset_exif" where "city" is not null group by @@ -354,12 +354,12 @@ with count("assetId") >= $1 ) select distinct - on ("exif"."city") "assetId" as "data", - "exif"."city" as "value" + on ("asset_exif"."city") "assetId" as "data", + "asset_exif"."city" as "value" from - "assets" - inner join "exif" on "assets"."id" = "exif"."assetId" - inner join "cities" on "exif"."city" = "cities"."city" + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" + inner join "cities" on "asset_exif"."city" = "cities"."city" where "ownerId" = $2::uuid and "visibility" = $3 @@ -370,63 +370,63 @@ limit -- AssetRepository.getAllForUserFullSync select - "assets".*, - to_json("exif") as "exifInfo", + "asset".*, + to_json("asset_exif") as "exifInfo", to_json("stacked_assets") as "stack" from - "assets" - left join "exif" on "assets"."id" = "exif"."assetId" - left join "asset_stack" on "asset_stack"."id" = "assets"."stackId" + "asset" + left join "asset_exif" on "asset"."id" = "asset_exif"."assetId" + left join "stack" on "stack"."id" = "asset"."stackId" left join lateral ( select - "asset_stack".*, + "stack".*, count("stacked") as "assetCount" from - "assets" as "stacked" + "asset" as "stacked" where - "stacked"."stackId" = "asset_stack"."id" + "stacked"."stackId" = "stack"."id" group by - "asset_stack"."id" - ) as "stacked_assets" on "asset_stack"."id" is not null + "stack"."id" + ) as "stacked_assets" on "stack"."id" is not null where - "assets"."ownerId" = $1::uuid - and "assets"."visibility" != $2 - and "assets"."updatedAt" <= $3 - and "assets"."id" > $4 + "asset"."ownerId" = $1::uuid + and "asset"."visibility" != $2 + and "asset"."updatedAt" <= $3 + and "asset"."id" > $4 order by - "assets"."id" + "asset"."id" limit $5 -- AssetRepository.getChangedDeltaSync select - "assets".*, - to_json("exif") as "exifInfo", + "asset".*, + to_json("asset_exif") as "exifInfo", to_json("stacked_assets") as "stack" from - "assets" - left join "exif" on "assets"."id" = "exif"."assetId" - left join "asset_stack" on "asset_stack"."id" = "assets"."stackId" + "asset" + left join "asset_exif" on "asset"."id" = "asset_exif"."assetId" + left join "stack" on "stack"."id" = "asset"."stackId" left join lateral ( select - "asset_stack".*, + "stack".*, count("stacked") as "assetCount" from - "assets" as "stacked" + "asset" as "stacked" where - "stacked"."stackId" = "asset_stack"."id" + "stacked"."stackId" = "stack"."id" group by - "asset_stack"."id" - ) as "stacked_assets" on "asset_stack"."id" is not null + "stack"."id" + ) as "stacked_assets" on "stack"."id" is not null where - "assets"."ownerId" = any ($1::uuid[]) - and "assets"."visibility" != $2 - and "assets"."updatedAt" > $3 + "asset"."ownerId" = any ($1::uuid[]) + and "asset"."visibility" != $2 + and "asset"."updatedAt" > $3 limit $4 -- AssetRepository.detectOfflineExternalAssets -update "assets" +update "asset" set "isOffline" = $1, "deletedAt" = $2 @@ -449,9 +449,9 @@ where select "originalPath" from - "assets" + "asset" where - "assets"."originalPath" = "path" + "asset"."originalPath" = "path" and "libraryId" = $2::uuid and "isExternal" = $3 ) diff --git a/server/src/queries/duplicate.repository.sql b/server/src/queries/duplicate.repository.sql index 727ddd5b8d..8913007dea 100644 --- a/server/src/queries/duplicate.repository.sql +++ b/server/src/queries/duplicate.repository.sql @@ -4,31 +4,31 @@ with "duplicates" as ( select - "assets"."duplicateId", + "asset"."duplicateId", json_agg( - "asset" + "asset2" order by - "assets"."localDateTime" asc + "asset"."localDateTime" asc ) as "assets" from - "assets" + "asset" left join lateral ( select - "assets".*, - "exif" as "exifInfo" + "asset".*, + "asset_exif" as "exifInfo" from - "exif" + "asset_exif" where - "exif"."assetId" = "assets"."id" - ) as "asset" on true + "asset_exif"."assetId" = "asset"."id" + ) as "asset2" on true where - "assets"."visibility" in ('archive', 'timeline') - and "assets"."ownerId" = $1::uuid - and "assets"."duplicateId" is not null - and "assets"."deletedAt" is null - and "assets"."stackId" is null + "asset"."visibility" in ('archive', 'timeline') + and "asset"."ownerId" = $1::uuid + and "asset"."duplicateId" is not null + and "asset"."deletedAt" is null + and "asset"."stackId" is null group by - "assets"."duplicateId" + "asset"."duplicateId" ), "unique" as ( select @@ -39,13 +39,13 @@ with json_array_length("assets") = $2 ), "removed_unique" as ( - update "assets" + update "asset" set "duplicateId" = $3 from "unique" where - "assets"."duplicateId" = "unique"."duplicateId" + "asset"."duplicateId" = "unique"."duplicateId" ) select * @@ -61,7 +61,7 @@ where ) -- DuplicateRepository.delete -update "assets" +update "asset" set "duplicateId" = $1 where @@ -69,7 +69,7 @@ where and "duplicateId" = $3 -- DuplicateRepository.deleteAll -update "assets" +update "asset" set "duplicateId" = $1 where @@ -83,19 +83,19 @@ set with "cte" as ( select - "assets"."id" as "assetId", - "assets"."duplicateId", + "asset"."id" as "assetId", + "asset"."duplicateId", smart_search.embedding <=> $1 as "distance" from - "assets" - inner join "smart_search" on "assets"."id" = "smart_search"."assetId" + "asset" + inner join "smart_search" on "asset"."id" = "smart_search"."assetId" where - "assets"."visibility" in ('archive', 'timeline') - and "assets"."ownerId" = any ($2::uuid[]) - and "assets"."deletedAt" is null - and "assets"."type" = $3 - and "assets"."id" != $4::uuid - and "assets"."stackId" is null + "asset"."visibility" in ('archive', 'timeline') + and "asset"."ownerId" = any ($2::uuid[]) + and "asset"."deletedAt" is null + and "asset"."type" = $3 + and "asset"."id" != $4::uuid + and "asset"."stackId" is null order by "distance" limit @@ -110,7 +110,7 @@ where commit -- DuplicateRepository.merge -update "assets" +update "asset" set where ( diff --git a/server/src/queries/library.repository.sql b/server/src/queries/library.repository.sql index f17d9663d6..f0bd05973f 100644 --- a/server/src/queries/library.repository.sql +++ b/server/src/queries/library.repository.sql @@ -2,30 +2,30 @@ -- LibraryRepository.get select - "libraries".* + "library".* from - "libraries" + "library" where - "libraries"."id" = $1 - and "libraries"."deletedAt" is null + "library"."id" = $1 + and "library"."deletedAt" is null -- LibraryRepository.getAll select - "libraries".* + "library".* from - "libraries" + "library" where - "libraries"."deletedAt" is null + "library"."deletedAt" is null order by "createdAt" asc -- LibraryRepository.getAllDeleted select - "libraries".* + "library".* from - "libraries" + "library" where - "libraries"."deletedAt" is not null + "library"."deletedAt" is not null order by "createdAt" asc @@ -34,32 +34,32 @@ select count(*) filter ( where ( - "assets"."type" = $1 - and "assets"."visibility" != $2 + "asset"."type" = $1 + and "asset"."visibility" != $2 ) ) as "photos", count(*) filter ( where ( - "assets"."type" = $3 - and "assets"."visibility" != $4 + "asset"."type" = $3 + and "asset"."visibility" != $4 ) ) as "videos", - coalesce(sum("exif"."fileSizeInByte"), $5) as "usage" + coalesce(sum("asset_exif"."fileSizeInByte"), $5) as "usage" from - "libraries" - inner join "assets" on "assets"."libraryId" = "libraries"."id" - left join "exif" on "exif"."assetId" = "assets"."id" + "library" + inner join "asset" on "asset"."libraryId" = "library"."id" + left join "asset_exif" on "asset_exif"."assetId" = "asset"."id" where - "libraries"."id" = $6 + "library"."id" = $6 group by - "libraries"."id" + "library"."id" select 0::int as "photos", 0::int as "videos", 0::int as "usage", 0::int as "total" from - "libraries" + "library" where - "libraries"."id" = $1 + "library"."id" = $1 diff --git a/server/src/queries/map.repository.sql b/server/src/queries/map.repository.sql index edfdec13d2..df2136a422 100644 --- a/server/src/queries/map.repository.sql +++ b/server/src/queries/map.repository.sql @@ -3,28 +3,28 @@ -- MapRepository.getMapMarkers select "id", - "exif"."latitude" as "lat", - "exif"."longitude" as "lon", - "exif"."city", - "exif"."state", - "exif"."country" + "asset_exif"."latitude" as "lat", + "asset_exif"."longitude" as "lon", + "asset_exif"."city", + "asset_exif"."state", + "asset_exif"."country" from - "assets" - inner join "exif" on "assets"."id" = "exif"."assetId" - and "exif"."latitude" is not null - and "exif"."longitude" is not null + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" + and "asset_exif"."latitude" is not null + and "asset_exif"."longitude" is not null where - "assets"."visibility" = $1 + "asset"."visibility" = $1 and "deletedAt" is null and ( "ownerId" in ($2) or exists ( select from - "albums_assets_assets" + "album_asset" where - "assets"."id" = "albums_assets_assets"."assetsId" - and "albums_assets_assets"."albumsId" in ($3) + "asset"."id" = "album_asset"."assetsId" + and "album_asset"."albumsId" in ($3) ) ) order by diff --git a/server/src/queries/memory.repository.sql b/server/src/queries/memory.repository.sql index 3cdda8a45c..b3cc7240ae 100644 --- a/server/src/queries/memory.repository.sql +++ b/server/src/queries/memory.repository.sql @@ -4,7 +4,7 @@ select count(*) as "total" from - "memories" + "memory" where "deletedAt" is null and "ownerId" = $1 @@ -13,7 +13,7 @@ where select count(*) as "total" from - "memories" + "memory" where ( "showAt" is null @@ -34,21 +34,21 @@ select from ( select - "assets".* + "asset".* from - "assets" - inner join "memories_assets_assets" on "assets"."id" = "memories_assets_assets"."assetsId" + "asset" + inner join "memory_asset" on "asset"."id" = "memory_asset"."assetsId" where - "memories_assets_assets"."memoriesId" = "memories"."id" - and "assets"."visibility" = 'timeline' - and "assets"."deletedAt" is null + "memory_asset"."memoriesId" = "memory"."id" + and "asset"."visibility" = 'timeline' + and "asset"."deletedAt" is null order by - "assets"."fileCreatedAt" asc + "asset"."fileCreatedAt" asc ) as agg ) as "assets", - "memories".* + "memory".* from - "memories" + "memory" where "deletedAt" is null and "ownerId" = $1 @@ -63,21 +63,21 @@ select from ( select - "assets".* + "asset".* from - "assets" - inner join "memories_assets_assets" on "assets"."id" = "memories_assets_assets"."assetsId" + "asset" + inner join "memory_asset" on "asset"."id" = "memory_asset"."assetsId" where - "memories_assets_assets"."memoriesId" = "memories"."id" - and "assets"."visibility" = 'timeline' - and "assets"."deletedAt" is null + "memory_asset"."memoriesId" = "memory"."id" + and "asset"."visibility" = 'timeline' + and "asset"."deletedAt" is null order by - "assets"."fileCreatedAt" asc + "asset"."fileCreatedAt" asc ) as agg ) as "assets", - "memories".* + "memory".* from - "memories" + "memory" where ( "showAt" is null @@ -94,66 +94,66 @@ order by -- MemoryRepository.get select - "memories".*, + "memory".*, ( select coalesce(json_agg(agg), '[]') from ( select - "assets".* + "asset".* from - "assets" - inner join "memories_assets_assets" on "assets"."id" = "memories_assets_assets"."assetsId" + "asset" + inner join "memory_asset" on "asset"."id" = "memory_asset"."assetsId" where - "memories_assets_assets"."memoriesId" = "memories"."id" - and "assets"."visibility" = 'timeline' - and "assets"."deletedAt" is null + "memory_asset"."memoriesId" = "memory"."id" + and "asset"."visibility" = 'timeline' + and "asset"."deletedAt" is null order by - "assets"."fileCreatedAt" asc + "asset"."fileCreatedAt" asc ) as agg ) as "assets" from - "memories" + "memory" where "id" = $1 and "deletedAt" is null -- MemoryRepository.update -update "memories" +update "memory" set "ownerId" = $1, "isSaved" = $2 where "id" = $3 select - "memories".*, + "memory".*, ( select coalesce(json_agg(agg), '[]') from ( select - "assets".* + "asset".* from - "assets" - inner join "memories_assets_assets" on "assets"."id" = "memories_assets_assets"."assetsId" + "asset" + inner join "memory_asset" on "asset"."id" = "memory_asset"."assetsId" where - "memories_assets_assets"."memoriesId" = "memories"."id" - and "assets"."visibility" = 'timeline' - and "assets"."deletedAt" is null + "memory_asset"."memoriesId" = "memory"."id" + and "asset"."visibility" = 'timeline' + and "asset"."deletedAt" is null order by - "assets"."fileCreatedAt" asc + "asset"."fileCreatedAt" asc ) as agg ) as "assets" from - "memories" + "memory" where "id" = $1 and "deletedAt" is null -- MemoryRepository.delete -delete from "memories" +delete from "memory" where "id" = $1 @@ -161,13 +161,13 @@ where select "assetsId" from - "memories_assets_assets" + "memory_asset" where "memoriesId" = $1 and "assetsId" in ($2) -- MemoryRepository.addAssetIds insert into - "memories_assets_assets" ("memoriesId", "assetsId") + "memory_asset" ("memoriesId", "assetsId") values ($1, $2) diff --git a/server/src/queries/notification.repository.sql b/server/src/queries/notification.repository.sql index f7e211d80a..cc704af2be 100644 --- a/server/src/queries/notification.repository.sql +++ b/server/src/queries/notification.repository.sql @@ -11,7 +11,7 @@ select "data", "readAt" from - "notifications" + "notification" where "userId" = $1 and "deletedAt" is null @@ -29,7 +29,7 @@ select "data", "readAt" from - "notifications" + "notification" where ( "userId" = $1 diff --git a/server/src/queries/partner.repository.sql b/server/src/queries/partner.repository.sql index 100f1bc638..213aa27d93 100644 --- a/server/src/queries/partner.repository.sql +++ b/server/src/queries/partner.repository.sql @@ -2,7 +2,7 @@ -- PartnerRepository.getAll select - "partners".*, + "partner".*, ( select to_json(obj) @@ -16,9 +16,9 @@ select "profileImagePath", "profileChangedAt" from - "users" as "sharedBy" + "user" as "sharedBy" where - "sharedBy"."id" = "partners"."sharedById" + "sharedBy"."id" = "partner"."sharedById" ) as obj ) as "sharedBy", ( @@ -34,16 +34,16 @@ select "profileImagePath", "profileChangedAt" from - "users" as "sharedWith" + "user" as "sharedWith" where - "sharedWith"."id" = "partners"."sharedWithId" + "sharedWith"."id" = "partner"."sharedWithId" ) as obj ) as "sharedWith" from - "partners" - inner join "users" as "sharedBy" on "partners"."sharedById" = "sharedBy"."id" + "partner" + inner join "user" as "sharedBy" on "partner"."sharedById" = "sharedBy"."id" and "sharedBy"."deletedAt" is null - inner join "users" as "sharedWith" on "partners"."sharedWithId" = "sharedWith"."id" + inner join "user" as "sharedWith" on "partner"."sharedWithId" = "sharedWith"."id" and "sharedWith"."deletedAt" is null where ( @@ -53,7 +53,7 @@ where -- PartnerRepository.get select - "partners".*, + "partner".*, ( select to_json(obj) @@ -67,9 +67,9 @@ select "profileImagePath", "profileChangedAt" from - "users" as "sharedBy" + "user" as "sharedBy" where - "sharedBy"."id" = "partners"."sharedById" + "sharedBy"."id" = "partner"."sharedById" ) as obj ) as "sharedBy", ( @@ -85,23 +85,23 @@ select "profileImagePath", "profileChangedAt" from - "users" as "sharedWith" + "user" as "sharedWith" where - "sharedWith"."id" = "partners"."sharedWithId" + "sharedWith"."id" = "partner"."sharedWithId" ) as obj ) as "sharedWith" from - "partners" - inner join "users" as "sharedBy" on "partners"."sharedById" = "sharedBy"."id" + "partner" + inner join "user" as "sharedBy" on "partner"."sharedById" = "sharedBy"."id" and "sharedBy"."deletedAt" is null - inner join "users" as "sharedWith" on "partners"."sharedWithId" = "sharedWith"."id" + inner join "user" as "sharedWith" on "partner"."sharedWithId" = "sharedWith"."id" and "sharedWith"."deletedAt" is null where "sharedWithId" = $1 and "sharedById" = $2 -- PartnerRepository.update -update "partners" +update "partner" set "inTimeline" = $1 where @@ -122,9 +122,9 @@ returning "profileImagePath", "profileChangedAt" from - "users" as "sharedBy" + "user" as "sharedBy" where - "sharedBy"."id" = "partners"."sharedById" + "sharedBy"."id" = "partner"."sharedById" ) as obj ) as "sharedBy", ( @@ -140,14 +140,14 @@ returning "profileImagePath", "profileChangedAt" from - "users" as "sharedWith" + "user" as "sharedWith" where - "sharedWith"."id" = "partners"."sharedWithId" + "sharedWith"."id" = "partner"."sharedWithId" ) as obj ) as "sharedWith" -- PartnerRepository.remove -delete from "partners" +delete from "partner" where "sharedWithId" = $1 and "sharedById" = $2 diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index cbd952cd04..3e41edde9c 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -1,11 +1,11 @@ -- NOTE: This file is auto generated by ./sql-generator -- PersonRepository.reassignFaces -update "asset_faces" +update "asset_face" set "personId" = $1 where - "asset_faces"."personId" = $2 + "asset_face"."personId" = $2 -- PersonRepository.delete delete from "person" @@ -17,26 +17,26 @@ select "person".* from "person" - inner join "asset_faces" on "asset_faces"."personId" = "person"."id" - inner join "assets" on "asset_faces"."assetId" = "assets"."id" - and "assets"."visibility" = 'timeline' - and "assets"."deletedAt" is null + inner join "asset_face" on "asset_face"."personId" = "person"."id" + inner join "asset" on "asset_face"."assetId" = "asset"."id" + and "asset"."visibility" = 'timeline' + and "asset"."deletedAt" is null where "person"."ownerId" = $1 - and "asset_faces"."deletedAt" is null + and "asset_face"."deletedAt" is null and "person"."isHidden" = $2 group by "person"."id" having ( "person"."name" != $3 - or count("asset_faces"."assetId") >= $4 + or count("asset_face"."assetId") >= $4 ) order by "person"."isHidden" asc, "person"."isFavorite" desc, NULLIF(person.name, '') is null asc, - count("asset_faces"."assetId") desc, + count("asset_face"."assetId") desc, NULLIF(person.name, '') asc nulls last, "person"."createdAt" limit @@ -49,17 +49,17 @@ select "person".* from "person" - left join "asset_faces" on "asset_faces"."personId" = "person"."id" + left join "asset_face" on "asset_face"."personId" = "person"."id" where - "asset_faces"."deletedAt" is null + "asset_face"."deletedAt" is null group by "person"."id" having - count("asset_faces"."assetId") = $1 + count("asset_face"."assetId") = $1 -- PersonRepository.getFaces select - "asset_faces".*, + "asset_face".*, ( select to_json(obj) @@ -70,20 +70,20 @@ select from "person" where - "person"."id" = "asset_faces"."personId" + "person"."id" = "asset_face"."personId" ) as obj ) as "person" from - "asset_faces" + "asset_face" where - "asset_faces"."assetId" = $1 - and "asset_faces"."deletedAt" is null + "asset_face"."assetId" = $1 + and "asset_face"."deletedAt" is null order by - "asset_faces"."boundingBoxX1" asc + "asset_face"."boundingBoxX1" asc -- PersonRepository.getFaceById select - "asset_faces".*, + "asset_face".*, ( select to_json(obj) @@ -94,33 +94,33 @@ select from "person" where - "person"."id" = "asset_faces"."personId" + "person"."id" = "asset_face"."personId" ) as obj ) as "person" from - "asset_faces" + "asset_face" where - "asset_faces"."id" = $1 - and "asset_faces"."deletedAt" is null + "asset_face"."id" = $1 + and "asset_face"."deletedAt" is null -- PersonRepository.getFaceForFacialRecognitionJob select - "asset_faces"."id", - "asset_faces"."personId", - "asset_faces"."sourceType", + "asset_face"."id", + "asset_face"."personId", + "asset_face"."sourceType", ( select to_json(obj) from ( select - "assets"."ownerId", - "assets"."visibility", - "assets"."fileCreatedAt" + "asset"."ownerId", + "asset"."visibility", + "asset"."fileCreatedAt" from - "assets" + "asset" where - "assets"."id" = "asset_faces"."assetId" + "asset"."id" = "asset_face"."assetId" ) as obj ) as "asset", ( @@ -133,51 +133,51 @@ select from "face_search" where - "face_search"."faceId" = "asset_faces"."id" + "face_search"."faceId" = "asset_face"."id" ) as obj ) as "faceSearch" from - "asset_faces" + "asset_face" where - "asset_faces"."id" = $1 - and "asset_faces"."deletedAt" is null + "asset_face"."id" = $1 + and "asset_face"."deletedAt" is null -- PersonRepository.getDataForThumbnailGenerationJob select "person"."ownerId", - "asset_faces"."boundingBoxX1" as "x1", - "asset_faces"."boundingBoxY1" as "y1", - "asset_faces"."boundingBoxX2" as "x2", - "asset_faces"."boundingBoxY2" as "y2", - "asset_faces"."imageWidth" as "oldWidth", - "asset_faces"."imageHeight" as "oldHeight", - "assets"."type", - "assets"."originalPath", - "exif"."orientation" as "exifOrientation", + "asset_face"."boundingBoxX1" as "x1", + "asset_face"."boundingBoxY1" as "y1", + "asset_face"."boundingBoxX2" as "x2", + "asset_face"."boundingBoxY2" as "y2", + "asset_face"."imageWidth" as "oldWidth", + "asset_face"."imageHeight" as "oldHeight", + "asset"."type", + "asset"."originalPath", + "asset_exif"."orientation" as "exifOrientation", ( select - "asset_files"."path" + "asset_file"."path" from - "asset_files" + "asset_file" where - "asset_files"."assetId" = "assets"."id" - and "asset_files"."type" = 'preview' + "asset_file"."assetId" = "asset"."id" + and "asset_file"."type" = 'preview' ) as "previewPath" from "person" - inner join "asset_faces" on "asset_faces"."id" = "person"."faceAssetId" - inner join "assets" on "asset_faces"."assetId" = "assets"."id" - left join "exif" on "exif"."assetId" = "assets"."id" + inner join "asset_face" on "asset_face"."id" = "person"."faceAssetId" + inner join "asset" on "asset_face"."assetId" = "asset"."id" + left join "asset_exif" on "asset_exif"."assetId" = "asset"."id" where "person"."id" = $1 - and "asset_faces"."deletedAt" is null + and "asset_face"."deletedAt" is null -- PersonRepository.reassignFace -update "asset_faces" +update "asset_face" set "personId" = $1 where - "asset_faces"."id" = $2 + "asset_face"."id" = $2 -- PersonRepository.getByName select @@ -209,15 +209,15 @@ where -- PersonRepository.getStatistics select - count(distinct ("assets"."id")) as "count" + count(distinct ("asset"."id")) as "count" from - "asset_faces" - left join "assets" on "assets"."id" = "asset_faces"."assetId" - and "asset_faces"."personId" = $1 - and "assets"."visibility" = 'timeline' - and "assets"."deletedAt" is null + "asset_face" + left join "asset" on "asset"."id" = "asset_face"."assetId" + and "asset_face"."personId" = $1 + and "asset"."visibility" = 'timeline' + and "asset"."deletedAt" is null where - "asset_faces"."deletedAt" is null + "asset_face"."deletedAt" is null -- PersonRepository.getNumberOfPeople select @@ -235,18 +235,18 @@ where exists ( select from - "asset_faces" + "asset_face" where - "asset_faces"."personId" = "person"."id" - and "asset_faces"."deletedAt" is null + "asset_face"."personId" = "person"."id" + and "asset_face"."deletedAt" is null and exists ( select from - "assets" + "asset" where - "assets"."id" = "asset_faces"."assetId" - and "assets"."visibility" = 'timeline' - and "assets"."deletedAt" is null + "asset"."id" = "asset_face"."assetId" + and "asset"."visibility" = 'timeline' + and "asset"."deletedAt" is null ) ) and "person"."ownerId" = $2 @@ -268,18 +268,18 @@ from -- PersonRepository.getFacesByIds select - "asset_faces".*, + "asset_face".*, ( select to_json(obj) from ( select - "assets".* + "asset".* from - "assets" + "asset" where - "assets"."id" = "asset_faces"."assetId" + "asset"."id" = "asset_face"."assetId" ) as obj ) as "asset", ( @@ -292,24 +292,24 @@ select from "person" where - "person"."id" = "asset_faces"."personId" + "person"."id" = "asset_face"."personId" ) as obj ) as "person" from - "asset_faces" + "asset_face" where - "asset_faces"."assetId" in ($1) - and "asset_faces"."personId" in ($2) - and "asset_faces"."deletedAt" is null + "asset_face"."assetId" in ($1) + and "asset_face"."personId" in ($2) + and "asset_face"."deletedAt" is null -- PersonRepository.getRandomFace select - "asset_faces".* + "asset_face".* from - "asset_faces" + "asset_face" where - "asset_faces"."personId" = $1 - and "asset_faces"."deletedAt" is null + "asset_face"."personId" = $1 + and "asset_face"."deletedAt" is null -- PersonRepository.getLatestFaceDate select @@ -318,16 +318,16 @@ from "asset_job_status" -- PersonRepository.deleteAssetFace -delete from "asset_faces" +delete from "asset_face" where - "asset_faces"."id" = $1 + "asset_face"."id" = $1 -- PersonRepository.softDeleteAssetFaces -update "asset_faces" +update "asset_face" set "deletedAt" = $1 where - "asset_faces"."id" = $2 + "asset_face"."id" = $2 -- PersonRepository.getForPeopleDelete select diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index abbc063e8f..ef5363126f 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -2,19 +2,19 @@ -- SearchRepository.searchMetadata select - "assets".* + "asset".* from - "assets" - inner join "exif" on "assets"."id" = "exif"."assetId" + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where - "assets"."visibility" = $1 - and "assets"."fileCreatedAt" >= $2 - and "exif"."lensModel" = $3 - and "assets"."ownerId" = any ($4::uuid[]) - and "assets"."isFavorite" = $5 - and "assets"."deletedAt" is null + "asset"."visibility" = $1 + and "asset"."fileCreatedAt" >= $2 + and "asset_exif"."lensModel" = $3 + and "asset"."ownerId" = any ($4::uuid[]) + and "asset"."isFavorite" = $5 + and "asset"."deletedAt" is null order by - "assets"."fileCreatedAt" desc + "asset"."fileCreatedAt" desc limit $6 offset @@ -24,31 +24,31 @@ offset select count(*) as "total" from - "assets" - inner join "exif" on "assets"."id" = "exif"."assetId" + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where - "assets"."visibility" = $1 - and "assets"."fileCreatedAt" >= $2 - and "exif"."lensModel" = $3 - and "assets"."ownerId" = any ($4::uuid[]) - and "assets"."isFavorite" = $5 - and "assets"."deletedAt" is null + "asset"."visibility" = $1 + and "asset"."fileCreatedAt" >= $2 + and "asset_exif"."lensModel" = $3 + and "asset"."ownerId" = any ($4::uuid[]) + and "asset"."isFavorite" = $5 + and "asset"."deletedAt" is null -- SearchRepository.searchRandom ( select - "assets".* + "asset".* from - "assets" - inner join "exif" on "assets"."id" = "exif"."assetId" + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where - "assets"."visibility" = $1 - and "assets"."fileCreatedAt" >= $2 - and "exif"."lensModel" = $3 - and "assets"."ownerId" = any ($4::uuid[]) - and "assets"."isFavorite" = $5 - and "assets"."deletedAt" is null - and "assets"."id" < $6 + "asset"."visibility" = $1 + and "asset"."fileCreatedAt" >= $2 + and "asset_exif"."lensModel" = $3 + and "asset"."ownerId" = any ($4::uuid[]) + and "asset"."isFavorite" = $5 + and "asset"."deletedAt" is null + and "asset"."id" < $6 order by random() limit @@ -57,18 +57,18 @@ where union all ( select - "assets".* + "asset".* from - "assets" - inner join "exif" on "assets"."id" = "exif"."assetId" + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where - "assets"."visibility" = $8 - and "assets"."fileCreatedAt" >= $9 - and "exif"."lensModel" = $10 - and "assets"."ownerId" = any ($11::uuid[]) - and "assets"."isFavorite" = $12 - and "assets"."deletedAt" is null - and "assets"."id" > $13 + "asset"."visibility" = $8 + and "asset"."fileCreatedAt" >= $9 + and "asset_exif"."lensModel" = $10 + and "asset"."ownerId" = any ($11::uuid[]) + and "asset"."isFavorite" = $12 + and "asset"."deletedAt" is null + and "asset"."id" > $13 order by random() limit @@ -82,18 +82,18 @@ begin set local vchordrq.probes = 1 select - "assets".* + "asset".* from - "assets" - inner join "exif" on "assets"."id" = "exif"."assetId" - inner join "smart_search" on "assets"."id" = "smart_search"."assetId" + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" + inner join "smart_search" on "asset"."id" = "smart_search"."assetId" where - "assets"."visibility" = $1 - and "assets"."fileCreatedAt" >= $2 - and "exif"."lensModel" = $3 - and "assets"."ownerId" = any ($4::uuid[]) - and "assets"."isFavorite" = $5 - and "assets"."deletedAt" is null + "asset"."visibility" = $1 + and "asset"."fileCreatedAt" >= $2 + and "asset_exif"."lensModel" = $3 + and "asset"."ownerId" = any ($4::uuid[]) + and "asset"."isFavorite" = $5 + and "asset"."deletedAt" is null order by smart_search.embedding <=> $6 limit @@ -109,17 +109,17 @@ set with "cte" as ( select - "asset_faces"."id", - "asset_faces"."personId", + "asset_face"."id", + "asset_face"."personId", face_search.embedding <=> $1 as "distance" from - "asset_faces" - inner join "assets" on "assets"."id" = "asset_faces"."assetId" - inner join "face_search" on "face_search"."faceId" = "asset_faces"."id" - left join "person" on "person"."id" = "asset_faces"."personId" + "asset_face" + inner join "asset" on "asset"."id" = "asset_face"."assetId" + inner join "face_search" on "face_search"."faceId" = "asset_face"."id" + left join "person" on "person"."id" = "asset_face"."personId" where - "assets"."ownerId" = any ($2::uuid[]) - and "assets"."deletedAt" is null + "asset"."ownerId" = any ($2::uuid[]) + and "asset"."deletedAt" is null order by "distance" limit @@ -165,13 +165,13 @@ with recursive "city", "assetId" from - "exif" - inner join "assets" on "assets"."id" = "exif"."assetId" + "asset_exif" + inner join "asset" on "asset"."id" = "asset_exif"."assetId" where - "assets"."ownerId" = any ($1::uuid[]) - and "assets"."visibility" = $2 - and "assets"."type" = $3 - and "assets"."deletedAt" is null + "asset"."ownerId" = any ($1::uuid[]) + and "asset"."visibility" = $2 + and "asset"."type" = $3 + and "asset"."deletedAt" is null order by "city" limit @@ -189,14 +189,14 @@ with recursive "city", "assetId" from - "exif" - inner join "assets" on "assets"."id" = "exif"."assetId" + "asset_exif" + inner join "asset" on "asset"."id" = "asset_exif"."assetId" where - "assets"."ownerId" = any ($5::uuid[]) - and "assets"."visibility" = $6 - and "assets"."type" = $7 - and "assets"."deletedAt" is null - and "exif"."city" > "cte"."city" + "asset"."ownerId" = any ($5::uuid[]) + and "asset"."visibility" = $6 + and "asset"."type" = $7 + and "asset"."deletedAt" is null + and "asset_exif"."city" > "cte"."city" order by "city" limit @@ -205,21 +205,21 @@ with recursive ) ) select - "assets".*, - to_jsonb("exif") as "exifInfo" + "asset".*, + to_jsonb("asset_exif") as "exifInfo" from - "assets" - inner join "exif" on "assets"."id" = "exif"."assetId" - inner join "cte" on "assets"."id" = "cte"."assetId" + "asset" + inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" + inner join "cte" on "asset"."id" = "cte"."assetId" order by - "exif"."city" + "asset_exif"."city" -- SearchRepository.getStates select distinct on ("state") "state" from - "exif" - inner join "assets" on "assets"."id" = "exif"."assetId" + "asset_exif" + inner join "asset" on "asset"."id" = "asset_exif"."assetId" where "ownerId" = any ($1::uuid[]) and "visibility" = $2 @@ -230,8 +230,8 @@ where select distinct on ("city") "city" from - "exif" - inner join "assets" on "assets"."id" = "exif"."assetId" + "asset_exif" + inner join "asset" on "asset"."id" = "asset_exif"."assetId" where "ownerId" = any ($1::uuid[]) and "visibility" = $2 @@ -242,8 +242,8 @@ where select distinct on ("make") "make" from - "exif" - inner join "assets" on "assets"."id" = "exif"."assetId" + "asset_exif" + inner join "asset" on "asset"."id" = "asset_exif"."assetId" where "ownerId" = any ($1::uuid[]) and "visibility" = $2 @@ -254,8 +254,8 @@ where select distinct on ("model") "model" from - "exif" - inner join "assets" on "assets"."id" = "exif"."assetId" + "asset_exif" + inner join "asset" on "asset"."id" = "asset_exif"."assetId" where "ownerId" = any ($1::uuid[]) and "visibility" = $2 diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql index 6a9b69c2e3..24ffdcb5e1 100644 --- a/server/src/queries/session.repository.sql +++ b/server/src/queries/session.repository.sql @@ -6,68 +6,81 @@ select "expiresAt", "pinExpiresAt" from - "sessions" + "session" where "id" = $1 -- SessionRepository.getByToken select - "sessions"."id", - "sessions"."updatedAt", - "sessions"."pinExpiresAt", + "session"."id", + "session"."isPendingSyncReset", + "session"."updatedAt", + "session"."pinExpiresAt", ( select to_json(obj) from ( select - "users"."id", - "users"."name", - "users"."email", - "users"."isAdmin", - "users"."quotaUsageInBytes", - "users"."quotaSizeInBytes" + "user"."id", + "user"."name", + "user"."email", + "user"."isAdmin", + "user"."quotaUsageInBytes", + "user"."quotaSizeInBytes" from - "users" + "user" where - "users"."id" = "sessions"."userId" - and "users"."deletedAt" is null + "user"."id" = "session"."userId" + and "user"."deletedAt" is null ) as obj ) as "user" from - "sessions" + "session" where - "sessions"."token" = $1 + "session"."token" = $1 and ( - "sessions"."expiresAt" is null - or "sessions"."expiresAt" > $2 + "session"."expiresAt" is null + or "session"."expiresAt" > $2 ) -- SessionRepository.getByUserId select - "sessions".* + "session".* from - "sessions" - inner join "users" on "users"."id" = "sessions"."userId" - and "users"."deletedAt" is null + "session" + inner join "user" on "user"."id" = "session"."userId" + and "user"."deletedAt" is null where - "sessions"."userId" = $1 + "session"."userId" = $1 and ( - "sessions"."expiresAt" is null - or "sessions"."expiresAt" > $2 + "session"."expiresAt" is null + or "session"."expiresAt" > $2 ) order by - "sessions"."updatedAt" desc, - "sessions"."createdAt" desc + "session"."updatedAt" desc, + "session"."createdAt" desc -- SessionRepository.delete -delete from "sessions" +delete from "session" where "id" = $1::uuid -- SessionRepository.lockAll -update "sessions" +update "session" set "pinExpiresAt" = $1 where "userId" = $2 + +-- SessionRepository.resetSyncProgress +begin +update "session" +set + "isPendingSyncReset" = $1 +where + "id" = $2 +delete from "session_sync_checkpoint" +where + "sessionId" = $1 +commit diff --git a/server/src/queries/shared.link.repository.sql b/server/src/queries/shared.link.repository.sql index 1044c1c883..ed3507fa2f 100644 --- a/server/src/queries/shared.link.repository.sql +++ b/server/src/queries/shared.link.repository.sql @@ -2,7 +2,7 @@ -- SharedLinkRepository.get select - "shared_links".*, + "shared_link".*, coalesce( json_agg("a") filter ( where @@ -12,31 +12,31 @@ select ) as "assets", to_json("album") as "album" from - "shared_links" + "shared_link" left join lateral ( select - "assets".*, + "asset".*, to_json("exifInfo") as "exifInfo" from - "shared_link__asset" - inner join "assets" on "assets"."id" = "shared_link__asset"."assetsId" + "shared_link_asset" + inner join "asset" on "asset"."id" = "shared_link_asset"."assetsId" inner join lateral ( select - "exif".* + "asset_exif".* from - "exif" + "asset_exif" where - "exif"."assetId" = "assets"."id" + "asset_exif"."assetId" = "asset"."id" ) as "exifInfo" on true where - "shared_links"."id" = "shared_link__asset"."sharedLinksId" - and "assets"."deletedAt" is null + "shared_link"."id" = "shared_link_asset"."sharedLinksId" + and "asset"."deletedAt" is null order by - "assets"."fileCreatedAt" asc + "asset"."fileCreatedAt" asc ) as "a" on true left join lateral ( select - "albums".*, + "album".*, coalesce( json_agg("assets") filter ( where @@ -46,151 +46,151 @@ from ) as "assets", to_json("owner") as "owner" from - "albums" - left join "albums_assets_assets" on "albums_assets_assets"."albumsId" = "albums"."id" + "album" + left join "album_asset" on "album_asset"."albumsId" = "album"."id" left join lateral ( select - "assets".*, - to_json("assets_exifInfo") as "exifInfo" + "asset".*, + to_json("exifInfo") as "exifInfo" from - "assets" + "asset" inner join lateral ( select - "exif".* + "asset_exif".* from - "exif" + "asset_exif" where - "exif"."assetId" = "assets"."id" - ) as "assets_exifInfo" on true + "asset_exif"."assetId" = "asset"."id" + ) as "exifInfo" on true where - "albums_assets_assets"."assetsId" = "assets"."id" - and "assets"."deletedAt" is null + "album_asset"."assetsId" = "asset"."id" + and "asset"."deletedAt" is null order by - "assets"."fileCreatedAt" asc + "asset"."fileCreatedAt" asc ) as "assets" on true inner join lateral ( select - "users".* + "user".* from - "users" + "user" where - "users"."id" = "albums"."ownerId" - and "users"."deletedAt" is null + "user"."id" = "album"."ownerId" + and "user"."deletedAt" is null ) as "owner" on true where - "albums"."id" = "shared_links"."albumId" - and "albums"."deletedAt" is null + "album"."id" = "shared_link"."albumId" + and "album"."deletedAt" is null group by - "albums"."id", + "album"."id", "owner".* ) as "album" on true where - "shared_links"."id" = $1 - and "shared_links"."userId" = $2 + "shared_link"."id" = $1 + and "shared_link"."userId" = $2 and ( - "shared_links"."type" = $3 + "shared_link"."type" = $3 or "album"."id" is not null ) group by - "shared_links"."id", + "shared_link"."id", "album".* order by - "shared_links"."createdAt" desc + "shared_link"."createdAt" desc -- SharedLinkRepository.getAll select distinct - on ("shared_links"."createdAt") "shared_links".*, + on ("shared_link"."createdAt") "shared_link".*, "assets"."assets", to_json("album") as "album" from - "shared_links" - left join "shared_link__asset" on "shared_link__asset"."sharedLinksId" = "shared_links"."id" + "shared_link" + left join "shared_link_asset" on "shared_link_asset"."sharedLinksId" = "shared_link"."id" left join lateral ( select - json_agg("assets") as "assets" + json_agg("asset") as "assets" from - "assets" + "asset" where - "assets"."id" = "shared_link__asset"."assetsId" - and "assets"."deletedAt" is null + "asset"."id" = "shared_link_asset"."assetsId" + and "asset"."deletedAt" is null ) as "assets" on true left join lateral ( select - "albums".*, + "album".*, to_json("owner") as "owner" from - "albums" + "album" inner join lateral ( select - "users"."id", - "users"."email", - "users"."createdAt", - "users"."profileImagePath", - "users"."isAdmin", - "users"."shouldChangePassword", - "users"."deletedAt", - "users"."oauthId", - "users"."updatedAt", - "users"."storageLabel", - "users"."name", - "users"."quotaSizeInBytes", - "users"."quotaUsageInBytes", - "users"."status", - "users"."profileChangedAt" + "user"."id", + "user"."email", + "user"."createdAt", + "user"."profileImagePath", + "user"."isAdmin", + "user"."shouldChangePassword", + "user"."deletedAt", + "user"."oauthId", + "user"."updatedAt", + "user"."storageLabel", + "user"."name", + "user"."quotaSizeInBytes", + "user"."quotaUsageInBytes", + "user"."status", + "user"."profileChangedAt" from - "users" + "user" where - "users"."id" = "albums"."ownerId" - and "users"."deletedAt" is null + "user"."id" = "album"."ownerId" + and "user"."deletedAt" is null ) as "owner" on true where - "albums"."id" = "shared_links"."albumId" - and "albums"."deletedAt" is null + "album"."id" = "shared_link"."albumId" + and "album"."deletedAt" is null ) as "album" on true where - "shared_links"."userId" = $1 + "shared_link"."userId" = $1 and ( - "shared_links"."type" = $2 + "shared_link"."type" = $2 or "album"."id" is not null ) - and "shared_links"."albumId" = $3 + and "shared_link"."albumId" = $3 order by - "shared_links"."createdAt" desc + "shared_link"."createdAt" desc -- SharedLinkRepository.getByKey select - "shared_links"."id", - "shared_links"."userId", - "shared_links"."expiresAt", - "shared_links"."showExif", - "shared_links"."allowUpload", - "shared_links"."allowDownload", - "shared_links"."password", + "shared_link"."id", + "shared_link"."userId", + "shared_link"."expiresAt", + "shared_link"."showExif", + "shared_link"."allowUpload", + "shared_link"."allowDownload", + "shared_link"."password", ( select to_json(obj) from ( select - "users"."id", - "users"."name", - "users"."email", - "users"."isAdmin", - "users"."quotaUsageInBytes", - "users"."quotaSizeInBytes" + "user"."id", + "user"."name", + "user"."email", + "user"."isAdmin", + "user"."quotaUsageInBytes", + "user"."quotaSizeInBytes" from - "users" + "user" where - "users"."id" = "shared_links"."userId" + "user"."id" = "shared_link"."userId" ) as obj ) as "user" from - "shared_links" - left join "albums" on "albums"."id" = "shared_links"."albumId" + "shared_link" + left join "album" on "album"."id" = "shared_link"."albumId" where - "shared_links"."key" = $1 - and "albums"."deletedAt" is null + "shared_link"."key" = $1 + and "album"."deletedAt" is null and ( - "shared_links"."type" = $2 - or "albums"."id" is not null + "shared_link"."type" = $2 + or "album"."id" is not null ) diff --git a/server/src/queries/stack.repository.sql b/server/src/queries/stack.repository.sql index 6d450cd435..a256cdfc76 100644 --- a/server/src/queries/stack.repository.sql +++ b/server/src/queries/stack.repository.sql @@ -2,66 +2,66 @@ -- StackRepository.search select - "asset_stack".*, + "stack".*, ( select coalesce(json_agg(agg), '[]') from ( select - "assets".*, + "asset".*, to_json("exifInfo") as "exifInfo" from - "assets" + "asset" inner join lateral ( select - "exif"."assetId", - "exif"."autoStackId", - "exif"."bitsPerSample", - "exif"."city", - "exif"."colorspace", - "exif"."country", - "exif"."dateTimeOriginal", - "exif"."description", - "exif"."exifImageHeight", - "exif"."exifImageWidth", - "exif"."exposureTime", - "exif"."fileSizeInByte", - "exif"."fNumber", - "exif"."focalLength", - "exif"."fps", - "exif"."iso", - "exif"."latitude", - "exif"."lensModel", - "exif"."livePhotoCID", - "exif"."longitude", - "exif"."make", - "exif"."model", - "exif"."modifyDate", - "exif"."orientation", - "exif"."profileDescription", - "exif"."projectionType", - "exif"."rating", - "exif"."state", - "exif"."timeZone" + "asset_exif"."assetId", + "asset_exif"."autoStackId", + "asset_exif"."bitsPerSample", + "asset_exif"."city", + "asset_exif"."colorspace", + "asset_exif"."country", + "asset_exif"."dateTimeOriginal", + "asset_exif"."description", + "asset_exif"."exifImageHeight", + "asset_exif"."exifImageWidth", + "asset_exif"."exposureTime", + "asset_exif"."fileSizeInByte", + "asset_exif"."fNumber", + "asset_exif"."focalLength", + "asset_exif"."fps", + "asset_exif"."iso", + "asset_exif"."latitude", + "asset_exif"."lensModel", + "asset_exif"."livePhotoCID", + "asset_exif"."longitude", + "asset_exif"."make", + "asset_exif"."model", + "asset_exif"."modifyDate", + "asset_exif"."orientation", + "asset_exif"."profileDescription", + "asset_exif"."projectionType", + "asset_exif"."rating", + "asset_exif"."state", + "asset_exif"."timeZone" from - "exif" + "asset_exif" where - "exif"."assetId" = "assets"."id" + "asset_exif"."assetId" = "asset"."id" ) as "exifInfo" on true where - "assets"."deletedAt" is null - and "assets"."stackId" = "asset_stack"."id" - and "assets"."visibility" in ('archive', 'timeline') + "asset"."deletedAt" is null + and "asset"."stackId" = "stack"."id" + and "asset"."visibility" in ('archive', 'timeline') ) as agg ) as "assets" from - "asset_stack" + "stack" where - "asset_stack"."ownerId" = $1 + "stack"."ownerId" = $1 -- StackRepository.delete -delete from "asset_stack" +delete from "stack" where "id" = $1::uuid @@ -74,72 +74,72 @@ select from ( select - "assets".*, + "asset".*, ( select coalesce(json_agg(agg), '[]') from ( select - "tags"."id", - "tags"."value", - "tags"."createdAt", - "tags"."updatedAt", - "tags"."color", - "tags"."parentId" + "tag"."id", + "tag"."value", + "tag"."createdAt", + "tag"."updatedAt", + "tag"."color", + "tag"."parentId" from - "tags" - inner join "tag_asset" on "tags"."id" = "tag_asset"."tagsId" + "tag" + inner join "tag_asset" on "tag"."id" = "tag_asset"."tagsId" where - "tag_asset"."assetsId" = "assets"."id" + "tag_asset"."assetsId" = "asset"."id" ) as agg ) as "tags", to_json("exifInfo") as "exifInfo" from - "assets" + "asset" inner join lateral ( select - "exif"."assetId", - "exif"."autoStackId", - "exif"."bitsPerSample", - "exif"."city", - "exif"."colorspace", - "exif"."country", - "exif"."dateTimeOriginal", - "exif"."description", - "exif"."exifImageHeight", - "exif"."exifImageWidth", - "exif"."exposureTime", - "exif"."fileSizeInByte", - "exif"."fNumber", - "exif"."focalLength", - "exif"."fps", - "exif"."iso", - "exif"."latitude", - "exif"."lensModel", - "exif"."livePhotoCID", - "exif"."longitude", - "exif"."make", - "exif"."model", - "exif"."modifyDate", - "exif"."orientation", - "exif"."profileDescription", - "exif"."projectionType", - "exif"."rating", - "exif"."state", - "exif"."timeZone" + "asset_exif"."assetId", + "asset_exif"."autoStackId", + "asset_exif"."bitsPerSample", + "asset_exif"."city", + "asset_exif"."colorspace", + "asset_exif"."country", + "asset_exif"."dateTimeOriginal", + "asset_exif"."description", + "asset_exif"."exifImageHeight", + "asset_exif"."exifImageWidth", + "asset_exif"."exposureTime", + "asset_exif"."fileSizeInByte", + "asset_exif"."fNumber", + "asset_exif"."focalLength", + "asset_exif"."fps", + "asset_exif"."iso", + "asset_exif"."latitude", + "asset_exif"."lensModel", + "asset_exif"."livePhotoCID", + "asset_exif"."longitude", + "asset_exif"."make", + "asset_exif"."model", + "asset_exif"."modifyDate", + "asset_exif"."orientation", + "asset_exif"."profileDescription", + "asset_exif"."projectionType", + "asset_exif"."rating", + "asset_exif"."state", + "asset_exif"."timeZone" from - "exif" + "asset_exif" where - "exif"."assetId" = "assets"."id" + "asset_exif"."assetId" = "asset"."id" ) as "exifInfo" on true where - "assets"."deletedAt" is null - and "assets"."stackId" = "asset_stack"."id" - and "assets"."visibility" in ('archive', 'timeline') + "asset"."deletedAt" is null + and "asset"."stackId" = "stack"."id" + and "asset"."visibility" in ('archive', 'timeline') ) as agg ) as "assets" from - "asset_stack" + "stack" where "id" = $1::uuid diff --git a/server/src/queries/sync.checkpoint.repository.sql b/server/src/queries/sync.checkpoint.repository.sql index 40edf0f405..018054ff79 100644 --- a/server/src/queries/sync.checkpoint.repository.sql +++ b/server/src/queries/sync.checkpoint.repository.sql @@ -5,11 +5,11 @@ select "type", "ack" from - "session_sync_checkpoints" + "session_sync_checkpoint" where "sessionId" = $1 -- SyncCheckpointRepository.deleteAll -delete from "session_sync_checkpoints" +delete from "session_sync_checkpoint" where "sessionId" = $1 diff --git a/server/src/queries/sync.repository.sql b/server/src/queries/sync.repository.sql index 50a89655cb..4782eedf1d 100644 --- a/server/src/queries/sync.repository.sql +++ b/server/src/queries/sync.repository.sql @@ -5,7 +5,7 @@ select "albumsId" as "id", "createId" from - "albums_shared_users_users" + "album_user" where "usersId" = $1 and "createId" >= $2 @@ -18,7 +18,7 @@ select "id", "albumId" from - "albums_audit" + "album_audit" where "userId" = $1 and "deletedAt" < now() - interval '1 millisecond' @@ -27,165 +27,169 @@ order by -- SyncRepository.album.getUpserts select distinct - on ("albums"."id", "albums"."updateId") "albums"."id", - "albums"."ownerId", - "albums"."albumName" as "name", - "albums"."description", - "albums"."createdAt", - "albums"."updatedAt", - "albums"."albumThumbnailAssetId" as "thumbnailAssetId", - "albums"."isActivityEnabled", - "albums"."order", - "albums"."updateId" + on ("album"."id", "album"."updateId") "album"."id", + "album"."ownerId", + "album"."albumName" as "name", + "album"."description", + "album"."createdAt", + "album"."updatedAt", + "album"."albumThumbnailAssetId" as "thumbnailAssetId", + "album"."isActivityEnabled", + "album"."order", + "album"."updateId" from - "albums" - left join "albums_shared_users_users" as "album_users" on "albums"."id" = "album_users"."albumsId" + "album" + left join "album_user" as "album_users" on "album"."id" = "album_users"."albumsId" where - "albums"."updatedAt" < now() - interval '1 millisecond' + "album"."updatedAt" < now() - interval '1 millisecond' and ( - "albums"."ownerId" = $1 + "album"."ownerId" = $1 or "album_users"."usersId" = $2 ) order by - "albums"."updateId" asc + "album"."updateId" asc -- SyncRepository.albumAsset.getBackfill select - "assets"."id", - "assets"."ownerId", - "assets"."originalFileName", - "assets"."thumbhash", - "assets"."checksum", - "assets"."fileCreatedAt", - "assets"."fileModifiedAt", - "assets"."localDateTime", - "assets"."type", - "assets"."deletedAt", - "assets"."isFavorite", - "assets"."visibility", - "assets"."duration", - "assets"."updateId" + "asset"."id", + "asset"."ownerId", + "asset"."originalFileName", + "asset"."thumbhash", + "asset"."checksum", + "asset"."fileCreatedAt", + "asset"."fileModifiedAt", + "asset"."localDateTime", + "asset"."type", + "asset"."deletedAt", + "asset"."isFavorite", + "asset"."visibility", + "asset"."duration", + "asset"."livePhotoVideoId", + "asset"."stackId", + "asset"."updateId" from - "assets" - inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "assets"."id" + "asset" + inner join "album_asset" on "album_asset"."assetsId" = "asset"."id" where - "album_assets"."albumsId" = $1 - and "assets"."updatedAt" < now() - interval '1 millisecond' - and "assets"."updateId" <= $2 - and "assets"."updateId" >= $3 + "album_asset"."albumsId" = $1 + and "asset"."updatedAt" < now() - interval '1 millisecond' + and "asset"."updateId" <= $2 + and "asset"."updateId" >= $3 order by - "assets"."updateId" asc + "asset"."updateId" asc -- SyncRepository.albumAsset.getUpserts select - "assets"."id", - "assets"."ownerId", - "assets"."originalFileName", - "assets"."thumbhash", - "assets"."checksum", - "assets"."fileCreatedAt", - "assets"."fileModifiedAt", - "assets"."localDateTime", - "assets"."type", - "assets"."deletedAt", - "assets"."isFavorite", - "assets"."visibility", - "assets"."duration", - "assets"."updateId" + "asset"."id", + "asset"."ownerId", + "asset"."originalFileName", + "asset"."thumbhash", + "asset"."checksum", + "asset"."fileCreatedAt", + "asset"."fileModifiedAt", + "asset"."localDateTime", + "asset"."type", + "asset"."deletedAt", + "asset"."isFavorite", + "asset"."visibility", + "asset"."duration", + "asset"."livePhotoVideoId", + "asset"."stackId", + "asset"."updateId" from - "assets" - inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "assets"."id" - inner join "albums" on "albums"."id" = "album_assets"."albumsId" - left join "albums_shared_users_users" as "album_users" on "album_users"."albumsId" = "album_assets"."albumsId" + "asset" + inner join "album_asset" on "album_asset"."assetsId" = "asset"."id" + inner join "album" on "album"."id" = "album_asset"."albumsId" + left join "album_user" on "album_user"."albumsId" = "album_asset"."albumsId" where - "assets"."updatedAt" < now() - interval '1 millisecond' + "asset"."updatedAt" < now() - interval '1 millisecond' and ( - "albums"."ownerId" = $1 - or "album_users"."usersId" = $2 + "album"."ownerId" = $1 + or "album_user"."usersId" = $2 ) order by - "assets"."updateId" asc + "asset"."updateId" asc -- SyncRepository.albumAssetExif.getBackfill select - "exif"."assetId", - "exif"."description", - "exif"."exifImageWidth", - "exif"."exifImageHeight", - "exif"."fileSizeInByte", - "exif"."orientation", - "exif"."dateTimeOriginal", - "exif"."modifyDate", - "exif"."timeZone", - "exif"."latitude", - "exif"."longitude", - "exif"."projectionType", - "exif"."city", - "exif"."state", - "exif"."country", - "exif"."make", - "exif"."model", - "exif"."lensModel", - "exif"."fNumber", - "exif"."focalLength", - "exif"."iso", - "exif"."exposureTime", - "exif"."profileDescription", - "exif"."rating", - "exif"."fps", - "exif"."updateId" + "asset_exif"."assetId", + "asset_exif"."description", + "asset_exif"."exifImageWidth", + "asset_exif"."exifImageHeight", + "asset_exif"."fileSizeInByte", + "asset_exif"."orientation", + "asset_exif"."dateTimeOriginal", + "asset_exif"."modifyDate", + "asset_exif"."timeZone", + "asset_exif"."latitude", + "asset_exif"."longitude", + "asset_exif"."projectionType", + "asset_exif"."city", + "asset_exif"."state", + "asset_exif"."country", + "asset_exif"."make", + "asset_exif"."model", + "asset_exif"."lensModel", + "asset_exif"."fNumber", + "asset_exif"."focalLength", + "asset_exif"."iso", + "asset_exif"."exposureTime", + "asset_exif"."profileDescription", + "asset_exif"."rating", + "asset_exif"."fps", + "asset_exif"."updateId" from - "exif" - inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "exif"."assetId" + "asset_exif" + inner join "album_asset" on "album_asset"."assetsId" = "asset_exif"."assetId" where - "album_assets"."albumsId" = $1 - and "exif"."updatedAt" < now() - interval '1 millisecond' - and "exif"."updateId" <= $2 - and "exif"."updateId" >= $3 + "album_asset"."albumsId" = $1 + and "asset_exif"."updatedAt" < now() - interval '1 millisecond' + and "asset_exif"."updateId" <= $2 + and "asset_exif"."updateId" >= $3 order by - "exif"."updateId" asc + "asset_exif"."updateId" asc -- SyncRepository.albumAssetExif.getUpserts select - "exif"."assetId", - "exif"."description", - "exif"."exifImageWidth", - "exif"."exifImageHeight", - "exif"."fileSizeInByte", - "exif"."orientation", - "exif"."dateTimeOriginal", - "exif"."modifyDate", - "exif"."timeZone", - "exif"."latitude", - "exif"."longitude", - "exif"."projectionType", - "exif"."city", - "exif"."state", - "exif"."country", - "exif"."make", - "exif"."model", - "exif"."lensModel", - "exif"."fNumber", - "exif"."focalLength", - "exif"."iso", - "exif"."exposureTime", - "exif"."profileDescription", - "exif"."rating", - "exif"."fps", - "exif"."updateId" + "asset_exif"."assetId", + "asset_exif"."description", + "asset_exif"."exifImageWidth", + "asset_exif"."exifImageHeight", + "asset_exif"."fileSizeInByte", + "asset_exif"."orientation", + "asset_exif"."dateTimeOriginal", + "asset_exif"."modifyDate", + "asset_exif"."timeZone", + "asset_exif"."latitude", + "asset_exif"."longitude", + "asset_exif"."projectionType", + "asset_exif"."city", + "asset_exif"."state", + "asset_exif"."country", + "asset_exif"."make", + "asset_exif"."model", + "asset_exif"."lensModel", + "asset_exif"."fNumber", + "asset_exif"."focalLength", + "asset_exif"."iso", + "asset_exif"."exposureTime", + "asset_exif"."profileDescription", + "asset_exif"."rating", + "asset_exif"."fps", + "asset_exif"."updateId" from - "exif" - inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "exif"."assetId" - inner join "albums" on "albums"."id" = "album_assets"."albumsId" - left join "albums_shared_users_users" as "album_users" on "album_users"."albumsId" = "album_assets"."albumsId" + "asset_exif" + inner join "album_asset" on "album_asset"."assetsId" = "asset_exif"."assetId" + inner join "album" on "album"."id" = "album_asset"."albumsId" + left join "album_user" on "album_user"."albumsId" = "album_asset"."albumsId" where - "exif"."updatedAt" < now() - interval '1 millisecond' + "asset_exif"."updatedAt" < now() - interval '1 millisecond' and ( - "albums"."ownerId" = $1 - or "album_users"."usersId" = $2 + "album"."ownerId" = $1 + or "album_user"."usersId" = $2 ) order by - "exif"."updateId" asc + "asset_exif"."updateId" asc -- SyncRepository.albumToAsset.getBackfill select @@ -193,7 +197,7 @@ select "album_assets"."albumsId" as "albumId", "album_assets"."updateId" from - "albums_assets_assets" as "album_assets" + "album_asset" as "album_assets" where "album_assets"."albumsId" = $1 and "album_assets"."updatedAt" < now() - interval '1 millisecond' @@ -208,23 +212,23 @@ select "assetId", "albumId" from - "album_assets_audit" + "album_asset_audit" where "albumId" in ( select "id" from - "albums" + "album" where "ownerId" = $1 union ( select - "albumUsers"."albumsId" as "id" + "album_user"."albumsId" as "id" from - "albums_shared_users_users" as "albumUsers" + "album_user" where - "albumUsers"."usersId" = $2 + "album_user"."usersId" = $2 ) ) and "deletedAt" < now() - interval '1 millisecond' @@ -233,30 +237,30 @@ order by -- SyncRepository.albumToAsset.getUpserts select - "album_assets"."assetsId" as "assetId", - "album_assets"."albumsId" as "albumId", - "album_assets"."updateId" + "album_asset"."assetsId" as "assetId", + "album_asset"."albumsId" as "albumId", + "album_asset"."updateId" from - "albums_assets_assets" as "album_assets" - inner join "albums" on "albums"."id" = "album_assets"."albumsId" - left join "albums_shared_users_users" as "album_users" on "album_users"."albumsId" = "album_assets"."albumsId" + "album_asset" + inner join "album" on "album"."id" = "album_asset"."albumsId" + left join "album_user" on "album_user"."albumsId" = "album_asset"."albumsId" where - "album_assets"."updatedAt" < now() - interval '1 millisecond' + "album_asset"."updatedAt" < now() - interval '1 millisecond' and ( - "albums"."ownerId" = $1 - or "album_users"."usersId" = $2 + "album"."ownerId" = $1 + or "album_user"."usersId" = $2 ) order by - "album_assets"."updateId" asc + "album_asset"."updateId" asc -- SyncRepository.albumUser.getBackfill select - "album_users"."albumsId" as "albumId", - "album_users"."usersId" as "userId", - "album_users"."role", - "album_users"."updateId" + "album_user"."albumsId" as "albumId", + "album_user"."usersId" as "userId", + "album_user"."role", + "album_user"."updateId" from - "albums_shared_users_users" as "album_users" + "album_user" where "albumsId" = $1 and "updatedAt" < now() - interval '1 millisecond' @@ -271,23 +275,23 @@ select "userId", "albumId" from - "album_users_audit" + "album_user_audit" where "albumId" in ( select "id" from - "albums" + "album" where "ownerId" = $1 union ( select - "albumUsers"."albumsId" as "id" + "album_user"."albumsId" as "id" from - "albums_shared_users_users" as "albumUsers" + "album_user" where - "albumUsers"."usersId" = $2 + "album_user"."usersId" = $2 ) ) and "deletedAt" < now() - interval '1 millisecond' @@ -296,19 +300,19 @@ order by -- SyncRepository.albumUser.getUpserts select - "album_users"."albumsId" as "albumId", - "album_users"."usersId" as "userId", - "album_users"."role", - "album_users"."updateId" + "album_user"."albumsId" as "albumId", + "album_user"."usersId" as "userId", + "album_user"."role", + "album_user"."updateId" from - "albums_shared_users_users" as "album_users" + "album_user" where - "album_users"."updatedAt" < now() - interval '1 millisecond' - and "album_users"."albumsId" in ( + "album_user"."updatedAt" < now() - interval '1 millisecond' + and "album_user"."albumsId" in ( select "id" from - "albums" + "album" where "ownerId" = $1 union @@ -316,20 +320,20 @@ where select "albumUsers"."albumsId" as "id" from - "albums_shared_users_users" as "albumUsers" + "album_user" as "albumUsers" where "albumUsers"."usersId" = $2 ) ) order by - "album_users"."updateId" asc + "album_user"."updateId" asc -- SyncRepository.asset.getDeletes select "id", "assetId" from - "assets_audit" + "asset_audit" where "ownerId" = $1 and "deletedAt" < now() - interval '1 millisecond' @@ -338,22 +342,24 @@ order by -- SyncRepository.asset.getUpserts select - "assets"."id", - "assets"."ownerId", - "assets"."originalFileName", - "assets"."thumbhash", - "assets"."checksum", - "assets"."fileCreatedAt", - "assets"."fileModifiedAt", - "assets"."localDateTime", - "assets"."type", - "assets"."deletedAt", - "assets"."isFavorite", - "assets"."visibility", - "assets"."duration", - "assets"."updateId" + "asset"."id", + "asset"."ownerId", + "asset"."originalFileName", + "asset"."thumbhash", + "asset"."checksum", + "asset"."fileCreatedAt", + "asset"."fileModifiedAt", + "asset"."localDateTime", + "asset"."type", + "asset"."deletedAt", + "asset"."isFavorite", + "asset"."visibility", + "asset"."duration", + "asset"."livePhotoVideoId", + "asset"."stackId", + "asset"."updateId" from - "assets" + "asset" where "ownerId" = $1 and "updatedAt" < now() - interval '1 millisecond' @@ -362,40 +368,40 @@ order by -- SyncRepository.assetExif.getUpserts select - "exif"."assetId", - "exif"."description", - "exif"."exifImageWidth", - "exif"."exifImageHeight", - "exif"."fileSizeInByte", - "exif"."orientation", - "exif"."dateTimeOriginal", - "exif"."modifyDate", - "exif"."timeZone", - "exif"."latitude", - "exif"."longitude", - "exif"."projectionType", - "exif"."city", - "exif"."state", - "exif"."country", - "exif"."make", - "exif"."model", - "exif"."lensModel", - "exif"."fNumber", - "exif"."focalLength", - "exif"."iso", - "exif"."exposureTime", - "exif"."profileDescription", - "exif"."rating", - "exif"."fps", - "exif"."updateId" + "asset_exif"."assetId", + "asset_exif"."description", + "asset_exif"."exifImageWidth", + "asset_exif"."exifImageHeight", + "asset_exif"."fileSizeInByte", + "asset_exif"."orientation", + "asset_exif"."dateTimeOriginal", + "asset_exif"."modifyDate", + "asset_exif"."timeZone", + "asset_exif"."latitude", + "asset_exif"."longitude", + "asset_exif"."projectionType", + "asset_exif"."city", + "asset_exif"."state", + "asset_exif"."country", + "asset_exif"."make", + "asset_exif"."model", + "asset_exif"."lensModel", + "asset_exif"."fNumber", + "asset_exif"."focalLength", + "asset_exif"."iso", + "asset_exif"."exposureTime", + "asset_exif"."profileDescription", + "asset_exif"."rating", + "asset_exif"."fps", + "asset_exif"."updateId" from - "exif" + "asset_exif" where "assetId" in ( select "id" from - "assets" + "asset" where "ownerId" = $1 ) @@ -408,7 +414,7 @@ select "id", "memoryId" from - "memories_audit" + "memory_audit" where "userId" = $1 and "deletedAt" < now() - interval '1 millisecond' @@ -431,7 +437,7 @@ select "hideAt", "updateId" from - "memories" + "memory" where "ownerId" = $1 and "updatedAt" < now() - interval '1 millisecond' @@ -444,13 +450,13 @@ select "memoryId", "assetId" from - "memory_assets_audit" + "memory_asset_audit" where "memoryId" in ( select "id" from - "memories" + "memory" where "ownerId" = $1 ) @@ -464,13 +470,13 @@ select "assetsId" as "assetId", "updateId" from - "memories_assets_assets" + "memory_asset" where "memoriesId" in ( select "id" from - "memories" + "memory" where "ownerId" = $1 ) @@ -483,13 +489,13 @@ select "sharedById", "createId" from - "partners" + "partner" where "sharedWithId" = $1 and "createId" >= $2 and "createdAt" < now() - interval '1 millisecond' order by - "partners"."createId" asc + "partner"."createId" asc -- SyncRepository.partner.getDeletes select @@ -497,7 +503,7 @@ select "sharedById", "sharedWithId" from - "partners_audit" + "partner_audit" where ( "sharedById" = $1 @@ -514,7 +520,7 @@ select "inTimeline", "updateId" from - "partners" + "partner" where ( "sharedById" = $1 @@ -526,22 +532,24 @@ order by -- SyncRepository.partnerAsset.getBackfill select - "assets"."id", - "assets"."ownerId", - "assets"."originalFileName", - "assets"."thumbhash", - "assets"."checksum", - "assets"."fileCreatedAt", - "assets"."fileModifiedAt", - "assets"."localDateTime", - "assets"."type", - "assets"."deletedAt", - "assets"."isFavorite", - "assets"."visibility", - "assets"."duration", - "assets"."updateId" + "asset"."id", + "asset"."ownerId", + "asset"."originalFileName", + "asset"."thumbhash", + "asset"."checksum", + "asset"."fileCreatedAt", + "asset"."fileModifiedAt", + "asset"."localDateTime", + "asset"."type", + "asset"."deletedAt", + "asset"."isFavorite", + "asset"."visibility", + "asset"."duration", + "asset"."livePhotoVideoId", + "asset"."stackId", + "asset"."updateId" from - "assets" + "asset" where "ownerId" = $1 and "updatedAt" < now() - interval '1 millisecond' @@ -555,13 +563,13 @@ select "id", "assetId" from - "assets_audit" + "asset_audit" where "ownerId" in ( select "sharedById" from - "partners" + "partner" where "sharedWithId" = $1 ) @@ -571,28 +579,30 @@ order by -- SyncRepository.partnerAsset.getUpserts select - "assets"."id", - "assets"."ownerId", - "assets"."originalFileName", - "assets"."thumbhash", - "assets"."checksum", - "assets"."fileCreatedAt", - "assets"."fileModifiedAt", - "assets"."localDateTime", - "assets"."type", - "assets"."deletedAt", - "assets"."isFavorite", - "assets"."visibility", - "assets"."duration", - "assets"."updateId" + "asset"."id", + "asset"."ownerId", + "asset"."originalFileName", + "asset"."thumbhash", + "asset"."checksum", + "asset"."fileCreatedAt", + "asset"."fileModifiedAt", + "asset"."localDateTime", + "asset"."type", + "asset"."deletedAt", + "asset"."isFavorite", + "asset"."visibility", + "asset"."duration", + "asset"."livePhotoVideoId", + "asset"."stackId", + "asset"."updateId" from - "assets" + "asset" where "ownerId" in ( select "sharedById" from - "partners" + "partner" where "sharedWithId" = $1 ) @@ -602,85 +612,85 @@ order by -- SyncRepository.partnerAssetExif.getBackfill select - "exif"."assetId", - "exif"."description", - "exif"."exifImageWidth", - "exif"."exifImageHeight", - "exif"."fileSizeInByte", - "exif"."orientation", - "exif"."dateTimeOriginal", - "exif"."modifyDate", - "exif"."timeZone", - "exif"."latitude", - "exif"."longitude", - "exif"."projectionType", - "exif"."city", - "exif"."state", - "exif"."country", - "exif"."make", - "exif"."model", - "exif"."lensModel", - "exif"."fNumber", - "exif"."focalLength", - "exif"."iso", - "exif"."exposureTime", - "exif"."profileDescription", - "exif"."rating", - "exif"."fps", - "exif"."updateId" + "asset_exif"."assetId", + "asset_exif"."description", + "asset_exif"."exifImageWidth", + "asset_exif"."exifImageHeight", + "asset_exif"."fileSizeInByte", + "asset_exif"."orientation", + "asset_exif"."dateTimeOriginal", + "asset_exif"."modifyDate", + "asset_exif"."timeZone", + "asset_exif"."latitude", + "asset_exif"."longitude", + "asset_exif"."projectionType", + "asset_exif"."city", + "asset_exif"."state", + "asset_exif"."country", + "asset_exif"."make", + "asset_exif"."model", + "asset_exif"."lensModel", + "asset_exif"."fNumber", + "asset_exif"."focalLength", + "asset_exif"."iso", + "asset_exif"."exposureTime", + "asset_exif"."profileDescription", + "asset_exif"."rating", + "asset_exif"."fps", + "asset_exif"."updateId" from - "exif" - inner join "assets" on "assets"."id" = "exif"."assetId" + "asset_exif" + inner join "asset" on "asset"."id" = "asset_exif"."assetId" where - "assets"."ownerId" = $1 - and "exif"."updatedAt" < now() - interval '1 millisecond' - and "exif"."updateId" <= $2 - and "exif"."updateId" >= $3 + "asset"."ownerId" = $1 + and "asset_exif"."updatedAt" < now() - interval '1 millisecond' + and "asset_exif"."updateId" <= $2 + and "asset_exif"."updateId" >= $3 order by - "exif"."updateId" asc + "asset_exif"."updateId" asc -- SyncRepository.partnerAssetExif.getUpserts select - "exif"."assetId", - "exif"."description", - "exif"."exifImageWidth", - "exif"."exifImageHeight", - "exif"."fileSizeInByte", - "exif"."orientation", - "exif"."dateTimeOriginal", - "exif"."modifyDate", - "exif"."timeZone", - "exif"."latitude", - "exif"."longitude", - "exif"."projectionType", - "exif"."city", - "exif"."state", - "exif"."country", - "exif"."make", - "exif"."model", - "exif"."lensModel", - "exif"."fNumber", - "exif"."focalLength", - "exif"."iso", - "exif"."exposureTime", - "exif"."profileDescription", - "exif"."rating", - "exif"."fps", - "exif"."updateId" + "asset_exif"."assetId", + "asset_exif"."description", + "asset_exif"."exifImageWidth", + "asset_exif"."exifImageHeight", + "asset_exif"."fileSizeInByte", + "asset_exif"."orientation", + "asset_exif"."dateTimeOriginal", + "asset_exif"."modifyDate", + "asset_exif"."timeZone", + "asset_exif"."latitude", + "asset_exif"."longitude", + "asset_exif"."projectionType", + "asset_exif"."city", + "asset_exif"."state", + "asset_exif"."country", + "asset_exif"."make", + "asset_exif"."model", + "asset_exif"."lensModel", + "asset_exif"."fNumber", + "asset_exif"."focalLength", + "asset_exif"."iso", + "asset_exif"."exposureTime", + "asset_exif"."profileDescription", + "asset_exif"."rating", + "asset_exif"."fps", + "asset_exif"."updateId" from - "exif" + "asset_exif" where "assetId" in ( select "id" from - "assets" + "asset" where "ownerId" in ( select "sharedById" from - "partners" + "partner" where "sharedWithId" = $1 ) @@ -694,13 +704,13 @@ select "id", "stackId" from - "stacks_audit" + "stack_audit" where "userId" in ( select "sharedById" from - "partners" + "partner" where "sharedWithId" = $1 ) @@ -710,14 +720,14 @@ order by -- SyncRepository.partnerStack.getBackfill select - "asset_stack"."id", - "asset_stack"."createdAt", - "asset_stack"."updatedAt", - "asset_stack"."primaryAssetId", - "asset_stack"."ownerId", + "stack"."id", + "stack"."createdAt", + "stack"."updatedAt", + "stack"."primaryAssetId", + "stack"."ownerId", "updateId" from - "asset_stack" + "stack" where "ownerId" = $1 and "updatedAt" < now() - interval '1 millisecond' @@ -728,20 +738,20 @@ order by -- SyncRepository.partnerStack.getUpserts select - "asset_stack"."id", - "asset_stack"."createdAt", - "asset_stack"."updatedAt", - "asset_stack"."primaryAssetId", - "asset_stack"."ownerId", + "stack"."id", + "stack"."createdAt", + "stack"."updatedAt", + "stack"."primaryAssetId", + "stack"."ownerId", "updateId" from - "asset_stack" + "stack" where "ownerId" in ( select "sharedById" from - "partners" + "partner" where "sharedWithId" = $1 ) @@ -749,12 +759,46 @@ where order by "updateId" asc +-- SyncRepository.people.getDeletes +select + "id", + "personId" +from + "person_audit" +where + "ownerId" = $1 + and "deletedAt" < now() - interval '1 millisecond' +order by + "id" asc + +-- SyncRepository.people.getUpserts +select + "id", + "createdAt", + "updatedAt", + "ownerId", + "name", + "birthDate", + "thumbnailPath", + "isHidden", + "isFavorite", + "color", + "updateId", + "faceAssetId" +from + "person" +where + "ownerId" = $1 + and "updatedAt" < now() - interval '1 millisecond' +order by + "updateId" asc + -- SyncRepository.stack.getDeletes select "id", "stackId" from - "stacks_audit" + "stack_audit" where "userId" = $1 and "deletedAt" < now() - interval '1 millisecond' @@ -763,14 +807,14 @@ order by -- SyncRepository.stack.getUpserts select - "asset_stack"."id", - "asset_stack"."createdAt", - "asset_stack"."updatedAt", - "asset_stack"."primaryAssetId", - "asset_stack"."ownerId", + "stack"."id", + "stack"."createdAt", + "stack"."updatedAt", + "stack"."primaryAssetId", + "stack"."ownerId", "updateId" from - "asset_stack" + "stack" where "ownerId" = $1 and "updatedAt" < now() - interval '1 millisecond' @@ -782,7 +826,7 @@ select "id", "userId" from - "users_audit" + "user_audit" where "deletedAt" < now() - interval '1 millisecond' order by @@ -796,8 +840,35 @@ select "deletedAt", "updateId" from - "users" + "user" where "updatedAt" < now() - interval '1 millisecond' order by "updateId" asc + +-- SyncRepository.userMetadata.getDeletes +select + "id", + "userId", + "key" +from + "user_metadata_audit" +where + "userId" = $1 + and "deletedAt" < now() - interval '1 millisecond' +order by + "id" asc + +-- SyncRepository.userMetadata.getUpserts +select + "userId", + "key", + "value", + "updateId" +from + "user_metadata" +where + "userId" = $1 + and "updatedAt" < now() - interval '1 millisecond' +order by + "updateId" asc diff --git a/server/src/queries/tag.repository.sql b/server/src/queries/tag.repository.sql index af757d96b7..ee961f3801 100644 --- a/server/src/queries/tag.repository.sql +++ b/server/src/queries/tag.repository.sql @@ -2,27 +2,27 @@ -- TagRepository.get select - "tags"."id", - "tags"."value", - "tags"."createdAt", - "tags"."updatedAt", - "tags"."color", - "tags"."parentId" + "tag"."id", + "tag"."value", + "tag"."createdAt", + "tag"."updatedAt", + "tag"."color", + "tag"."parentId" from - "tags" + "tag" where "id" = $1 -- TagRepository.getByValue select - "tags"."id", - "tags"."value", - "tags"."createdAt", - "tags"."updatedAt", - "tags"."color", - "tags"."parentId" + "tag"."id", + "tag"."value", + "tag"."createdAt", + "tag"."updatedAt", + "tag"."color", + "tag"."parentId" from - "tags" + "tag" where "userId" = $1 and "value" = $2 @@ -30,31 +30,31 @@ where -- TagRepository.upsertValue begin insert into - "tags" ("userId", "value", "parentId") + "tag" ("userId", "value", "parentId") values ($1, $2, $3) on conflict ("userId", "value") do update set "parentId" = $4 returning - "tags"."id", - "tags"."value", - "tags"."createdAt", - "tags"."updatedAt", - "tags"."color", - "tags"."parentId" + "tag"."id", + "tag"."value", + "tag"."createdAt", + "tag"."updatedAt", + "tag"."color", + "tag"."parentId" rollback -- TagRepository.getAll select - "tags"."id", - "tags"."value", - "tags"."createdAt", - "tags"."updatedAt", - "tags"."color", - "tags"."parentId" + "tag"."id", + "tag"."value", + "tag"."createdAt", + "tag"."updatedAt", + "tag"."color", + "tag"."parentId" from - "tags" + "tag" where "userId" = $1 order by @@ -62,14 +62,14 @@ order by -- TagRepository.create insert into - "tags" ("userId", "color", "value") + "tag" ("userId", "color", "value") values ($1, $2, $3) returning * -- TagRepository.update -update "tags" +update "tag" set "color" = $1 where @@ -78,7 +78,7 @@ returning * -- TagRepository.delete -delete from "tags" +delete from "tag" where "id" = $1 diff --git a/server/src/queries/trash.repository.sql b/server/src/queries/trash.repository.sql index 77c2ea51d0..57b2f32e37 100644 --- a/server/src/queries/trash.repository.sql +++ b/server/src/queries/trash.repository.sql @@ -1,7 +1,7 @@ -- NOTE: This file is auto generated by ./sql-generator -- TrashRepository.restore -update "assets" +update "asset" set "status" = $1, "deletedAt" = $2 @@ -10,7 +10,7 @@ where and "status" = $4 -- TrashRepository.empty -update "assets" +update "asset" set "status" = $1 where @@ -18,7 +18,7 @@ where and "status" = $3 -- TrashRepository.restoreAll -update "assets" +update "asset" set "status" = $1, "deletedAt" = $2 diff --git a/server/src/queries/user.repository.sql b/server/src/queries/user.repository.sql index 88f94c85ac..f1809464bf 100644 --- a/server/src/queries/user.repository.sql +++ b/server/src/queries/user.repository.sql @@ -30,14 +30,14 @@ select from "user_metadata" where - "users"."id" = "user_metadata"."userId" + "user"."id" = "user_metadata"."userId" ) as agg ) as "metadata" from - "users" + "user" where - "users"."id" = $1 - and "users"."deletedAt" is null + "user"."id" = $1 + and "user"."deletedAt" is null -- UserRepository.getAdmin select @@ -69,43 +69,43 @@ select from "user_metadata" where - "users"."id" = "user_metadata"."userId" + "user"."id" = "user_metadata"."userId" ) as agg ) as "metadata" from - "users" + "user" where - "users"."isAdmin" = $1 - and "users"."deletedAt" is null + "user"."isAdmin" = $1 + and "user"."deletedAt" is null -- UserRepository.hasAdmin select - "users"."id" + "user"."id" from - "users" + "user" where - "users"."isAdmin" = $1 - and "users"."deletedAt" is null + "user"."isAdmin" = $1 + and "user"."deletedAt" is null -- UserRepository.getForPinCode select - "users"."pinCode", - "users"."password" + "user"."pinCode", + "user"."password" from - "users" + "user" where - "users"."id" = $1 - and "users"."deletedAt" is null + "user"."id" = $1 + and "user"."deletedAt" is null -- UserRepository.getForChangePassword select - "users"."id", - "users"."password" + "user"."id", + "user"."password" from - "users" + "user" where - "users"."id" = $1 - and "users"."deletedAt" is null + "user"."id" = $1 + and "user"."deletedAt" is null -- UserRepository.getByEmail select @@ -137,14 +137,14 @@ select from "user_metadata" where - "users"."id" = "user_metadata"."userId" + "user"."id" = "user_metadata"."userId" ) as agg ) as "metadata" from - "users" + "user" where "email" = $1 - and "users"."deletedAt" is null + and "user"."deletedAt" is null -- UserRepository.getByStorageLabel select @@ -166,10 +166,10 @@ select "quotaSizeInBytes", "quotaUsageInBytes" from - "users" + "user" where - "users"."storageLabel" = $1 - and "users"."deletedAt" is null + "user"."storageLabel" = $1 + and "user"."deletedAt" is null -- UserRepository.getByOAuthId select @@ -201,22 +201,22 @@ select from "user_metadata" where - "users"."id" = "user_metadata"."userId" + "user"."id" = "user_metadata"."userId" ) as agg ) as "metadata" from - "users" + "user" where - "users"."oauthId" = $1 - and "users"."deletedAt" is null + "user"."oauthId" = $1 + and "user"."deletedAt" is null -- UserRepository.getDeletedAfter select "id" from - "users" + "user" where - "users"."deletedAt" < $1 + "user"."deletedAt" < $1 -- UserRepository.getList (with deleted) select @@ -248,11 +248,11 @@ select from "user_metadata" where - "users"."id" = "user_metadata"."userId" + "user"."id" = "user_metadata"."userId" ) as agg ) as "metadata" from - "users" + "user" order by "createdAt" desc @@ -286,95 +286,95 @@ select from "user_metadata" where - "users"."id" = "user_metadata"."userId" + "user"."id" = "user_metadata"."userId" ) as agg ) as "metadata" from - "users" + "user" where - "users"."deletedAt" is null + "user"."deletedAt" is null order by "createdAt" desc -- UserRepository.getUserStats select - "users"."id" as "userId", - "users"."name" as "userName", - "users"."quotaSizeInBytes", + "user"."id" as "userId", + "user"."name" as "userName", + "user"."quotaSizeInBytes", count(*) filter ( where ( - "assets"."type" = 'IMAGE' - and "assets"."visibility" != 'hidden' + "asset"."type" = 'IMAGE' + and "asset"."visibility" != 'hidden' ) ) as "photos", count(*) filter ( where ( - "assets"."type" = 'VIDEO' - and "assets"."visibility" != 'hidden' + "asset"."type" = 'VIDEO' + and "asset"."visibility" != 'hidden' ) ) as "videos", coalesce( - sum("exif"."fileSizeInByte") filter ( + sum("asset_exif"."fileSizeInByte") filter ( where - "assets"."libraryId" is null + "asset"."libraryId" is null ), 0 ) as "usage", coalesce( - sum("exif"."fileSizeInByte") filter ( + sum("asset_exif"."fileSizeInByte") filter ( where ( - "assets"."libraryId" is null - and "assets"."type" = 'IMAGE' + "asset"."libraryId" is null + and "asset"."type" = 'IMAGE' ) ), 0 ) as "usagePhotos", coalesce( - sum("exif"."fileSizeInByte") filter ( + sum("asset_exif"."fileSizeInByte") filter ( where ( - "assets"."libraryId" is null - and "assets"."type" = 'VIDEO' + "asset"."libraryId" is null + and "asset"."type" = 'VIDEO' ) ), 0 ) as "usageVideos" from - "users" - left join "assets" on "assets"."ownerId" = "users"."id" - and "assets"."deletedAt" is null - left join "exif" on "exif"."assetId" = "assets"."id" + "user" + left join "asset" on "asset"."ownerId" = "user"."id" + and "asset"."deletedAt" is null + left join "asset_exif" on "asset_exif"."assetId" = "asset"."id" group by - "users"."id" + "user"."id" order by - "users"."createdAt" asc + "user"."createdAt" asc -- UserRepository.updateUsage -update "users" +update "user" set "quotaUsageInBytes" = "quotaUsageInBytes" + $1, "updatedAt" = $2 where "id" = $3::uuid - and "users"."deletedAt" is null + and "user"."deletedAt" is null -- UserRepository.syncUsage -update "users" +update "user" set "quotaUsageInBytes" = ( select - coalesce(sum("exif"."fileSizeInByte"), 0) as "usage" + coalesce(sum("asset_exif"."fileSizeInByte"), 0) as "usage" from - "assets" - left join "exif" on "exif"."assetId" = "assets"."id" + "asset" + left join "asset_exif" on "asset_exif"."assetId" = "asset"."id" where - "assets"."libraryId" is null - and "assets"."ownerId" = "users"."id" + "asset"."libraryId" is null + and "asset"."ownerId" = "user"."id" ), "updatedAt" = $1 where - "users"."deletedAt" is null - and "users"."id" = $2::uuid + "user"."deletedAt" is null + and "user"."id" = $2::uuid diff --git a/server/src/queries/view.repository.sql b/server/src/queries/view.repository.sql index a2260ce5f6..81f5ca20b8 100644 --- a/server/src/queries/view.repository.sql +++ b/server/src/queries/view.repository.sql @@ -2,9 +2,9 @@ -- ViewRepository.getUniqueOriginalPaths select distinct - substring("assets"."originalPath", $1) as "directoryPath" + substring("asset"."originalPath", $1) as "directoryPath" from - "assets" + "asset" where "ownerId" = $2::uuid and "visibility" = $3 @@ -15,11 +15,11 @@ where -- ViewRepository.getAssetsByOriginalPath select - "assets".*, - to_json("exif") as "exifInfo" + "asset".*, + to_json("asset_exif") as "exifInfo" from - "assets" - left join "exif" on "assets"."id" = "exif"."assetId" + "asset" + left join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where "ownerId" = $1::uuid and "visibility" = $2 @@ -30,4 +30,4 @@ where and "originalPath" like $3 and "originalPath" not like $4 order by - regexp_replace("assets"."originalPath", $5, $6) asc + regexp_replace("asset"."originalPath", $5, $6) asc diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index 8863d117bc..14a765778e 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -35,9 +35,9 @@ class ActivityAccess { return this.db .selectFrom('activity') .select('activity.id') - .leftJoin('albums', (join) => join.onRef('activity.albumId', '=', 'albums.id').on('albums.deletedAt', 'is', null)) + .leftJoin('album', (join) => join.onRef('activity.albumId', '=', 'album.id').on('album.deletedAt', 'is', null)) .where('activity.id', 'in', [...activityIds]) - .whereRef('albums.ownerId', '=', asUuid(userId)) + .whereRef('album.ownerId', '=', asUuid(userId)) .execute() .then((activities) => new Set(activities.map((activity) => activity.id))); } @@ -50,14 +50,14 @@ class ActivityAccess { } return this.db - .selectFrom('albums') - .select('albums.id') - .leftJoin('albums_shared_users_users as albumUsers', 'albumUsers.albumsId', 'albums.id') - .leftJoin('users', (join) => join.onRef('users.id', '=', 'albumUsers.usersId').on('users.deletedAt', 'is', null)) - .where('albums.id', 'in', [...albumIds]) - .where('albums.isActivityEnabled', '=', true) - .where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('users.id', '=', userId)])) - .where('albums.deletedAt', 'is', null) + .selectFrom('album') + .select('album.id') + .leftJoin('album_user as albumUsers', 'albumUsers.albumsId', 'album.id') + .leftJoin('user', (join) => join.onRef('user.id', '=', 'albumUsers.usersId').on('user.deletedAt', 'is', null)) + .where('album.id', 'in', [...albumIds]) + .where('album.isActivityEnabled', '=', true) + .where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('user.id', '=', userId)])) + .where('album.deletedAt', 'is', null) .execute() .then((albums) => new Set(albums.map((album) => album.id))); } @@ -74,11 +74,11 @@ class AlbumAccess { } return this.db - .selectFrom('albums') - .select('albums.id') - .where('albums.id', 'in', [...albumIds]) - .where('albums.ownerId', '=', userId) - .where('albums.deletedAt', 'is', null) + .selectFrom('album') + .select('album.id') + .where('album.id', 'in', [...albumIds]) + .where('album.ownerId', '=', userId) + .where('album.deletedAt', 'is', null) .execute() .then((albums) => new Set(albums.map((album) => album.id))); } @@ -94,14 +94,14 @@ class AlbumAccess { access === AlbumUserRole.EDITOR ? [AlbumUserRole.EDITOR] : [AlbumUserRole.EDITOR, AlbumUserRole.VIEWER]; return this.db - .selectFrom('albums') - .select('albums.id') - .leftJoin('albums_shared_users_users as albumUsers', 'albumUsers.albumsId', 'albums.id') - .leftJoin('users', (join) => join.onRef('users.id', '=', 'albumUsers.usersId').on('users.deletedAt', 'is', null)) - .where('albums.id', 'in', [...albumIds]) - .where('albums.deletedAt', 'is', null) - .where('users.id', '=', userId) - .where('albumUsers.role', 'in', [...accessRole]) + .selectFrom('album') + .select('album.id') + .leftJoin('album_user', 'album_user.albumsId', 'album.id') + .leftJoin('user', (join) => join.onRef('user.id', '=', 'album_user.usersId').on('user.deletedAt', 'is', null)) + .where('album.id', 'in', [...albumIds]) + .where('album.deletedAt', 'is', null) + .where('user.id', '=', userId) + .where('album_user.role', 'in', [...accessRole]) .execute() .then((albums) => new Set(albums.map((album) => album.id))); } @@ -114,10 +114,10 @@ class AlbumAccess { } return this.db - .selectFrom('shared_links') - .select('shared_links.albumId') - .where('shared_links.id', '=', sharedLinkId) - .where('shared_links.albumId', 'in', [...albumIds]) + .selectFrom('shared_link') + .select('shared_link.albumId') + .where('shared_link.id', '=', sharedLinkId) + .where('shared_link.albumId', 'in', [...albumIds]) .execute() .then( (sharedLinks) => new Set(sharedLinks.flatMap((sharedLink) => (sharedLink.albumId ? [sharedLink.albumId] : []))), @@ -136,21 +136,21 @@ class AssetAccess { } return this.db - .selectFrom('albums') - .innerJoin('albums_assets_assets as albumAssets', 'albums.id', 'albumAssets.albumsId') - .innerJoin('assets', (join) => - join.onRef('assets.id', '=', 'albumAssets.assetsId').on('assets.deletedAt', 'is', null), + .selectFrom('album') + .innerJoin('album_asset as albumAssets', 'album.id', 'albumAssets.albumsId') + .innerJoin('asset', (join) => + join.onRef('asset.id', '=', 'albumAssets.assetsId').on('asset.deletedAt', 'is', null), ) - .leftJoin('albums_shared_users_users as albumUsers', 'albumUsers.albumsId', 'albums.id') - .leftJoin('users', (join) => join.onRef('users.id', '=', 'albumUsers.usersId').on('users.deletedAt', 'is', null)) - .select(['assets.id', 'assets.livePhotoVideoId']) + .leftJoin('album_user as albumUsers', 'albumUsers.albumsId', 'album.id') + .leftJoin('user', (join) => join.onRef('user.id', '=', 'albumUsers.usersId').on('user.deletedAt', 'is', null)) + .select(['asset.id', 'asset.livePhotoVideoId']) .where( - sql`array["assets"."id", "assets"."livePhotoVideoId"]`, + sql`array["asset"."id", "asset"."livePhotoVideoId"]`, '&&', sql`array[${sql.join([...assetIds])}]::uuid[] `, ) - .where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('users.id', '=', userId)])) - .where('albums.deletedAt', 'is', null) + .where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('user.id', '=', userId)])) + .where('album.deletedAt', 'is', null) .execute() .then((assets) => { const allowedIds = new Set(); @@ -174,11 +174,11 @@ class AssetAccess { } return this.db - .selectFrom('assets') - .select('assets.id') - .where('assets.id', 'in', [...assetIds]) - .where('assets.ownerId', '=', userId) - .$if(!hasElevatedPermission, (eb) => eb.where('assets.visibility', '!=', AssetVisibility.LOCKED)) + .selectFrom('asset') + .select('asset.id') + .where('asset.id', 'in', [...assetIds]) + .where('asset.ownerId', '=', userId) + .$if(!hasElevatedPermission, (eb) => eb.where('asset.visibility', '!=', AssetVisibility.LOCKED)) .execute() .then((assets) => new Set(assets.map((asset) => asset.id))); } @@ -191,23 +191,21 @@ class AssetAccess { } return this.db - .selectFrom('partners as partner') - .innerJoin('users as sharedBy', (join) => + .selectFrom('partner') + .innerJoin('user as sharedBy', (join) => join.onRef('sharedBy.id', '=', 'partner.sharedById').on('sharedBy.deletedAt', 'is', null), ) - .innerJoin('assets', (join) => - join.onRef('assets.ownerId', '=', 'sharedBy.id').on('assets.deletedAt', 'is', null), - ) - .select('assets.id') + .innerJoin('asset', (join) => join.onRef('asset.ownerId', '=', 'sharedBy.id').on('asset.deletedAt', 'is', null)) + .select('asset.id') .where('partner.sharedWithId', '=', userId) .where((eb) => eb.or([ - eb('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)), - eb('assets.visibility', '=', sql.lit(AssetVisibility.HIDDEN)), + eb('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)), + eb('asset.visibility', '=', sql.lit(AssetVisibility.HIDDEN)), ]), ) - .where('assets.id', 'in', [...assetIds]) + .where('asset.id', 'in', [...assetIds]) .execute() .then((assets) => new Set(assets.map((asset) => asset.id))); } @@ -220,27 +218,25 @@ class AssetAccess { } return this.db - .selectFrom('shared_links') - .leftJoin('albums', (join) => - join.onRef('albums.id', '=', 'shared_links.albumId').on('albums.deletedAt', 'is', null), + .selectFrom('shared_link') + .leftJoin('album', (join) => join.onRef('album.id', '=', 'shared_link.albumId').on('album.deletedAt', 'is', null)) + .leftJoin('shared_link_asset', 'shared_link_asset.sharedLinksId', 'shared_link.id') + .leftJoin('asset', (join) => + join.onRef('asset.id', '=', 'shared_link_asset.assetsId').on('asset.deletedAt', 'is', null), ) - .leftJoin('shared_link__asset', 'shared_link__asset.sharedLinksId', 'shared_links.id') - .leftJoin('assets', (join) => - join.onRef('assets.id', '=', 'shared_link__asset.assetsId').on('assets.deletedAt', 'is', null), - ) - .leftJoin('albums_assets_assets', 'albums_assets_assets.albumsId', 'albums.id') - .leftJoin('assets as albumAssets', (join) => - join.onRef('albumAssets.id', '=', 'albums_assets_assets.assetsId').on('albumAssets.deletedAt', 'is', null), + .leftJoin('album_asset', 'album_asset.albumsId', 'album.id') + .leftJoin('asset as albumAssets', (join) => + join.onRef('albumAssets.id', '=', 'album_asset.assetsId').on('albumAssets.deletedAt', 'is', null), ) .select([ - 'assets.id as assetId', - 'assets.livePhotoVideoId as assetLivePhotoVideoId', + 'asset.id as assetId', + 'asset.livePhotoVideoId as assetLivePhotoVideoId', 'albumAssets.id as albumAssetId', 'albumAssets.livePhotoVideoId as albumAssetLivePhotoVideoId', ]) - .where('shared_links.id', '=', sharedLinkId) + .where('shared_link.id', '=', sharedLinkId) .where( - sql`array["assets"."id", "assets"."livePhotoVideoId", "albumAssets"."id", "albumAssets"."livePhotoVideoId"]`, + sql`array["asset"."id", "asset"."livePhotoVideoId", "albumAssets"."id", "albumAssets"."livePhotoVideoId"]`, '&&', sql`array[${sql.join([...assetIds])}]::uuid[] `, ) @@ -277,10 +273,10 @@ class AuthDeviceAccess { } return this.db - .selectFrom('sessions') - .select('sessions.id') - .where('sessions.userId', '=', userId) - .where('sessions.id', 'in', [...deviceIds]) + .selectFrom('session') + .select('session.id') + .where('session.userId', '=', userId) + .where('session.id', 'in', [...deviceIds]) .execute() .then((tokens) => new Set(tokens.map((token) => token.id))); } @@ -297,10 +293,10 @@ class NotificationAccess { } return this.db - .selectFrom('notifications') - .select('notifications.id') - .where('notifications.id', 'in', [...notificationIds]) - .where('notifications.userId', '=', userId) + .selectFrom('notification') + .select('notification.id') + .where('notification.id', 'in', [...notificationIds]) + .where('notification.userId', '=', userId) .execute() .then((stacks) => new Set(stacks.map((stack) => stack.id))); } @@ -317,10 +313,10 @@ class SessionAccess { } return this.db - .selectFrom('sessions') - .select('sessions.id') - .where('sessions.id', 'in', [...sessionIds]) - .where('sessions.userId', '=', userId) + .selectFrom('session') + .select('session.id') + .where('session.id', 'in', [...sessionIds]) + .where('session.userId', '=', userId) .execute() .then((sessions) => new Set(sessions.map((session) => session.id))); } @@ -336,10 +332,10 @@ class StackAccess { } return this.db - .selectFrom('asset_stack as stacks') - .select('stacks.id') - .where('stacks.id', 'in', [...stackIds]) - .where('stacks.ownerId', '=', userId) + .selectFrom('stack') + .select('stack.id') + .where('stack.id', 'in', [...stackIds]) + .where('stack.ownerId', '=', userId) .execute() .then((stacks) => new Set(stacks.map((stack) => stack.id))); } @@ -356,10 +352,10 @@ class TimelineAccess { } return this.db - .selectFrom('partners') - .select('partners.sharedById') - .where('partners.sharedById', 'in', [...partnerIds]) - .where('partners.sharedWithId', '=', userId) + .selectFrom('partner') + .select('partner.sharedById') + .where('partner.sharedById', 'in', [...partnerIds]) + .where('partner.sharedWithId', '=', userId) .execute() .then((partners) => new Set(partners.map((partner) => partner.sharedById))); } @@ -376,11 +372,11 @@ class MemoryAccess { } return this.db - .selectFrom('memories') - .select('memories.id') - .where('memories.id', 'in', [...memoryIds]) - .where('memories.ownerId', '=', userId) - .where('memories.deletedAt', 'is', null) + .selectFrom('memory') + .select('memory.id') + .where('memory.id', 'in', [...memoryIds]) + .where('memory.ownerId', '=', userId) + .where('memory.deletedAt', 'is', null) .execute() .then((memories) => new Set(memories.map((memory) => memory.id))); } @@ -413,13 +409,11 @@ class PersonAccess { } return this.db - .selectFrom('asset_faces') - .select('asset_faces.id') - .leftJoin('assets', (join) => - join.onRef('assets.id', '=', 'asset_faces.assetId').on('assets.deletedAt', 'is', null), - ) - .where('asset_faces.id', 'in', [...assetFaceIds]) - .where('assets.ownerId', '=', userId) + .selectFrom('asset_face') + .select('asset_face.id') + .leftJoin('asset', (join) => join.onRef('asset.id', '=', 'asset_face.assetId').on('asset.deletedAt', 'is', null)) + .where('asset_face.id', 'in', [...assetFaceIds]) + .where('asset.ownerId', '=', userId) .execute() .then((faces) => new Set(faces.map((face) => face.id))); } @@ -436,10 +430,10 @@ class PartnerAccess { } return this.db - .selectFrom('partners') - .select('partners.sharedById') - .where('partners.sharedById', 'in', [...partnerIds]) - .where('partners.sharedWithId', '=', userId) + .selectFrom('partner') + .select('partner.sharedById') + .where('partner.sharedById', 'in', [...partnerIds]) + .where('partner.sharedWithId', '=', userId) .execute() .then((partners) => new Set(partners.map((partner) => partner.sharedById))); } @@ -456,10 +450,10 @@ class TagAccess { } return this.db - .selectFrom('tags') - .select('tags.id') - .where('tags.id', 'in', [...tagIds]) - .where('tags.userId', '=', userId) + .selectFrom('tag') + .select('tag.id') + .where('tag.id', 'in', [...tagIds]) + .where('tag.userId', '=', userId) .execute() .then((tags) => new Set(tags.map((tag) => tag.id))); } diff --git a/server/src/repositories/activity.repository.ts b/server/src/repositories/activity.repository.ts index 0714d058cf..9b991ef17f 100644 --- a/server/src/repositories/activity.repository.ts +++ b/server/src/repositories/activity.repository.ts @@ -27,7 +27,9 @@ export class ActivityRepository { return this.db .selectFrom('activity') .selectAll('activity') - .innerJoin('users', (join) => join.onRef('users.id', '=', 'activity.userId').on('users.deletedAt', 'is', null)) + .innerJoin('user as user2', (join) => + join.onRef('user2.id', '=', 'activity.userId').on('user2.deletedAt', 'is', null), + ) .innerJoinLateral( (eb) => eb @@ -37,13 +39,13 @@ export class ActivityRepository { (join) => join.onTrue(), ) .select((eb) => eb.fn.toJson('user').as('user')) - .leftJoin('assets', 'assets.id', 'activity.assetId') + .leftJoin('asset', 'asset.id', 'activity.assetId') .$if(!!userId, (qb) => qb.where('activity.userId', '=', userId!)) .$if(assetId === null, (qb) => qb.where('assetId', 'is', null)) .$if(!!assetId, (qb) => qb.where('activity.assetId', '=', assetId!)) .$if(!!albumId, (qb) => qb.where('activity.albumId', '=', albumId!)) .$if(isLiked !== undefined, (qb) => qb.where('activity.isLiked', '=', isLiked!)) - .where('assets.deletedAt', 'is', null) + .where('asset.deletedAt', 'is', null) .orderBy('activity.createdAt', 'asc') .execute(); } @@ -55,7 +57,7 @@ export class ActivityRepository { .values(activity) .returningAll() .returning((eb) => - jsonObjectFrom(eb.selectFrom('users').whereRef('users.id', '=', 'activity.userId').select(columns.user)).as( + jsonObjectFrom(eb.selectFrom('user').whereRef('user.id', '=', 'activity.userId').select(columns.user)).as( 'user', ), ) @@ -82,14 +84,14 @@ export class ActivityRepository { eb.fn.countAll().filterWhere('activity.isLiked', '=', false).as('comments'), eb.fn.countAll().filterWhere('activity.isLiked', '=', true).as('likes'), ]) - .innerJoin('users', (join) => join.onRef('users.id', '=', 'activity.userId').on('users.deletedAt', 'is', null)) - .leftJoin('assets', 'assets.id', 'activity.assetId') + .innerJoin('user', (join) => join.onRef('user.id', '=', 'activity.userId').on('user.deletedAt', 'is', null)) + .leftJoin('asset', 'asset.id', 'activity.assetId') .$if(!!assetId, (qb) => qb.where('activity.assetId', '=', assetId!)) .where('activity.albumId', '=', albumId) .where(({ or, and, eb }) => or([ - and([eb('assets.deletedAt', 'is', null), eb('assets.visibility', '!=', sql.lit(AssetVisibility.LOCKED))]), - eb('assets.id', 'is', null), + and([eb('asset.deletedAt', 'is', null), eb('asset.visibility', '!=', sql.lit(AssetVisibility.LOCKED))]), + eb('asset.id', 'is', null), ]), ) .executeTakeFirstOrThrow(); diff --git a/server/src/repositories/album-user.repository.ts b/server/src/repositories/album-user.repository.ts index 82e631a6dc..d968ed100c 100644 --- a/server/src/repositories/album-user.repository.ts +++ b/server/src/repositories/album-user.repository.ts @@ -18,7 +18,7 @@ export class AlbumUserRepository { @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }] }) create(albumUser: Insertable) { return this.db - .insertInto('albums_shared_users_users') + .insertInto('album_user') .values(albumUser) .returning(['usersId', 'albumsId', 'role']) .executeTakeFirstOrThrow(); @@ -27,7 +27,7 @@ export class AlbumUserRepository { @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }, { role: AlbumUserRole.VIEWER }] }) update({ usersId, albumsId }: AlbumPermissionId, dto: Updateable) { return this.db - .updateTable('albums_shared_users_users') + .updateTable('album_user') .set(dto) .where('usersId', '=', usersId) .where('albumsId', '=', albumsId) @@ -37,10 +37,6 @@ export class AlbumUserRepository { @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }] }) async delete({ usersId, albumsId }: AlbumPermissionId): Promise { - await this.db - .deleteFrom('albums_shared_users_users') - .where('usersId', '=', usersId) - .where('albumsId', '=', albumsId) - .execute(); + await this.db.deleteFrom('album_user').where('usersId', '=', usersId).where('albumsId', '=', albumsId).execute(); } } diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index a52fa9c990..f077c36c41 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -21,47 +21,47 @@ export interface AlbumInfoOptions { withAssets: boolean; } -const withOwner = (eb: ExpressionBuilder) => { - return jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'albums.ownerId')) +const withOwner = (eb: ExpressionBuilder) => { + return jsonObjectFrom(eb.selectFrom('user').select(columns.user).whereRef('user.id', '=', 'album.ownerId')) .$notNull() .as('owner'); }; -const withAlbumUsers = (eb: ExpressionBuilder) => { +const withAlbumUsers = (eb: ExpressionBuilder) => { return jsonArrayFrom( eb - .selectFrom('albums_shared_users_users as album_users') - .select('album_users.role') + .selectFrom('album_user') + .select('album_user.role') .select((eb) => - jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'album_users.usersId')) + jsonObjectFrom(eb.selectFrom('user').select(columns.user).whereRef('user.id', '=', 'album_user.usersId')) .$notNull() .as('user'), ) - .whereRef('album_users.albumsId', '=', 'albums.id'), + .whereRef('album_user.albumsId', '=', 'album.id'), ) .$notNull() .as('albumUsers'); }; -const withSharedLink = (eb: ExpressionBuilder) => { - return jsonArrayFrom(eb.selectFrom('shared_links').selectAll().whereRef('shared_links.albumId', '=', 'albums.id')).as( +const withSharedLink = (eb: ExpressionBuilder) => { + return jsonArrayFrom(eb.selectFrom('shared_link').selectAll().whereRef('shared_link.albumId', '=', 'album.id')).as( 'sharedLinks', ); }; -const withAssets = (eb: ExpressionBuilder) => { +const withAssets = (eb: ExpressionBuilder) => { return eb .selectFrom((eb) => eb - .selectFrom('assets') - .selectAll('assets') - .leftJoin('exif', 'assets.id', 'exif.assetId') - .select((eb) => eb.table('exif').$castTo().as('exifInfo')) - .innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id') - .whereRef('albums_assets_assets.albumsId', '=', 'albums.id') - .where('assets.deletedAt', 'is', null) + .selectFrom('asset') + .selectAll('asset') + .leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .select((eb) => eb.table('asset_exif').$castTo().as('exifInfo')) + .innerJoin('album_asset', 'album_asset.assetsId', 'asset.id') + .whereRef('album_asset.albumsId', '=', 'album.id') + .where('asset.deletedAt', 'is', null) .$call(withDefaultVisibility) - .orderBy('assets.fileCreatedAt', 'desc') + .orderBy('asset.fileCreatedAt', 'desc') .as('asset'), ) .select((eb) => eb.fn.jsonAgg('asset').as('assets')) @@ -75,10 +75,10 @@ export class AlbumRepository { @GenerateSql({ params: [DummyValue.UUID, { withAssets: true }] }) async getById(id: string, options: AlbumInfoOptions) { return this.db - .selectFrom('albums') - .selectAll('albums') - .where('albums.id', '=', id) - .where('albums.deletedAt', 'is', null) + .selectFrom('album') + .selectAll('album') + .where('album.id', '=', id) + .where('album.deletedAt', 'is', null) .select(withOwner) .select(withAlbumUsers) .select(withSharedLink) @@ -90,26 +90,26 @@ export class AlbumRepository { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) async getByAssetId(ownerId: string, assetId: string) { return this.db - .selectFrom('albums') - .selectAll('albums') - .innerJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id') + .selectFrom('album') + .selectAll('album') + .innerJoin('album_asset', 'album_asset.albumsId', 'album.id') .where((eb) => eb.or([ - eb('albums.ownerId', '=', ownerId), + eb('album.ownerId', '=', ownerId), eb.exists( eb - .selectFrom('albums_shared_users_users as album_users') - .whereRef('album_users.albumsId', '=', 'albums.id') - .where('album_users.usersId', '=', ownerId), + .selectFrom('album_user') + .whereRef('album_user.albumsId', '=', 'album.id') + .where('album_user.usersId', '=', ownerId), ), ]), ) - .where('album_assets.assetsId', '=', assetId) - .where('albums.deletedAt', 'is', null) - .orderBy('albums.createdAt', 'desc') + .where('album_asset.assetsId', '=', assetId) + .where('album.deletedAt', 'is', null) + .orderBy('album.createdAt', 'desc') .select(withOwner) .select(withAlbumUsers) - .orderBy('albums.createdAt', 'desc') + .orderBy('album.createdAt', 'desc') .execute(); } @@ -123,18 +123,18 @@ export class AlbumRepository { return ( this.db - .selectFrom('assets') + .selectFrom('asset') .$call(withDefaultVisibility) - .innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'assets.id') - .select('album_assets.albumsId as albumId') - .select((eb) => eb.fn.min(sql`("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date`).as('startDate')) - .select((eb) => eb.fn.max(sql`("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date`).as('endDate')) + .innerJoin('album_asset', 'album_asset.assetsId', 'asset.id') + .select('album_asset.albumsId as albumId') + .select((eb) => eb.fn.min(sql`("asset"."localDateTime" AT TIME ZONE 'UTC'::text)::date`).as('startDate')) + .select((eb) => eb.fn.max(sql`("asset"."localDateTime" AT TIME ZONE 'UTC'::text)::date`).as('endDate')) // lastModifiedAssetTimestamp is only used in mobile app, please remove if not need - .select((eb) => eb.fn.max('assets.updatedAt').as('lastModifiedAssetTimestamp')) - .select((eb) => sql`${eb.fn.count('assets.id')}::int`.as('assetCount')) - .where('album_assets.albumsId', 'in', ids) - .where('assets.deletedAt', 'is', null) - .groupBy('album_assets.albumsId') + .select((eb) => eb.fn.max('asset.updatedAt').as('lastModifiedAssetTimestamp')) + .select((eb) => sql`${eb.fn.count('asset.id')}::int`.as('assetCount')) + .where('album_asset.albumsId', 'in', ids) + .where('asset.deletedAt', 'is', null) + .groupBy('album_asset.albumsId') .execute() ); } @@ -142,14 +142,14 @@ export class AlbumRepository { @GenerateSql({ params: [DummyValue.UUID] }) async getOwned(ownerId: string) { return this.db - .selectFrom('albums') - .selectAll('albums') + .selectFrom('album') + .selectAll('album') .select(withOwner) .select(withAlbumUsers) .select(withSharedLink) - .where('albums.ownerId', '=', ownerId) - .where('albums.deletedAt', 'is', null) - .orderBy('albums.createdAt', 'desc') + .where('album.ownerId', '=', ownerId) + .where('album.deletedAt', 'is', null) + .orderBy('album.createdAt', 'desc') .execute(); } @@ -159,29 +159,29 @@ export class AlbumRepository { @GenerateSql({ params: [DummyValue.UUID] }) async getShared(ownerId: string) { return this.db - .selectFrom('albums') - .selectAll('albums') + .selectFrom('album') + .selectAll('album') .where((eb) => eb.or([ eb.exists( eb - .selectFrom('albums_shared_users_users as album_users') - .whereRef('album_users.albumsId', '=', 'albums.id') - .where((eb) => eb.or([eb('albums.ownerId', '=', ownerId), eb('album_users.usersId', '=', ownerId)])), + .selectFrom('album_user') + .whereRef('album_user.albumsId', '=', 'album.id') + .where((eb) => eb.or([eb('album.ownerId', '=', ownerId), eb('album_user.usersId', '=', ownerId)])), ), eb.exists( eb - .selectFrom('shared_links') - .whereRef('shared_links.albumId', '=', 'albums.id') - .where('shared_links.userId', '=', ownerId), + .selectFrom('shared_link') + .whereRef('shared_link.albumId', '=', 'album.id') + .where('shared_link.userId', '=', ownerId), ), ]), ) - .where('albums.deletedAt', 'is', null) + .where('album.deletedAt', 'is', null) .select(withAlbumUsers) .select(withOwner) .select(withSharedLink) - .orderBy('albums.createdAt', 'desc') + .orderBy('album.createdAt', 'desc') .execute(); } @@ -191,43 +191,33 @@ export class AlbumRepository { @GenerateSql({ params: [DummyValue.UUID] }) async getNotShared(ownerId: string) { return this.db - .selectFrom('albums') - .selectAll('albums') - .where('albums.ownerId', '=', ownerId) - .where('albums.deletedAt', 'is', null) - .where((eb) => - eb.not( - eb.exists( - eb - .selectFrom('albums_shared_users_users as album_users') - .whereRef('album_users.albumsId', '=', 'albums.id'), - ), - ), - ) - .where((eb) => - eb.not(eb.exists(eb.selectFrom('shared_links').whereRef('shared_links.albumId', '=', 'albums.id'))), - ) + .selectFrom('album') + .selectAll('album') + .where('album.ownerId', '=', ownerId) + .where('album.deletedAt', 'is', null) + .where((eb) => eb.not(eb.exists(eb.selectFrom('album_user').whereRef('album_user.albumsId', '=', 'album.id')))) + .where((eb) => eb.not(eb.exists(eb.selectFrom('shared_link').whereRef('shared_link.albumId', '=', 'album.id')))) .select(withOwner) - .orderBy('albums.createdAt', 'desc') + .orderBy('album.createdAt', 'desc') .execute(); } async restoreAll(userId: string): Promise { - await this.db.updateTable('albums').set({ deletedAt: null }).where('ownerId', '=', userId).execute(); + await this.db.updateTable('album').set({ deletedAt: null }).where('ownerId', '=', userId).execute(); } async softDeleteAll(userId: string): Promise { - await this.db.updateTable('albums').set({ deletedAt: new Date() }).where('ownerId', '=', userId).execute(); + await this.db.updateTable('album').set({ deletedAt: new Date() }).where('ownerId', '=', userId).execute(); } async deleteAll(userId: string): Promise { - await this.db.deleteFrom('albums').where('ownerId', '=', userId).execute(); + await this.db.deleteFrom('album').where('ownerId', '=', userId).execute(); } @GenerateSql({ params: [[DummyValue.UUID]] }) @Chunked() async removeAssetsFromAll(assetIds: string[]): Promise { - await this.db.deleteFrom('albums_assets_assets').where('albums_assets_assets.assetsId', 'in', assetIds).execute(); + await this.db.deleteFrom('album_asset').where('album_asset.assetsId', 'in', assetIds).execute(); } @Chunked({ paramIndex: 1 }) @@ -237,9 +227,9 @@ export class AlbumRepository { } await this.db - .deleteFrom('albums_assets_assets') - .where('albums_assets_assets.albumsId', '=', albumId) - .where('albums_assets_assets.assetsId', 'in', assetIds) + .deleteFrom('album_asset') + .where('album_asset.albumsId', '=', albumId) + .where('album_asset.assetsId', 'in', assetIds) .execute(); } @@ -258,10 +248,10 @@ export class AlbumRepository { } return this.db - .selectFrom('albums_assets_assets') + .selectFrom('album_asset') .selectAll() - .where('albums_assets_assets.albumsId', '=', albumId) - .where('albums_assets_assets.assetsId', 'in', assetIds) + .where('album_asset.albumsId', '=', albumId) + .where('album_asset.assetsId', 'in', assetIds) .execute() .then((results) => new Set(results.map(({ assetsId }) => assetsId))); } @@ -272,7 +262,7 @@ export class AlbumRepository { create(album: Insertable, assetIds: string[], albumUsers: AlbumUserCreateDto[]) { return this.db.transaction().execute(async (tx) => { - const newAlbum = await tx.insertInto('albums').values(album).returning('albums.id').executeTakeFirst(); + const newAlbum = await tx.insertInto('album').values(album).returning('album.id').executeTakeFirst(); if (!newAlbum) { throw new Error('Failed to create album'); @@ -284,7 +274,7 @@ export class AlbumRepository { if (albumUsers.length > 0) { await tx - .insertInto('albums_shared_users_users') + .insertInto('album_user') .values( albumUsers.map((albumUser) => ({ albumsId: newAlbum.id, usersId: albumUser.userId, role: albumUser.role })), ) @@ -292,7 +282,7 @@ export class AlbumRepository { } return tx - .selectFrom('albums') + .selectFrom('album') .selectAll() .where('id', '=', newAlbum.id) .select(withOwner) @@ -305,10 +295,10 @@ export class AlbumRepository { update(id: string, album: Updateable) { return this.db - .updateTable('albums') + .updateTable('album') .set(album) .where('id', '=', id) - .returningAll('albums') + .returningAll('album') .returning(withOwner) .returning(withSharedLink) .returning(withAlbumUsers) @@ -316,7 +306,7 @@ export class AlbumRepository { } async delete(id: string): Promise { - await this.db.deleteFrom('albums').where('id', '=', id).execute(); + await this.db.deleteFrom('album').where('id', '=', id).execute(); } @Chunked({ paramIndex: 2, chunkSize: 30_000 }) @@ -326,7 +316,7 @@ export class AlbumRepository { } await db - .insertInto('albums_assets_assets') + .insertInto('album_asset') .values(assetIds.map((assetId) => ({ albumsId: albumId, assetsId: assetId }))) .execute(); } @@ -343,11 +333,11 @@ export class AlbumRepository { // Subquery for getting a new thumbnail. const result = await this.db - .updateTable('albums') + .updateTable('album') .set((eb) => ({ albumThumbnailAssetId: this.updateThumbnailBuilder(eb) - .select('album_assets.assetsId') - .orderBy('assets.fileCreatedAt', 'desc') + .select('album_asset.assetsId') + .orderBy('asset.fileCreatedAt', 'desc') .limit(1), })) .where((eb) => @@ -362,7 +352,7 @@ export class AlbumRepository { eb.exists( this.updateThumbnailBuilder(eb) .select(sql`1`.as('1')) - .whereRef('albums.albumThumbnailAssetId', '=', 'album_assets.assetsId'), // Has invalid assets + .whereRef('album.albumThumbnailAssetId', '=', 'album_asset.assetsId'), // Has invalid assets ), ), ]), @@ -373,12 +363,12 @@ export class AlbumRepository { return Number(result[0].numUpdatedRows); } - private updateThumbnailBuilder(eb: ExpressionBuilder) { + private updateThumbnailBuilder(eb: ExpressionBuilder) { return eb - .selectFrom('albums_assets_assets as album_assets') - .innerJoin('assets', (join) => - join.onRef('album_assets.assetsId', '=', 'assets.id').on('assets.deletedAt', 'is', null), + .selectFrom('album_asset') + .innerJoin('asset', (join) => + join.onRef('album_asset.assetsId', '=', 'asset.id').on('asset.deletedAt', 'is', null), ) - .whereRef('album_assets.albumsId', '=', 'albums.id'); + .whereRef('album_asset.albumsId', '=', 'album.id'); } } diff --git a/server/src/repositories/api-key.repository.ts b/server/src/repositories/api-key.repository.ts index 5f71399a38..28307d7c83 100644 --- a/server/src/repositories/api-key.repository.ts +++ b/server/src/repositories/api-key.repository.ts @@ -13,45 +13,45 @@ export class ApiKeyRepository { constructor(@InjectKysely() private db: Kysely) {} create(dto: Insertable) { - return this.db.insertInto('api_keys').values(dto).returning(columns.apiKey).executeTakeFirstOrThrow(); + return this.db.insertInto('api_key').values(dto).returning(columns.apiKey).executeTakeFirstOrThrow(); } async update(userId: string, id: string, dto: Updateable) { return this.db - .updateTable('api_keys') + .updateTable('api_key') .set(dto) - .where('api_keys.userId', '=', userId) + .where('api_key.userId', '=', userId) .where('id', '=', asUuid(id)) .returning(columns.apiKey) .executeTakeFirstOrThrow(); } async delete(userId: string, id: string) { - await this.db.deleteFrom('api_keys').where('userId', '=', userId).where('id', '=', asUuid(id)).execute(); + await this.db.deleteFrom('api_key').where('userId', '=', userId).where('id', '=', asUuid(id)).execute(); } @GenerateSql({ params: [DummyValue.STRING] }) getKey(hashedToken: string) { return this.db - .selectFrom('api_keys') + .selectFrom('api_key') .select((eb) => [ ...columns.authApiKey, jsonObjectFrom( eb - .selectFrom('users') + .selectFrom('user') .select(columns.authUser) - .whereRef('users.id', '=', 'api_keys.userId') - .where('users.deletedAt', 'is', null), + .whereRef('user.id', '=', 'api_key.userId') + .where('user.deletedAt', 'is', null), ).as('user'), ]) - .where('api_keys.key', '=', hashedToken) + .where('api_key.key', '=', hashedToken) .executeTakeFirst(); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) getById(userId: string, id: string) { return this.db - .selectFrom('api_keys') + .selectFrom('api_key') .select(columns.apiKey) .where('id', '=', asUuid(id)) .where('userId', '=', userId) @@ -61,7 +61,7 @@ export class ApiKeyRepository { @GenerateSql({ params: [DummyValue.UUID] }) getByUserId(userId: string) { return this.db - .selectFrom('api_keys') + .selectFrom('api_key') .select(columns.apiKey) .where('userId', '=', userId) .orderBy('createdAt', 'desc') diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index 4ae52556b4..c784ae276f 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -26,9 +26,9 @@ export class AssetJobRepository { @GenerateSql({ params: [DummyValue.UUID] }) getForSearchDuplicatesJob(id: string) { return this.db - .selectFrom('assets') - .where('assets.id', '=', asUuid(id)) - .leftJoin('smart_search', 'assets.id', 'smart_search.assetId') + .selectFrom('asset') + .where('asset.id', '=', asUuid(id)) + .leftJoin('smart_search', 'asset.id', 'smart_search.assetId') .select(['id', 'type', 'ownerId', 'duplicateId', 'stackId', 'visibility', 'smart_search.embedding']) .limit(1) .executeTakeFirst(); @@ -37,18 +37,18 @@ export class AssetJobRepository { @GenerateSql({ params: [DummyValue.UUID] }) getForSidecarWriteJob(id: string) { return this.db - .selectFrom('assets') - .where('assets.id', '=', asUuid(id)) + .selectFrom('asset') + .where('asset.id', '=', asUuid(id)) .select((eb) => [ 'id', 'sidecarPath', 'originalPath', jsonArrayFrom( eb - .selectFrom('tags') - .select(['tags.value']) - .innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId') - .whereRef('assets.id', '=', 'tag_asset.assetsId'), + .selectFrom('tag') + .select(['tag.value']) + .innerJoin('tag_asset', 'tag.id', 'tag_asset.tagsId') + .whereRef('asset.id', '=', 'tag_asset.assetsId'), ).as('tags'), ]) .limit(1) @@ -58,20 +58,20 @@ export class AssetJobRepository { @GenerateSql({ params: [false], stream: true }) streamForThumbnailJob(force: boolean) { return this.db - .selectFrom('assets') - .select(['assets.id', 'assets.thumbhash']) + .selectFrom('asset') + .select(['asset.id', 'asset.thumbhash']) .select(withFiles) - .where('assets.deletedAt', 'is', null) - .where('assets.visibility', '!=', AssetVisibility.HIDDEN) + .where('asset.deletedAt', 'is', null) + .where('asset.visibility', '!=', AssetVisibility.HIDDEN) .$if(!force, (qb) => qb // If there aren't any entries, metadata extraction hasn't run yet which is required for thumbnails - .innerJoin('asset_job_status', 'asset_job_status.assetId', 'assets.id') + .innerJoin('asset_job_status', 'asset_job_status.assetId', 'asset.id') .where((eb) => eb.or([ eb('asset_job_status.previewAt', 'is', null), eb('asset_job_status.thumbnailAt', 'is', null), - eb('assets.thumbhash', 'is', null), + eb('asset.thumbhash', 'is', null), ]), ), ) @@ -81,72 +81,72 @@ export class AssetJobRepository { @GenerateSql({ params: [DummyValue.UUID] }) getForMigrationJob(id: string) { return this.db - .selectFrom('assets') - .select(['assets.id', 'assets.ownerId', 'assets.encodedVideoPath']) + .selectFrom('asset') + .select(['asset.id', 'asset.ownerId', 'asset.encodedVideoPath']) .select(withFiles) - .where('assets.id', '=', id) + .where('asset.id', '=', id) .executeTakeFirst(); } @GenerateSql({ params: [DummyValue.UUID] }) getForGenerateThumbnailJob(id: string) { return this.db - .selectFrom('assets') + .selectFrom('asset') .select([ - 'assets.id', - 'assets.visibility', - 'assets.originalFileName', - 'assets.originalPath', - 'assets.ownerId', - 'assets.thumbhash', - 'assets.type', + 'asset.id', + 'asset.visibility', + 'asset.originalFileName', + 'asset.originalPath', + 'asset.ownerId', + 'asset.thumbhash', + 'asset.type', ]) .select(withFiles) .$call(withExifInner) - .where('assets.id', '=', id) + .where('asset.id', '=', id) .executeTakeFirst(); } @GenerateSql({ params: [DummyValue.UUID] }) getForMetadataExtraction(id: string) { return this.db - .selectFrom('assets') + .selectFrom('asset') .select(columns.asset) .select(withFaces) - .where('assets.id', '=', id) + .where('asset.id', '=', id) .executeTakeFirst(); } @GenerateSql({ params: [DummyValue.UUID, AssetFileType.THUMBNAIL] }) getAlbumThumbnailFiles(id: string, fileType?: AssetFileType) { return this.db - .selectFrom('asset_files') + .selectFrom('asset_file') .select(columns.assetFiles) - .where('asset_files.assetId', '=', id) - .$if(!!fileType, (qb) => qb.where('asset_files.type', '=', fileType!)) + .where('asset_file.assetId', '=', id) + .$if(!!fileType, (qb) => qb.where('asset_file.type', '=', fileType!)) .execute(); } private assetsWithPreviews() { return this.db - .selectFrom('assets') - .where('assets.visibility', '!=', AssetVisibility.HIDDEN) - .where('assets.deletedAt', 'is', null) - .innerJoin('asset_job_status as job_status', 'assetId', 'assets.id') + .selectFrom('asset') + .where('asset.visibility', '!=', AssetVisibility.HIDDEN) + .where('asset.deletedAt', 'is', null) + .innerJoin('asset_job_status as job_status', 'assetId', 'asset.id') .where('job_status.previewAt', 'is not', null); } @GenerateSql({ params: [], stream: true }) streamForSearchDuplicates(force?: boolean) { return this.db - .selectFrom('assets') - .select(['assets.id']) - .where('assets.deletedAt', 'is', null) - .innerJoin('smart_search', 'assets.id', 'smart_search.assetId') + .selectFrom('asset') + .select(['asset.id']) + .where('asset.deletedAt', 'is', null) + .innerJoin('smart_search', 'asset.id', 'smart_search.assetId') .$call(withDefaultVisibility) .$if(!force, (qb) => qb - .innerJoin('asset_job_status as job_status', 'job_status.assetId', 'assets.id') + .innerJoin('asset_job_status as job_status', 'job_status.assetId', 'asset.id') .where('job_status.duplicatesDetectedAt', 'is', null), ) .stream(); @@ -155,11 +155,9 @@ export class AssetJobRepository { @GenerateSql({ params: [], stream: true }) streamForEncodeClip(force?: boolean) { return this.assetsWithPreviews() - .select(['assets.id']) + .select(['asset.id']) .$if(!force, (qb) => - qb.where((eb) => - eb.not((eb) => eb.exists(eb.selectFrom('smart_search').whereRef('assetId', '=', 'assets.id'))), - ), + qb.where((eb) => eb.not((eb) => eb.exists(eb.selectFrom('smart_search').whereRef('assetId', '=', 'asset.id')))), ) .stream(); } @@ -167,142 +165,142 @@ export class AssetJobRepository { @GenerateSql({ params: [DummyValue.UUID] }) getForClipEncoding(id: string) { return this.db - .selectFrom('assets') - .select(['assets.id', 'assets.visibility']) + .selectFrom('asset') + .select(['asset.id', 'asset.visibility']) .select((eb) => withFiles(eb, AssetFileType.PREVIEW)) - .where('assets.id', '=', id) + .where('asset.id', '=', id) .executeTakeFirst(); } @GenerateSql({ params: [DummyValue.UUID] }) getForDetectFacesJob(id: string) { return this.db - .selectFrom('assets') - .select(['assets.id', 'assets.visibility']) + .selectFrom('asset') + .select(['asset.id', 'asset.visibility']) .$call(withExifInner) .select((eb) => withFaces(eb, true)) .select((eb) => withFiles(eb, AssetFileType.PREVIEW)) - .where('assets.id', '=', id) + .where('asset.id', '=', id) .executeTakeFirst(); } @GenerateSql({ params: [[DummyValue.UUID]] }) getForSyncAssets(ids: string[]) { return this.db - .selectFrom('assets') + .selectFrom('asset') .select([ - 'assets.id', - 'assets.isOffline', - 'assets.libraryId', - 'assets.originalPath', - 'assets.status', - 'assets.fileModifiedAt', + 'asset.id', + 'asset.isOffline', + 'asset.libraryId', + 'asset.originalPath', + 'asset.status', + 'asset.fileModifiedAt', ]) - .where('assets.id', '=', anyUuid(ids)) + .where('asset.id', '=', anyUuid(ids)) .execute(); } @GenerateSql({ params: [DummyValue.UUID] }) getForAssetDeletion(id: string) { return this.db - .selectFrom('assets') + .selectFrom('asset') .select([ - 'assets.id', - 'assets.visibility', - 'assets.libraryId', - 'assets.ownerId', - 'assets.livePhotoVideoId', - 'assets.sidecarPath', - 'assets.encodedVideoPath', - 'assets.originalPath', + 'asset.id', + 'asset.visibility', + 'asset.libraryId', + 'asset.ownerId', + 'asset.livePhotoVideoId', + 'asset.sidecarPath', + 'asset.encodedVideoPath', + 'asset.originalPath', ]) .$call(withExif) .select(withFacesAndPeople) .select(withFiles) - .leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId') + .leftJoin('stack', 'stack.id', 'asset.stackId') .leftJoinLateral( (eb) => eb - .selectFrom('assets as stacked') - .select(['asset_stack.id', 'asset_stack.primaryAssetId']) + .selectFrom('asset as stacked') + .select(['stack.id', 'stack.primaryAssetId']) .select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets')) .where('stacked.deletedAt', 'is not', null) .where('stacked.visibility', '=', AssetVisibility.TIMELINE) - .whereRef('stacked.stackId', '=', 'asset_stack.id') - .groupBy('asset_stack.id') + .whereRef('stacked.stackId', '=', 'stack.id') + .groupBy('stack.id') .as('stacked_assets'), - (join) => join.on('asset_stack.id', 'is not', null), + (join) => join.on('stack.id', 'is not', null), ) .select((eb) => toJson(eb, 'stacked_assets').as('stack')) - .where('assets.id', '=', id) + .where('asset.id', '=', id) .executeTakeFirst(); } @GenerateSql({ params: [], stream: true }) streamForVideoConversion(force?: boolean) { return this.db - .selectFrom('assets') - .select(['assets.id']) - .where('assets.type', '=', AssetType.VIDEO) + .selectFrom('asset') + .select(['asset.id']) + .where('asset.type', '=', AssetType.VIDEO) .$if(!force, (qb) => qb - .where((eb) => eb.or([eb('assets.encodedVideoPath', 'is', null), eb('assets.encodedVideoPath', '=', '')])) - .where('assets.visibility', '!=', AssetVisibility.HIDDEN), + .where((eb) => eb.or([eb('asset.encodedVideoPath', 'is', null), eb('asset.encodedVideoPath', '=', '')])) + .where('asset.visibility', '!=', AssetVisibility.HIDDEN), ) - .where('assets.deletedAt', 'is', null) + .where('asset.deletedAt', 'is', null) .stream(); } @GenerateSql({ params: [DummyValue.UUID] }) getForVideoConversion(id: string) { return this.db - .selectFrom('assets') - .select(['assets.id', 'assets.ownerId', 'assets.originalPath', 'assets.encodedVideoPath']) - .where('assets.id', '=', id) - .where('assets.type', '=', AssetType.VIDEO) + .selectFrom('asset') + .select(['asset.id', 'asset.ownerId', 'asset.originalPath', 'asset.encodedVideoPath']) + .where('asset.id', '=', id) + .where('asset.type', '=', AssetType.VIDEO) .executeTakeFirst(); } @GenerateSql({ params: [], stream: true }) streamForMetadataExtraction(force?: boolean) { return this.db - .selectFrom('assets') - .select(['assets.id']) + .selectFrom('asset') + .select(['asset.id']) .$if(!force, (qb) => qb - .leftJoin('asset_job_status', 'asset_job_status.assetId', 'assets.id') + .leftJoin('asset_job_status', 'asset_job_status.assetId', 'asset.id') .where((eb) => eb.or([eb('asset_job_status.metadataExtractedAt', 'is', null), eb('asset_job_status.assetId', 'is', null)]), ), ) - .where('assets.deletedAt', 'is', null) + .where('asset.deletedAt', 'is', null) .stream(); } private storageTemplateAssetQuery() { return this.db - .selectFrom('assets') - .innerJoin('exif', 'assets.id', 'exif.assetId') + .selectFrom('asset') + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') .select([ - 'assets.id', - 'assets.ownerId', - 'assets.type', - 'assets.checksum', - 'assets.originalPath', - 'assets.isExternal', - 'assets.sidecarPath', - 'assets.originalFileName', - 'assets.livePhotoVideoId', - 'assets.fileCreatedAt', - 'exif.timeZone', - 'exif.fileSizeInByte', + 'asset.id', + 'asset.ownerId', + 'asset.type', + 'asset.checksum', + 'asset.originalPath', + 'asset.isExternal', + 'asset.sidecarPath', + 'asset.originalFileName', + 'asset.livePhotoVideoId', + 'asset.fileCreatedAt', + 'asset_exif.timeZone', + 'asset_exif.fileSizeInByte', ]) - .where('assets.deletedAt', 'is', null); + .where('asset.deletedAt', 'is', null); } @GenerateSql({ params: [DummyValue.UUID] }) getForStorageTemplateJob(id: string): Promise { - return this.storageTemplateAssetQuery().where('assets.id', '=', id).executeTakeFirst() as Promise< + return this.storageTemplateAssetQuery().where('asset.id', '=', id).executeTakeFirst() as Promise< StorageAsset | undefined >; } @@ -315,21 +313,21 @@ export class AssetJobRepository { @GenerateSql({ params: [DummyValue.DATE], stream: true }) streamForDeletedJob(trashedBefore: Date) { return this.db - .selectFrom('assets') + .selectFrom('asset') .select(['id', 'isOffline']) - .where('assets.deletedAt', '<=', trashedBefore) + .where('asset.deletedAt', '<=', trashedBefore) .stream(); } @GenerateSql({ params: [], stream: true }) streamForSidecar(force?: boolean) { return this.db - .selectFrom('assets') - .select(['assets.id']) + .selectFrom('asset') + .select(['asset.id']) .$if(!force, (qb) => - qb.where((eb) => eb.or([eb('assets.sidecarPath', '=', ''), eb('assets.sidecarPath', 'is', null)])), + qb.where((eb) => eb.or([eb('asset.sidecarPath', '=', ''), eb('asset.sidecarPath', 'is', null)])), ) - .where('assets.visibility', '!=', AssetVisibility.HIDDEN) + .where('asset.visibility', '!=', AssetVisibility.HIDDEN) .stream(); } @@ -337,13 +335,13 @@ export class AssetJobRepository { streamForDetectFacesJob(force?: boolean) { return this.assetsWithPreviews() .$if(!force, (qb) => qb.where('job_status.facesRecognizedAt', 'is', null)) - .select(['assets.id']) - .orderBy('assets.createdAt', 'desc') + .select(['asset.id']) + .orderBy('asset.createdAt', 'desc') .stream(); } @GenerateSql({ params: [DummyValue.DATE], stream: true }) streamForMigrationJob() { - return this.db.selectFrom('assets').select(['id']).where('assets.deletedAt', 'is', null).stream(); + return this.db.selectFrom('asset').select(['id']).where('asset.deletedAt', 'is', null).stream(); } } diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index f3e798d2b4..f00d0c170f 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -6,10 +6,10 @@ import { Stack } from 'src/database'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; import { AssetFileType, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; import { DB } from 'src/schema'; -import { AssetFileTable } from 'src/schema/tables/asset-files.table'; +import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; +import { AssetFileTable } from 'src/schema/tables/asset-file.table'; import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { ExifTable } from 'src/schema/tables/exif.table'; import { anyUuid, asUuid, @@ -114,10 +114,10 @@ interface GetByIdsRelations { export class AssetRepository { constructor(@InjectKysely() private db: Kysely) {} - async upsertExif(exif: Insertable): Promise { + async upsertExif(exif: Insertable): Promise { const value = { ...exif, assetId: asUuid(exif.assetId) }; await this.db - .insertInto('exif') + .insertInto('asset_exif') .values(value) .onConflict((oc) => oc.column('assetId').doUpdateSet((eb) => @@ -161,12 +161,12 @@ export class AssetRepository { @GenerateSql({ params: [[DummyValue.UUID], { model: DummyValue.STRING }] }) @Chunked() - async updateAllExif(ids: string[], options: Updateable): Promise { + async updateAllExif(ids: string[], options: Updateable): Promise { if (ids.length === 0) { return; } - await this.db.updateTable('exif').set(options).where('assetId', 'in', ids).execute(); + await this.db.updateTable('asset_exif').set(options).where('assetId', 'in', ids).execute(); } async upsertJobStatus(...jobStatus: Insertable[]): Promise { @@ -196,11 +196,11 @@ export class AssetRepository { } create(asset: Insertable) { - return this.db.insertInto('assets').values(asset).returningAll().executeTakeFirstOrThrow(); + return this.db.insertInto('asset').values(asset).returningAll().executeTakeFirstOrThrow(); } createAll(assets: Insertable[]) { - return this.db.insertInto('assets').values(assets).returningAll().execute(); + return this.db.insertInto('asset').values(assets).returningAll().execute(); } @GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] }) @@ -213,7 +213,7 @@ export class AssetRepository { .selectFrom((eb) => eb .fn('generate_series', [ - sql`(select date_part('year', min(("localDateTime" at time zone 'UTC')::date))::int from assets)`, + sql`(select date_part('year', min(("localDateTime" at time zone 'UTC')::date))::int from asset)`, sql`date_part('year', current_date)::int - 1`, ]) .as('year'), @@ -224,30 +224,30 @@ export class AssetRepository { .innerJoinLateral( (qb) => qb - .selectFrom('assets') - .selectAll('assets') - .innerJoin('asset_job_status', 'assets.id', 'asset_job_status.assetId') + .selectFrom('asset') + .selectAll('asset') + .innerJoin('asset_job_status', 'asset.id', 'asset_job_status.assetId') .where('asset_job_status.previewAt', 'is not', null) - .where(sql`(assets."localDateTime" at time zone 'UTC')::date`, '=', sql`today.date`) - .where('assets.ownerId', '=', anyUuid(ownerIds)) - .where('assets.visibility', '=', AssetVisibility.TIMELINE) + .where(sql`(asset."localDateTime" at time zone 'UTC')::date`, '=', sql`today.date`) + .where('asset.ownerId', '=', anyUuid(ownerIds)) + .where('asset.visibility', '=', AssetVisibility.TIMELINE) .where((eb) => eb.exists((qb) => qb - .selectFrom('asset_files') - .whereRef('assetId', '=', 'assets.id') - .where('asset_files.type', '=', AssetFileType.PREVIEW), + .selectFrom('asset_file') + .whereRef('assetId', '=', 'asset.id') + .where('asset_file.type', '=', AssetFileType.PREVIEW), ), ) - .where('assets.deletedAt', 'is', null) - .orderBy(sql`(assets."localDateTime" at time zone 'UTC')::date`, 'desc') + .where('asset.deletedAt', 'is', null) + .orderBy(sql`(asset."localDateTime" at time zone 'UTC')::date`, 'desc') .limit(20) .as('a'), (join) => join.onTrue(), ) - .innerJoin('exif', 'a.id', 'exif.assetId') + .innerJoin('asset_exif', 'a.id', 'asset_exif.assetId') .selectAll('a') - .select((eb) => eb.fn.toJson(eb.table('exif')).as('exifInfo')), + .select((eb) => eb.fn.toJson(eb.table('asset_exif')).as('exifInfo')), ) .selectFrom('res') .select(sql`date_part('year', ("localDateTime" at time zone 'UTC')::date)::int`.as('year')) @@ -260,30 +260,30 @@ export class AssetRepository { @GenerateSql({ params: [[DummyValue.UUID]] }) @ChunkedArray() getByIds(ids: string[]) { - return this.db.selectFrom('assets').selectAll('assets').where('assets.id', '=', anyUuid(ids)).execute(); + return this.db.selectFrom('asset').selectAll('asset').where('asset.id', '=', anyUuid(ids)).execute(); } @GenerateSql({ params: [[DummyValue.UUID]] }) @ChunkedArray() getByIdsWithAllRelationsButStacks(ids: string[]) { return this.db - .selectFrom('assets') - .selectAll('assets') + .selectFrom('asset') + .selectAll('asset') .select(withFacesAndPeople) .select(withTags) .$call(withExif) - .where('assets.id', '=', anyUuid(ids)) + .where('asset.id', '=', anyUuid(ids)) .execute(); } @GenerateSql({ params: [DummyValue.UUID] }) async deleteAll(ownerId: string): Promise { - await this.db.deleteFrom('assets').where('ownerId', '=', ownerId).execute(); + await this.db.deleteFrom('asset').where('ownerId', '=', ownerId).execute(); } async getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise { const assets = await this.db - .selectFrom('assets') + .selectFrom('asset') .select(['deviceAssetId']) .where('deviceAssetId', 'in', deviceAssetIds) .where('deviceId', '=', deviceId) @@ -296,8 +296,8 @@ export class AssetRepository { @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string) { return this.db - .selectFrom('assets') - .selectAll('assets') + .selectFrom('asset') + .selectAll('asset') .where('libraryId', '=', asUuid(libraryId)) .where('originalPath', '=', originalPath) .limit(1) @@ -314,7 +314,7 @@ export class AssetRepository { @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) async getAllByDeviceId(ownerId: string, deviceId: string): Promise { const items = await this.db - .selectFrom('assets') + .selectFrom('asset') .select(['deviceAssetId']) .where('ownerId', '=', asUuid(ownerId)) .where('deviceId', '=', deviceId) @@ -328,7 +328,7 @@ export class AssetRepository { @GenerateSql({ params: [DummyValue.UUID] }) async getLivePhotoCount(motionId: string): Promise { const [{ count }] = await this.db - .selectFrom('assets') + .selectFrom('asset') .select((eb) => eb.fn.countAll().as('count')) .where('livePhotoVideoId', '=', asUuid(motionId)) .execute(); @@ -338,9 +338,9 @@ export class AssetRepository { @GenerateSql({ params: [DummyValue.UUID] }) getById(id: string, { exifInfo, faces, files, library, owner, smartSearch, stack, tags }: GetByIdsRelations = {}) { return this.db - .selectFrom('assets') - .selectAll('assets') - .where('assets.id', '=', asUuid(id)) + .selectFrom('asset') + .selectAll('asset') + .where('asset.id', '=', asUuid(id)) .$if(!!exifInfo, withExif) .$if(!!faces, (qb) => qb.select(faces?.person ? withFacesAndPeople : withFaces).$narrowType<{ faces: NotNull }>()) .$if(!!library, (qb) => qb.select(withLibrary)) @@ -348,25 +348,25 @@ export class AssetRepository { .$if(!!smartSearch, withSmartSearch) .$if(!!stack, (qb) => qb - .leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId') + .leftJoin('stack', 'stack.id', 'asset.stackId') .$if(!stack!.assets, (qb) => - qb.select((eb) => eb.fn.toJson(eb.table('asset_stack')).$castTo().as('stack')), + qb.select((eb) => eb.fn.toJson(eb.table('stack')).$castTo().as('stack')), ) .$if(!!stack!.assets, (qb) => qb .leftJoinLateral( (eb) => eb - .selectFrom('assets as stacked') - .selectAll('asset_stack') + .selectFrom('asset as stacked') + .selectAll('stack') .select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets')) - .whereRef('stacked.stackId', '=', 'asset_stack.id') - .whereRef('stacked.id', '!=', 'asset_stack.primaryAssetId') + .whereRef('stacked.stackId', '=', 'stack.id') + .whereRef('stacked.id', '!=', 'stack.primaryAssetId') .where('stacked.deletedAt', 'is', null) .where('stacked.visibility', '=', AssetVisibility.TIMELINE) - .groupBy('asset_stack.id') + .groupBy('stack.id') .as('stacked_assets'), - (join) => join.on('asset_stack.id', 'is not', null), + (join) => join.on('stack.id', 'is not', null), ) .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo().as('stack')), ), @@ -383,11 +383,11 @@ export class AssetRepository { if (ids.length === 0) { return; } - await this.db.updateTable('assets').set(options).where('id', '=', anyUuid(ids)).execute(); + await this.db.updateTable('asset').set(options).where('id', '=', anyUuid(ids)).execute(); } async updateByLibraryId(libraryId: string, options: Updateable): Promise { - await this.db.updateTable('assets').set(options).where('libraryId', '=', asUuid(libraryId)).execute(); + await this.db.updateTable('asset').set(options).where('libraryId', '=', asUuid(libraryId)).execute(); } async update(asset: Updateable & { id: string }) { @@ -395,9 +395,9 @@ export class AssetRepository { delete value.id; if (!isEmpty(value)) { return this.db - .with('assets', (qb) => qb.updateTable('assets').set(asset).where('id', '=', asUuid(asset.id)).returningAll()) - .selectFrom('assets') - .selectAll('assets') + .with('asset', (qb) => qb.updateTable('asset').set(asset).where('id', '=', asUuid(asset.id)).returningAll()) + .selectFrom('asset') + .selectAll('asset') .$call(withExif) .$call((qb) => qb.select(withFacesAndPeople)) .executeTakeFirst(); @@ -407,14 +407,14 @@ export class AssetRepository { } async remove(asset: { id: string }): Promise { - await this.db.deleteFrom('assets').where('id', '=', asUuid(asset.id)).execute(); + await this.db.deleteFrom('asset').where('id', '=', asUuid(asset.id)).execute(); } @GenerateSql({ params: [{ ownerId: DummyValue.UUID, libraryId: DummyValue.UUID, checksum: DummyValue.BUFFER }] }) getByChecksum({ ownerId, libraryId, checksum }: AssetGetByChecksumOptions) { return this.db - .selectFrom('assets') - .selectAll('assets') + .selectFrom('asset') + .selectAll('asset') .where('ownerId', '=', asUuid(ownerId)) .where('checksum', '=', checksum) .$call((qb) => (libraryId ? qb.where('libraryId', '=', asUuid(libraryId)) : qb.where('libraryId', 'is', null))) @@ -425,7 +425,7 @@ export class AssetRepository { @GenerateSql({ params: [DummyValue.UUID, [DummyValue.BUFFER]] }) getByChecksums(userId: string, checksums: Buffer[]) { return this.db - .selectFrom('assets') + .selectFrom('asset') .select(['id', 'checksum', 'deletedAt']) .where('ownerId', '=', asUuid(userId)) .where('checksum', 'in', checksums) @@ -435,7 +435,7 @@ export class AssetRepository { @GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] }) async getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise { const asset = await this.db - .selectFrom('assets') + .selectFrom('asset') .select('id') .where('ownerId', '=', asUuid(ownerId)) .where('checksum', '=', checksum) @@ -449,37 +449,37 @@ export class AssetRepository { findLivePhotoMatch(options: LivePhotoSearchOptions) { const { ownerId, otherAssetId, livePhotoCID, type } = options; return this.db - .selectFrom('assets') - .select(['assets.id', 'assets.ownerId']) - .innerJoin('exif', 'assets.id', 'exif.assetId') + .selectFrom('asset') + .select(['asset.id', 'asset.ownerId']) + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') .where('id', '!=', asUuid(otherAssetId)) .where('ownerId', '=', asUuid(ownerId)) .where('type', '=', type) - .where('exif.livePhotoCID', '=', livePhotoCID) + .where('asset_exif.livePhotoCID', '=', livePhotoCID) .limit(1) .executeTakeFirst(); } getStatistics(ownerId: string, { visibility, isFavorite, isTrashed }: AssetStatsOptions): Promise { return this.db - .selectFrom('assets') + .selectFrom('asset') .select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.AUDIO).as(AssetType.AUDIO)) .select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.IMAGE).as(AssetType.IMAGE)) .select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.VIDEO).as(AssetType.VIDEO)) .select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.OTHER).as(AssetType.OTHER)) .where('ownerId', '=', asUuid(ownerId)) .$if(visibility === undefined, withDefaultVisibility) - .$if(!!visibility, (qb) => qb.where('assets.visibility', '=', visibility!)) + .$if(!!visibility, (qb) => qb.where('asset.visibility', '=', visibility!)) .$if(isFavorite !== undefined, (qb) => qb.where('isFavorite', '=', isFavorite!)) - .$if(!!isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED)) + .$if(!!isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.DELETED)) .where('deletedAt', isTrashed ? 'is not' : 'is', null) .executeTakeFirstOrThrow(); } getRandom(userIds: string[], take: number) { return this.db - .selectFrom('assets') - .selectAll('assets') + .selectFrom('asset') + .selectAll('asset') .$call(withExif) .$call(withDefaultVisibility) .where('ownerId', '=', anyUuid(userIds)) @@ -492,38 +492,36 @@ export class AssetRepository { @GenerateSql({ params: [{}] }) async getTimeBuckets(options: TimeBucketOptions): Promise { return this.db - .with('assets', (qb) => + .with('asset', (qb) => qb - .selectFrom('assets') + .selectFrom('asset') .select(truncatedDate().as('timeBucket')) - .$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED)) - .where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null) + .$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.DELETED)) + .where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null) .$if(options.visibility === undefined, withDefaultVisibility) - .$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!)) + .$if(!!options.visibility, (qb) => qb.where('asset.visibility', '=', options.visibility!)) .$if(!!options.albumId, (qb) => qb - .innerJoin('albums_assets_assets', 'assets.id', 'albums_assets_assets.assetsId') - .where('albums_assets_assets.albumsId', '=', asUuid(options.albumId!)), + .innerJoin('album_asset', 'asset.id', 'album_asset.assetsId') + .where('album_asset.albumsId', '=', asUuid(options.albumId!)), ) .$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!])) .$if(!!options.withStacked, (qb) => qb - .leftJoin('asset_stack', (join) => - join - .onRef('asset_stack.id', '=', 'assets.stackId') - .onRef('asset_stack.primaryAssetId', '=', 'assets.id'), + .leftJoin('stack', (join) => + join.onRef('stack.id', '=', 'asset.stackId').onRef('stack.primaryAssetId', '=', 'asset.id'), ) - .where((eb) => eb.or([eb('assets.stackId', 'is', null), eb(eb.table('asset_stack'), 'is not', null)])), + .where((eb) => eb.or([eb('asset.stackId', 'is', null), eb(eb.table('stack'), 'is not', null)])), ) - .$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!))) - .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) - .$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!)) + .$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!))) + .$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!)) + .$if(!!options.assetType, (qb) => qb.where('asset.type', '=', options.assetType!)) .$if(options.isDuplicate !== undefined, (qb) => - qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null), + qb.where('asset.duplicateId', options.isDuplicate ? 'is not' : 'is', null), ) .$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)), ) - .selectFrom('assets') + .selectFrom('asset') .select(sql`("timeBucket" AT TIME ZONE 'UTC')::date::text`.as('timeBucket')) .select((eb) => eb.fn.countAll().as('count')) .groupBy('timeBucket') @@ -538,75 +536,75 @@ export class AssetRepository { const query = this.db .with('cte', (qb) => qb - .selectFrom('assets') - .innerJoin('exif', 'assets.id', 'exif.assetId') + .selectFrom('asset') + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') .select((eb) => [ - 'assets.duration', - 'assets.id', - 'assets.visibility', - 'assets.isFavorite', - sql`assets.type = 'IMAGE'`.as('isImage'), - sql`assets."deletedAt" is not null`.as('isTrashed'), - 'assets.livePhotoVideoId', - sql`extract(epoch from (assets."localDateTime" - assets."fileCreatedAt" at time zone 'UTC'))::real / 3600`.as( + 'asset.duration', + 'asset.id', + 'asset.visibility', + 'asset.isFavorite', + sql`asset.type = 'IMAGE'`.as('isImage'), + sql`asset."deletedAt" is not null`.as('isTrashed'), + 'asset.livePhotoVideoId', + sql`extract(epoch from (asset."localDateTime" - asset."fileCreatedAt" at time zone 'UTC'))::real / 3600`.as( 'localOffsetHours', ), - 'assets.ownerId', - 'assets.status', - sql`assets."fileCreatedAt" at time zone 'utc'`.as('fileCreatedAt'), - eb.fn('encode', ['assets.thumbhash', sql.lit('base64')]).as('thumbhash'), - 'exif.city', - 'exif.country', - 'exif.projectionType', + 'asset.ownerId', + 'asset.status', + sql`asset."fileCreatedAt" at time zone 'utc'`.as('fileCreatedAt'), + eb.fn('encode', ['asset.thumbhash', sql.lit('base64')]).as('thumbhash'), + 'asset_exif.city', + 'asset_exif.country', + 'asset_exif.projectionType', eb.fn .coalesce( eb .case() - .when(sql`exif."exifImageHeight" = 0 or exif."exifImageWidth" = 0`) + .when(sql`asset_exif."exifImageHeight" = 0 or asset_exif."exifImageWidth" = 0`) .then(eb.lit(1)) - .when('exif.orientation', 'in', sql`('5', '6', '7', '8', '-90', '90')`) - .then(sql`round(exif."exifImageHeight"::numeric / exif."exifImageWidth"::numeric, 3)`) - .else(sql`round(exif."exifImageWidth"::numeric / exif."exifImageHeight"::numeric, 3)`) + .when('asset_exif.orientation', 'in', sql`('5', '6', '7', '8', '-90', '90')`) + .then(sql`round(asset_exif."exifImageHeight"::numeric / asset_exif."exifImageWidth"::numeric, 3)`) + .else(sql`round(asset_exif."exifImageWidth"::numeric / asset_exif."exifImageHeight"::numeric, 3)`) .end(), eb.lit(1), ) .as('ratio'), ]) - .where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null) + .where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null) .$if(options.visibility == undefined, withDefaultVisibility) - .$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!)) + .$if(!!options.visibility, (qb) => qb.where('asset.visibility', '=', options.visibility!)) .where(truncatedDate(), '=', timeBucket.replace(/^[+-]/, '')) .$if(!!options.albumId, (qb) => qb.where((eb) => eb.exists( eb - .selectFrom('albums_assets_assets') - .whereRef('albums_assets_assets.assetsId', '=', 'assets.id') - .where('albums_assets_assets.albumsId', '=', asUuid(options.albumId!)), + .selectFrom('album_asset') + .whereRef('album_asset.assetsId', '=', 'asset.id') + .where('album_asset.albumsId', '=', asUuid(options.albumId!)), ), ), ) .$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!])) - .$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!))) - .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) + .$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!))) + .$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!)) .$if(!!options.withStacked, (qb) => qb .where((eb) => eb.not( eb.exists( eb - .selectFrom('asset_stack') - .whereRef('asset_stack.id', '=', 'assets.stackId') - .whereRef('asset_stack.primaryAssetId', '!=', 'assets.id'), + .selectFrom('stack') + .whereRef('stack.id', '=', 'asset.stackId') + .whereRef('stack.primaryAssetId', '!=', 'asset.id'), ), ), ) .leftJoinLateral( (eb) => eb - .selectFrom('assets as stacked') + .selectFrom('asset as stacked') .select(sql`array[stacked."stackId"::text, count('stacked')::text]`.as('stack')) - .whereRef('stacked.stackId', '=', 'assets.stackId') + .whereRef('stacked.stackId', '=', 'asset.stackId') .where('stacked.deletedAt', 'is', null) .where('stacked.visibility', '=', AssetVisibility.TIMELINE) .groupBy('stacked.stackId') @@ -615,13 +613,13 @@ export class AssetRepository { ) .select('stack'), ) - .$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!)) + .$if(!!options.assetType, (qb) => qb.where('asset.type', '=', options.assetType!)) .$if(options.isDuplicate !== undefined, (qb) => - qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null), + qb.where('asset.duplicateId', options.isDuplicate ? 'is not' : 'is', null), ) - .$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED)) + .$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.DELETED)) .$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)) - .orderBy('assets.fileCreatedAt', options.order ?? 'desc'), + .orderBy('asset.fileCreatedAt', options.order ?? 'desc'), ) .with('agg', (qb) => qb @@ -660,17 +658,17 @@ export class AssetRepository { const items = await this.db .with('cities', (qb) => qb - .selectFrom('exif') + .selectFrom('asset_exif') .select('city') .where('city', 'is not', null) .groupBy('city') .having((eb) => eb.fn('count', [eb.ref('assetId')]), '>=', minAssetsPerField), ) - .selectFrom('assets') - .innerJoin('exif', 'assets.id', 'exif.assetId') - .innerJoin('cities', 'exif.city', 'cities.city') - .distinctOn('exif.city') - .select(['assetId as data', 'exif.city as value']) + .selectFrom('asset') + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .innerJoin('cities', 'asset_exif.city', 'cities.city') + .distinctOn('asset_exif.city') + .select(['assetId as data', 'asset_exif.city as value']) .$narrowType<{ value: NotNull }>() .where('ownerId', '=', asUuid(ownerId)) .where('visibility', '=', AssetVisibility.TIMELINE) @@ -695,27 +693,27 @@ export class AssetRepository { getAllForUserFullSync(options: AssetFullSyncOptions) { const { ownerId, lastId, updatedUntil, limit } = options; return this.db - .selectFrom('assets') - .selectAll('assets') + .selectFrom('asset') + .selectAll('asset') .$call(withExif) - .leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId') + .leftJoin('stack', 'stack.id', 'asset.stackId') .leftJoinLateral( (eb) => eb - .selectFrom('assets as stacked') - .selectAll('asset_stack') + .selectFrom('asset as stacked') + .selectAll('stack') .select((eb) => eb.fn.count(eb.table('stacked')).as('assetCount')) - .whereRef('stacked.stackId', '=', 'asset_stack.id') - .groupBy('asset_stack.id') + .whereRef('stacked.stackId', '=', 'stack.id') + .groupBy('stack.id') .as('stacked_assets'), - (join) => join.on('asset_stack.id', 'is not', null), + (join) => join.on('stack.id', 'is not', null), ) .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo().as('stack')) - .where('assets.ownerId', '=', asUuid(ownerId)) - .where('assets.visibility', '!=', AssetVisibility.HIDDEN) - .where('assets.updatedAt', '<=', updatedUntil) - .$if(!!lastId, (qb) => qb.where('assets.id', '>', lastId!)) - .orderBy('assets.id') + .where('asset.ownerId', '=', asUuid(ownerId)) + .where('asset.visibility', '!=', AssetVisibility.HIDDEN) + .where('asset.updatedAt', '<=', updatedUntil) + .$if(!!lastId, (qb) => qb.where('asset.id', '>', lastId!)) + .orderBy('asset.id') .limit(limit) .execute(); } @@ -723,25 +721,25 @@ export class AssetRepository { @GenerateSql({ params: [{ userIds: [DummyValue.UUID], updatedAfter: DummyValue.DATE, limit: 100 }] }) async getChangedDeltaSync(options: AssetDeltaSyncOptions) { return this.db - .selectFrom('assets') - .selectAll('assets') + .selectFrom('asset') + .selectAll('asset') .$call(withExif) - .leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId') + .leftJoin('stack', 'stack.id', 'asset.stackId') .leftJoinLateral( (eb) => eb - .selectFrom('assets as stacked') - .selectAll('asset_stack') + .selectFrom('asset as stacked') + .selectAll('stack') .select((eb) => eb.fn.count(eb.table('stacked')).as('assetCount')) - .whereRef('stacked.stackId', '=', 'asset_stack.id') - .groupBy('asset_stack.id') + .whereRef('stacked.stackId', '=', 'stack.id') + .groupBy('stack.id') .as('stacked_assets'), - (join) => join.on('asset_stack.id', 'is not', null), + (join) => join.on('stack.id', 'is not', null), ) .select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo()).as('stack')) - .where('assets.ownerId', '=', anyUuid(options.userIds)) - .where('assets.visibility', '!=', AssetVisibility.HIDDEN) - .where('assets.updatedAt', '>', options.updatedAfter) + .where('asset.ownerId', '=', anyUuid(options.userIds)) + .where('asset.visibility', '!=', AssetVisibility.HIDDEN) + .where('asset.updatedAt', '>', options.updatedAfter) .limit(options.limit) .execute(); } @@ -749,7 +747,7 @@ export class AssetRepository { async upsertFile(file: Pick, 'assetId' | 'path' | 'type'>): Promise { const value = { ...file, assetId: asUuid(file.assetId) }; await this.db - .insertInto('asset_files') + .insertInto('asset_file') .values(value) .onConflict((oc) => oc.columns(['assetId', 'type']).doUpdateSet((eb) => ({ @@ -766,7 +764,7 @@ export class AssetRepository { const values = files.map((row) => ({ ...row, assetId: asUuid(row.assetId) })); await this.db - .insertInto('asset_files') + .insertInto('asset_file') .values(values) .onConflict((oc) => oc.columns(['assetId', 'type']).doUpdateSet((eb) => ({ @@ -782,7 +780,7 @@ export class AssetRepository { } await this.db - .deleteFrom('asset_files') + .deleteFrom('asset_file') .where('id', '=', anyUuid(files.map((file) => file.id))) .execute(); } @@ -797,7 +795,7 @@ export class AssetRepository { const exclusions = exclusionPatterns.map((pattern) => globToSqlPattern(pattern)); return this.db - .updateTable('assets') + .updateTable('asset') .set({ isOffline: true, deletedAt: new Date(), @@ -823,9 +821,9 @@ export class AssetRepository { eb.not( eb.exists( this.db - .selectFrom('assets') + .selectFrom('asset') .select('originalPath') - .whereRef('assets.originalPath', '=', eb.ref('path')) + .whereRef('asset.originalPath', '=', eb.ref('path')) .where('libraryId', '=', asUuid(libraryId)) .where('isExternal', '=', true), ), @@ -838,7 +836,7 @@ export class AssetRepository { async getLibraryAssetCount(libraryId: string): Promise { const { count } = await this.db - .selectFrom('assets') + .selectFrom('asset') .select((eb) => eb.fn.countAll().as('count')) .where('libraryId', '=', asUuid(libraryId)) .executeTakeFirstOrThrow(); diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index 9a0a24f70f..dbb57bb141 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -334,6 +334,10 @@ export class ConfigRepository { return cached; } + isDev() { + return this.getEnv().environment === ImmichEnvironment.DEVELOPMENT; + } + getWorker() { return this.worker; } diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index b611ff6c7f..b1aefe19f8 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -406,6 +406,7 @@ export class DatabaseRepository { const migrator = new Migrator({ db: this.db, migrationLockTableName: 'kysely_migrations_lock', + allowUnorderedMigrations: this.configRepository.isDev(), migrationTableName: 'kysely_migrations', provider: new FileMigrationProvider({ fs: { readdir }, diff --git a/server/src/repositories/download.repository.ts b/server/src/repositories/download.repository.ts index aef00f1ed1..5645ca1217 100644 --- a/server/src/repositories/download.repository.ts +++ b/server/src/repositories/download.repository.ts @@ -7,34 +7,34 @@ import { anyUuid } from 'src/utils/database'; const builder = (db: Kysely) => db - .selectFrom('assets') - .innerJoin('exif', 'assetId', 'id') - .select(['assets.id', 'assets.livePhotoVideoId', 'exif.fileSizeInByte as size']) - .where('assets.deletedAt', 'is', null); + .selectFrom('asset') + .innerJoin('asset_exif', 'assetId', 'id') + .select(['asset.id', 'asset.livePhotoVideoId', 'asset_exif.fileSizeInByte as size']) + .where('asset.deletedAt', 'is', null); @Injectable() export class DownloadRepository { constructor(@InjectKysely() private db: Kysely) {} downloadAssetIds(ids: string[]) { - return builder(this.db).where('assets.id', '=', anyUuid(ids)).stream(); + return builder(this.db).where('asset.id', '=', anyUuid(ids)).stream(); } downloadMotionAssetIds(ids: string[]) { - return builder(this.db).select(['assets.originalPath']).where('assets.id', '=', anyUuid(ids)).stream(); + return builder(this.db).select(['asset.originalPath']).where('asset.id', '=', anyUuid(ids)).stream(); } downloadAlbumId(albumId: string) { return builder(this.db) - .innerJoin('albums_assets_assets', 'assets.id', 'albums_assets_assets.assetsId') - .where('albums_assets_assets.albumsId', '=', albumId) + .innerJoin('album_asset', 'asset.id', 'album_asset.assetsId') + .where('album_asset.albumsId', '=', albumId) .stream(); } downloadUserId(userId: string) { return builder(this.db) - .where('assets.ownerId', '=', userId) - .where('assets.visibility', '!=', AssetVisibility.HIDDEN) + .where('asset.ownerId', '=', userId) + .where('asset.visibility', '!=', AssetVisibility.HIDDEN) .stream(); } } diff --git a/server/src/repositories/duplicate.repository.ts b/server/src/repositories/duplicate.repository.ts index d654e1844e..ac9e5798e5 100644 --- a/server/src/repositories/duplicate.repository.ts +++ b/server/src/repositories/duplicate.repository.ts @@ -32,28 +32,28 @@ export class DuplicateRepository { this.db .with('duplicates', (qb) => qb - .selectFrom('assets') + .selectFrom('asset') .$call(withDefaultVisibility) .leftJoinLateral( (qb) => qb - .selectFrom('exif') - .selectAll('assets') - .select((eb) => eb.table('exif').as('exifInfo')) - .whereRef('exif.assetId', '=', 'assets.id') - .as('asset'), + .selectFrom('asset_exif') + .selectAll('asset') + .select((eb) => eb.table('asset_exif').as('exifInfo')) + .whereRef('asset_exif.assetId', '=', 'asset.id') + .as('asset2'), (join) => join.onTrue(), ) - .select('assets.duplicateId') + .select('asset.duplicateId') .select((eb) => - eb.fn.jsonAgg('asset').orderBy('assets.localDateTime', 'asc').$castTo().as('assets'), + eb.fn.jsonAgg('asset2').orderBy('asset.localDateTime', 'asc').$castTo().as('assets'), ) - .where('assets.ownerId', '=', asUuid(userId)) - .where('assets.duplicateId', 'is not', null) + .where('asset.ownerId', '=', asUuid(userId)) + .where('asset.duplicateId', 'is not', null) .$narrowType<{ duplicateId: NotNull }>() - .where('assets.deletedAt', 'is', null) - .where('assets.stackId', 'is', null) - .groupBy('assets.duplicateId'), + .where('asset.deletedAt', 'is', null) + .where('asset.stackId', 'is', null) + .groupBy('asset.duplicateId'), ) .with('unique', (qb) => qb @@ -63,10 +63,10 @@ export class DuplicateRepository { ) .with('removed_unique', (qb) => qb - .updateTable('assets') + .updateTable('asset') .set({ duplicateId: null }) .from('unique') - .whereRef('assets.duplicateId', '=', 'unique.duplicateId'), + .whereRef('asset.duplicateId', '=', 'unique.duplicateId'), ) .selectFrom('duplicates') .selectAll() @@ -81,7 +81,7 @@ export class DuplicateRepository { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) async delete(userId: string, id: string): Promise { await this.db - .updateTable('assets') + .updateTable('asset') .set({ duplicateId: null }) .where('ownerId', '=', userId) .where('duplicateId', '=', id) @@ -96,7 +96,7 @@ export class DuplicateRepository { } await this.db - .updateTable('assets') + .updateTable('asset') .set({ duplicateId: null }) .where('ownerId', '=', userId) .where('duplicateId', 'in', ids) @@ -120,19 +120,19 @@ export class DuplicateRepository { return await trx .with('cte', (qb) => qb - .selectFrom('assets') + .selectFrom('asset') .$call(withDefaultVisibility) .select([ - 'assets.id as assetId', - 'assets.duplicateId', + 'asset.id as assetId', + 'asset.duplicateId', sql`smart_search.embedding <=> ${embedding}`.as('distance'), ]) - .innerJoin('smart_search', 'assets.id', 'smart_search.assetId') - .where('assets.ownerId', '=', anyUuid(userIds)) - .where('assets.deletedAt', 'is', null) - .where('assets.type', '=', type) - .where('assets.id', '!=', asUuid(assetId)) - .where('assets.stackId', 'is', null) + .innerJoin('smart_search', 'asset.id', 'smart_search.assetId') + .where('asset.ownerId', '=', anyUuid(userIds)) + .where('asset.deletedAt', 'is', null) + .where('asset.type', '=', type) + .where('asset.id', '!=', asUuid(assetId)) + .where('asset.stackId', 'is', null) .orderBy('distance') .limit(64), ) @@ -148,7 +148,7 @@ export class DuplicateRepository { }) async merge(options: DuplicateMergeOptions): Promise { await this.db - .updateTable('assets') + .updateTable('asset') .set({ duplicateId: options.targetId }) .where((eb) => eb.or([eb('duplicateId', '=', anyUuid(options.sourceIds)), eb('id', '=', anyUuid(options.assetIds))]), diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index 307b8b0ef4..41966450ee 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -16,6 +16,7 @@ import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { NotificationDto } from 'src/dtos/notification.dto'; import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; +import { SyncAssetExifV1, SyncAssetV1 } from 'src/dtos/sync.dto'; import { ImmichWorker, MetadataKey, QueueName } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -109,6 +110,8 @@ export interface ClientEventMap { on_new_release: [ReleaseNotification]; on_notification: [NotificationDto]; on_session_delete: [string]; + + AssetUploadReadyV1: [{ asset: SyncAssetV1; exif: SyncAssetExifV1 }]; } export type EventItem = { diff --git a/server/src/repositories/library.repository.ts b/server/src/repositories/library.repository.ts index 704f809689..9f26191cfb 100644 --- a/server/src/repositories/library.repository.ts +++ b/server/src/repositories/library.repository.ts @@ -21,50 +21,50 @@ export class LibraryRepository { @GenerateSql({ params: [DummyValue.UUID] }) get(id: string, withDeleted = false) { return this.db - .selectFrom('libraries') - .selectAll('libraries') - .where('libraries.id', '=', id) - .$if(!withDeleted, (qb) => qb.where('libraries.deletedAt', 'is', null)) + .selectFrom('library') + .selectAll('library') + .where('library.id', '=', id) + .$if(!withDeleted, (qb) => qb.where('library.deletedAt', 'is', null)) .executeTakeFirst(); } @GenerateSql({ params: [] }) getAll(withDeleted = false) { return this.db - .selectFrom('libraries') - .selectAll('libraries') + .selectFrom('library') + .selectAll('library') .orderBy('createdAt', 'asc') - .$if(!withDeleted, (qb) => qb.where('libraries.deletedAt', 'is', null)) + .$if(!withDeleted, (qb) => qb.where('library.deletedAt', 'is', null)) .execute(); } @GenerateSql() getAllDeleted() { return this.db - .selectFrom('libraries') - .selectAll('libraries') - .where('libraries.deletedAt', 'is not', null) + .selectFrom('library') + .selectAll('library') + .where('library.deletedAt', 'is not', null) .orderBy('createdAt', 'asc') .execute(); } create(library: Insertable) { - return this.db.insertInto('libraries').values(library).returningAll().executeTakeFirstOrThrow(); + return this.db.insertInto('library').values(library).returningAll().executeTakeFirstOrThrow(); } async delete(id: string) { - await this.db.deleteFrom('libraries').where('libraries.id', '=', id).execute(); + await this.db.deleteFrom('library').where('library.id', '=', id).execute(); } async softDelete(id: string) { - await this.db.updateTable('libraries').set({ deletedAt: new Date() }).where('libraries.id', '=', id).execute(); + await this.db.updateTable('library').set({ deletedAt: new Date() }).where('library.id', '=', id).execute(); } update(id: string, library: Updateable) { return this.db - .updateTable('libraries') + .updateTable('library') .set(library) - .where('libraries.id', '=', id) + .where('library.id', '=', id) .returningAll() .executeTakeFirstOrThrow(); } @@ -72,14 +72,14 @@ export class LibraryRepository { @GenerateSql({ params: [DummyValue.UUID] }) async getStatistics(id: string): Promise { const stats = await this.db - .selectFrom('libraries') - .innerJoin('assets', 'assets.libraryId', 'libraries.id') - .leftJoin('exif', 'exif.assetId', 'assets.id') + .selectFrom('library') + .innerJoin('asset', 'asset.libraryId', 'library.id') + .leftJoin('asset_exif', 'asset_exif.assetId', 'asset.id') .select((eb) => eb.fn .countAll() .filterWhere((eb) => - eb.and([eb('assets.type', '=', AssetType.IMAGE), eb('assets.visibility', '!=', AssetVisibility.HIDDEN)]), + eb.and([eb('asset.type', '=', AssetType.IMAGE), eb('asset.visibility', '!=', AssetVisibility.HIDDEN)]), ) .as('photos'), ) @@ -87,25 +87,25 @@ export class LibraryRepository { eb.fn .countAll() .filterWhere((eb) => - eb.and([eb('assets.type', '=', AssetType.VIDEO), eb('assets.visibility', '!=', AssetVisibility.HIDDEN)]), + eb.and([eb('asset.type', '=', AssetType.VIDEO), eb('asset.visibility', '!=', AssetVisibility.HIDDEN)]), ) .as('videos'), ) - .select((eb) => eb.fn.coalesce((eb) => eb.fn.sum('exif.fileSizeInByte'), eb.val(0)).as('usage')) - .groupBy('libraries.id') - .where('libraries.id', '=', id) + .select((eb) => eb.fn.coalesce((eb) => eb.fn.sum('asset_exif.fileSizeInByte'), eb.val(0)).as('usage')) + .groupBy('library.id') + .where('library.id', '=', id) .executeTakeFirst(); // possibly a new library with 0 assets if (!stats) { const zero = sql`0::int`; return this.db - .selectFrom('libraries') + .selectFrom('library') .select(zero.as('photos')) .select(zero.as('videos')) .select(zero.as('usage')) .select(zero.as('total')) - .where('libraries.id', '=', id) + .where('library.id', '=', id) .executeTakeFirst(); } @@ -118,6 +118,6 @@ export class LibraryRepository { } streamAssetIds(libraryId: string) { - return this.db.selectFrom('assets').select(['id']).where('libraryId', '=', libraryId).stream(); + return this.db.selectFrom('asset').select(['id']).where('libraryId', '=', libraryId).stream(); } } diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index 36c45bcb71..ff2e28b8d7 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -83,25 +83,32 @@ export class MapRepository { { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore }: MapMarkerSearchOptions = {}, ) { return this.db - .selectFrom('assets') - .innerJoin('exif', (builder) => + .selectFrom('asset') + .innerJoin('asset_exif', (builder) => builder - .onRef('assets.id', '=', 'exif.assetId') - .on('exif.latitude', 'is not', null) - .on('exif.longitude', 'is not', null), + .onRef('asset.id', '=', 'asset_exif.assetId') + .on('asset_exif.latitude', 'is not', null) + .on('asset_exif.longitude', 'is not', null), ) - .select(['id', 'exif.latitude as lat', 'exif.longitude as lon', 'exif.city', 'exif.state', 'exif.country']) + .select([ + 'id', + 'asset_exif.latitude as lat', + 'asset_exif.longitude as lon', + 'asset_exif.city', + 'asset_exif.state', + 'asset_exif.country', + ]) .$narrowType<{ lat: NotNull; lon: NotNull }>() .$if(isArchived === true, (qb) => qb.where((eb) => eb.or([ - eb('assets.visibility', '=', AssetVisibility.TIMELINE), - eb('assets.visibility', '=', AssetVisibility.ARCHIVE), + eb('asset.visibility', '=', AssetVisibility.TIMELINE), + eb('asset.visibility', '=', AssetVisibility.ARCHIVE), ]), ), ) .$if(isArchived === false || isArchived === undefined, (qb) => - qb.where('assets.visibility', '=', AssetVisibility.TIMELINE), + qb.where('asset.visibility', '=', AssetVisibility.TIMELINE), ) .$if(isFavorite !== undefined, (q) => q.where('isFavorite', '=', isFavorite!)) .$if(fileCreatedAfter !== undefined, (q) => q.where('fileCreatedAt', '>=', fileCreatedAfter!)) @@ -118,9 +125,9 @@ export class MapRepository { expression.push( eb.exists((eb) => eb - .selectFrom('albums_assets_assets') - .whereRef('assets.id', '=', 'albums_assets_assets.assetsId') - .where('albums_assets_assets.albumsId', 'in', albumIds), + .selectFrom('album_asset') + .whereRef('asset.id', '=', 'album_asset.assetsId') + .where('album_asset.albumsId', 'in', albumIds), ), ); } diff --git a/server/src/repositories/memory.repository.ts b/server/src/repositories/memory.repository.ts index 65975a232d..7cf03508be 100644 --- a/server/src/repositories/memory.repository.ts +++ b/server/src/repositories/memory.repository.ts @@ -16,14 +16,14 @@ export class MemoryRepository implements IBulkAsset { async cleanup() { await this.db - .deleteFrom('memories_assets_assets') - .using('assets') - .whereRef('memories_assets_assets.assetsId', '=', 'assets.id') - .where('assets.visibility', '!=', AssetVisibility.TIMELINE) + .deleteFrom('memory_asset') + .using('asset') + .whereRef('memory_asset.assetsId', '=', 'asset.id') + .where('asset.visibility', '!=', AssetVisibility.TIMELINE) .execute(); return this.db - .deleteFrom('memories') + .deleteFrom('memory') .where('createdAt', '<', DateTime.now().minus({ days: 30 }).toJSDate()) .where('isSaved', '=', false) .execute(); @@ -31,7 +31,7 @@ export class MemoryRepository implements IBulkAsset { searchBuilder(ownerId: string, dto: MemorySearchDto) { return this.db - .selectFrom('memories') + .selectFrom('memory') .$if(dto.isSaved !== undefined, (qb) => qb.where('isSaved', '=', dto.isSaved!)) .$if(dto.type !== undefined, (qb) => qb.where('type', '=', dto.type!)) .$if(dto.for !== undefined, (qb) => @@ -62,16 +62,16 @@ export class MemoryRepository implements IBulkAsset { .select((eb) => jsonArrayFrom( eb - .selectFrom('assets') - .selectAll('assets') - .innerJoin('memories_assets_assets', 'assets.id', 'memories_assets_assets.assetsId') - .whereRef('memories_assets_assets.memoriesId', '=', 'memories.id') - .orderBy('assets.fileCreatedAt', 'asc') - .where('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) - .where('assets.deletedAt', 'is', null), + .selectFrom('asset') + .selectAll('asset') + .innerJoin('memory_asset', 'asset.id', 'memory_asset.assetsId') + .whereRef('memory_asset.memoriesId', '=', 'memory.id') + .orderBy('asset.fileCreatedAt', 'asc') + .where('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) + .where('asset.deletedAt', 'is', null), ).as('assets'), ) - .selectAll('memories') + .selectAll('memory') .orderBy('memoryAt', 'desc') .execute(); } @@ -83,11 +83,11 @@ export class MemoryRepository implements IBulkAsset { async create(memory: Insertable, assetIds: Set) { const id = await this.db.transaction().execute(async (tx) => { - const { id } = await tx.insertInto('memories').values(memory).returning('id').executeTakeFirstOrThrow(); + const { id } = await tx.insertInto('memory').values(memory).returning('id').executeTakeFirstOrThrow(); if (assetIds.size > 0) { const values = [...assetIds].map((assetId) => ({ memoriesId: id, assetsId: assetId })); - await tx.insertInto('memories_assets_assets').values(values).execute(); + await tx.insertInto('memory_asset').values(values).execute(); } return id; @@ -98,13 +98,13 @@ export class MemoryRepository implements IBulkAsset { @GenerateSql({ params: [DummyValue.UUID, { ownerId: DummyValue.UUID, isSaved: true }] }) async update(id: string, memory: Updateable) { - await this.db.updateTable('memories').set(memory).where('id', '=', id).execute(); + await this.db.updateTable('memory').set(memory).where('id', '=', id).execute(); return this.getByIdBuilder(id).executeTakeFirstOrThrow(); } @GenerateSql({ params: [DummyValue.UUID] }) async delete(id: string) { - await this.db.deleteFrom('memories').where('id', '=', id).execute(); + await this.db.deleteFrom('memory').where('id', '=', id).execute(); } @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) @@ -115,7 +115,7 @@ export class MemoryRepository implements IBulkAsset { } const results = await this.db - .selectFrom('memories_assets_assets') + .selectFrom('memory_asset') .select(['assetsId']) .where('memoriesId', '=', id) .where('assetsId', 'in', assetIds) @@ -131,7 +131,7 @@ export class MemoryRepository implements IBulkAsset { } await this.db - .insertInto('memories_assets_assets') + .insertInto('memory_asset') .values(assetIds.map((assetId) => ({ memoriesId: id, assetsId: assetId }))) .execute(); } @@ -143,27 +143,23 @@ export class MemoryRepository implements IBulkAsset { return; } - await this.db - .deleteFrom('memories_assets_assets') - .where('memoriesId', '=', id) - .where('assetsId', 'in', assetIds) - .execute(); + await this.db.deleteFrom('memory_asset').where('memoriesId', '=', id).where('assetsId', 'in', assetIds).execute(); } private getByIdBuilder(id: string) { return this.db - .selectFrom('memories') - .selectAll('memories') + .selectFrom('memory') + .selectAll('memory') .select((eb) => jsonArrayFrom( eb - .selectFrom('assets') - .selectAll('assets') - .innerJoin('memories_assets_assets', 'assets.id', 'memories_assets_assets.assetsId') - .whereRef('memories_assets_assets.memoriesId', '=', 'memories.id') - .orderBy('assets.fileCreatedAt', 'asc') - .where('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) - .where('assets.deletedAt', 'is', null), + .selectFrom('asset') + .selectAll('asset') + .innerJoin('memory_asset', 'asset.id', 'memory_asset.assetsId') + .whereRef('memory_asset.memoriesId', '=', 'memory.id') + .orderBy('asset.fileCreatedAt', 'asc') + .where('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) + .where('asset.deletedAt', 'is', null), ).as('assets'), ) .where('id', '=', id) diff --git a/server/src/repositories/move.repository.ts b/server/src/repositories/move.repository.ts index 3538dd4627..e416a65249 100644 --- a/server/src/repositories/move.repository.ts +++ b/server/src/repositories/move.repository.ts @@ -45,7 +45,7 @@ export class MoveRepository { eb( 'move_history.entityId', 'not in', - eb.selectFrom('assets').select('id').whereRef('assets.id', '=', 'move_history.entityId'), + eb.selectFrom('asset').select('id').whereRef('asset.id', '=', 'move_history.entityId'), ), ) .where('move_history.pathType', '=', sql.lit(AssetPathType.ORIGINAL)) diff --git a/server/src/repositories/notification.repository.ts b/server/src/repositories/notification.repository.ts index bbbd059e42..e892403416 100644 --- a/server/src/repositories/notification.repository.ts +++ b/server/src/repositories/notification.repository.ts @@ -12,7 +12,7 @@ export class NotificationRepository { cleanup() { return this.db - .deleteFrom('notifications') + .deleteFrom('notification') .where((eb) => eb.or([ // remove soft-deleted notifications @@ -38,7 +38,7 @@ export class NotificationRepository { @GenerateSql({ params: [DummyValue.UUID, {}] }, { name: 'unread', params: [DummyValue.UUID, { unread: true }] }) search(userId: string, dto: NotificationSearchDto) { return this.db - .selectFrom('notifications') + .selectFrom('notification') .select(columns.notification) .where((qb) => qb.and({ @@ -56,7 +56,7 @@ export class NotificationRepository { create(notification: Insertable) { return this.db - .insertInto('notifications') + .insertInto('notification') .values(notification) .returning(columns.notification) .executeTakeFirstOrThrow(); @@ -64,7 +64,7 @@ export class NotificationRepository { get(id: string) { return this.db - .selectFrom('notifications') + .selectFrom('notification') .select(columns.notification) .where('id', '=', id) .where('deletedAt', 'is not', null) @@ -73,7 +73,7 @@ export class NotificationRepository { update(id: string, notification: Updateable) { return this.db - .updateTable('notifications') + .updateTable('notification') .set(notification) .where('deletedAt', 'is', null) .where('id', '=', id) @@ -82,12 +82,12 @@ export class NotificationRepository { } async updateAll(ids: string[], notification: Updateable) { - await this.db.updateTable('notifications').set(notification).where('id', 'in', ids).execute(); + await this.db.updateTable('notification').set(notification).where('id', 'in', ids).execute(); } async delete(id: string) { await this.db - .updateTable('notifications') + .updateTable('notification') .set({ deletedAt: DateTime.now().toJSDate() }) .where('id', '=', id) .execute(); @@ -95,7 +95,7 @@ export class NotificationRepository { async deleteAll(ids: string[]) { await this.db - .updateTable('notifications') + .updateTable('notification') .set({ deletedAt: DateTime.now().toJSDate() }) .where('id', 'in', ids) .execute(); diff --git a/server/src/repositories/partner.repository.ts b/server/src/repositories/partner.repository.ts index af6d5d69c4..e386386be8 100644 --- a/server/src/repositories/partner.repository.ts +++ b/server/src/repositories/partner.repository.ts @@ -17,15 +17,15 @@ export enum PartnerDirection { SharedWith = 'shared-with', } -const withSharedBy = (eb: ExpressionBuilder) => { +const withSharedBy = (eb: ExpressionBuilder) => { return jsonObjectFrom( - eb.selectFrom('users as sharedBy').select(columns.user).whereRef('sharedBy.id', '=', 'partners.sharedById'), + eb.selectFrom('user as sharedBy').select(columns.user).whereRef('sharedBy.id', '=', 'partner.sharedById'), ).as('sharedBy'); }; -const withSharedWith = (eb: ExpressionBuilder) => { +const withSharedWith = (eb: ExpressionBuilder) => { return jsonObjectFrom( - eb.selectFrom('users as sharedWith').select(columns.user).whereRef('sharedWith.id', '=', 'partners.sharedWithId'), + eb.selectFrom('user as sharedWith').select(columns.user).whereRef('sharedWith.id', '=', 'partner.sharedWithId'), ).as('sharedWith'); }; @@ -50,7 +50,7 @@ export class PartnerRepository { create(values: Insertable) { return this.db - .insertInto('partners') + .insertInto('partner') .values(values) .returningAll() .returning(withSharedBy) @@ -62,7 +62,7 @@ export class PartnerRepository { @GenerateSql({ params: [{ sharedWithId: DummyValue.UUID, sharedById: DummyValue.UUID }, { inTimeline: true }] }) update({ sharedWithId, sharedById }: PartnerIds, values: Updateable) { return this.db - .updateTable('partners') + .updateTable('partner') .set(values) .where('sharedWithId', '=', sharedWithId) .where('sharedById', '=', sharedById) @@ -76,7 +76,7 @@ export class PartnerRepository { @GenerateSql({ params: [{ sharedWithId: DummyValue.UUID, sharedById: DummyValue.UUID }] }) async remove({ sharedWithId, sharedById }: PartnerIds) { await this.db - .deleteFrom('partners') + .deleteFrom('partner') .where('sharedWithId', '=', sharedWithId) .where('sharedById', '=', sharedById) .execute(); @@ -84,14 +84,14 @@ export class PartnerRepository { private builder() { return this.db - .selectFrom('partners') - .innerJoin('users as sharedBy', (join) => - join.onRef('partners.sharedById', '=', 'sharedBy.id').on('sharedBy.deletedAt', 'is', null), + .selectFrom('partner') + .innerJoin('user as sharedBy', (join) => + join.onRef('partner.sharedById', '=', 'sharedBy.id').on('sharedBy.deletedAt', 'is', null), ) - .innerJoin('users as sharedWith', (join) => - join.onRef('partners.sharedWithId', '=', 'sharedWith.id').on('sharedWith.deletedAt', 'is', null), + .innerJoin('user as sharedWith', (join) => + join.onRef('partner.sharedWithId', '=', 'sharedWith.id').on('sharedWith.deletedAt', 'is', null), ) - .selectAll('partners') + .selectAll('partner') .select(withSharedBy) .select(withSharedWith); } diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 949d39747e..1885a196ff 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -62,21 +62,21 @@ export type UnassignFacesOptions = DeleteFacesOptions; export type SelectFaceOptions = (keyof Selectable)[]; -const withPerson = (eb: ExpressionBuilder) => { +const withPerson = (eb: ExpressionBuilder) => { return jsonObjectFrom( - eb.selectFrom('person').selectAll('person').whereRef('person.id', '=', 'asset_faces.personId'), + eb.selectFrom('person').selectAll('person').whereRef('person.id', '=', 'asset_face.personId'), ).as('person'); }; -const withAsset = (eb: ExpressionBuilder) => { - return jsonObjectFrom( - eb.selectFrom('assets').selectAll('assets').whereRef('assets.id', '=', 'asset_faces.assetId'), - ).as('asset'); +const withAsset = (eb: ExpressionBuilder) => { + return jsonObjectFrom(eb.selectFrom('asset').selectAll('asset').whereRef('asset.id', '=', 'asset_face.assetId')).as( + 'asset', + ); }; -const withFaceSearch = (eb: ExpressionBuilder) => { +const withFaceSearch = (eb: ExpressionBuilder) => { return jsonObjectFrom( - eb.selectFrom('face_search').selectAll('face_search').whereRef('face_search.faceId', '=', 'asset_faces.id'), + eb.selectFrom('face_search').selectAll('face_search').whereRef('face_search.faceId', '=', 'asset_face.id'), ).as('faceSearch'); }; @@ -87,10 +87,10 @@ export class PersonRepository { @GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] }) async reassignFaces({ oldPersonId, faceIds, newPersonId }: UpdateFacesData): Promise { const result = await this.db - .updateTable('asset_faces') + .updateTable('asset_face') .set({ personId: newPersonId }) - .$if(!!oldPersonId, (qb) => qb.where('asset_faces.personId', '=', oldPersonId!)) - .$if(!!faceIds, (qb) => qb.where('asset_faces.id', 'in', faceIds!)) + .$if(!!oldPersonId, (qb) => qb.where('asset_face.personId', '=', oldPersonId!)) + .$if(!!faceIds, (qb) => qb.where('asset_face.id', 'in', faceIds!)) .executeTakeFirst(); return Number(result.numChangedRows ?? 0); @@ -98,9 +98,9 @@ export class PersonRepository { async unassignFaces({ sourceType }: UnassignFacesOptions): Promise { await this.db - .updateTable('asset_faces') + .updateTable('asset_face') .set({ personId: null }) - .where('asset_faces.sourceType', '=', sourceType) + .where('asset_face.sourceType', '=', sourceType) .execute(); } @@ -115,18 +115,18 @@ export class PersonRepository { } async deleteFaces({ sourceType }: DeleteFacesOptions): Promise { - await this.db.deleteFrom('asset_faces').where('asset_faces.sourceType', '=', sourceType).execute(); + await this.db.deleteFrom('asset_face').where('asset_face.sourceType', '=', sourceType).execute(); } getAllFaces(options: GetAllFacesOptions = {}) { return this.db - .selectFrom('asset_faces') - .selectAll('asset_faces') - .$if(options.personId === null, (qb) => qb.where('asset_faces.personId', 'is', null)) - .$if(!!options.personId, (qb) => qb.where('asset_faces.personId', '=', options.personId!)) - .$if(!!options.sourceType, (qb) => qb.where('asset_faces.sourceType', '=', options.sourceType!)) - .$if(!!options.assetId, (qb) => qb.where('asset_faces.assetId', '=', options.assetId!)) - .where('asset_faces.deletedAt', 'is', null) + .selectFrom('asset_face') + .selectAll('asset_face') + .$if(options.personId === null, (qb) => qb.where('asset_face.personId', 'is', null)) + .$if(!!options.personId, (qb) => qb.where('asset_face.personId', '=', options.personId!)) + .$if(!!options.sourceType, (qb) => qb.where('asset_face.sourceType', '=', options.sourceType!)) + .$if(!!options.assetId, (qb) => qb.where('asset_face.assetId', '=', options.assetId!)) + .where('asset_face.deletedAt', 'is', null) .stream(); } @@ -147,21 +147,21 @@ export class PersonRepository { const items = await this.db .selectFrom('person') .selectAll('person') - .innerJoin('asset_faces', 'asset_faces.personId', 'person.id') - .innerJoin('assets', (join) => + .innerJoin('asset_face', 'asset_face.personId', 'person.id') + .innerJoin('asset', (join) => join - .onRef('asset_faces.assetId', '=', 'assets.id') - .on('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) - .on('assets.deletedAt', 'is', null), + .onRef('asset_face.assetId', '=', 'asset.id') + .on('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) + .on('asset.deletedAt', 'is', null), ) .where('person.ownerId', '=', userId) - .where('asset_faces.deletedAt', 'is', null) + .where('asset_face.deletedAt', 'is', null) .orderBy('person.isHidden', 'asc') .orderBy('person.isFavorite', 'desc') .having((eb) => eb.or([ eb('person.name', '!=', ''), - eb((innerEb) => innerEb.fn.count('asset_faces.assetId'), '>=', options?.minimumFaceCount || 1), + eb((innerEb) => innerEb.fn.count('asset_face.assetId'), '>=', options?.minimumFaceCount || 1), ]), ) .groupBy('person.id') @@ -185,7 +185,7 @@ export class PersonRepository { .$if(!options?.closestFaceAssetId, (qb) => qb .orderBy(sql`NULLIF(person.name, '') is null`, 'asc') - .orderBy((eb) => eb.fn.count('asset_faces.assetId'), 'desc') + .orderBy((eb) => eb.fn.count('asset_face.assetId'), 'desc') .orderBy(sql`NULLIF(person.name, '')`, (om) => om.asc().nullsLast()) .orderBy('person.createdAt'), ) @@ -202,9 +202,9 @@ export class PersonRepository { return this.db .selectFrom('person') .selectAll('person') - .leftJoin('asset_faces', 'asset_faces.personId', 'person.id') - .where('asset_faces.deletedAt', 'is', null) - .having((eb) => eb.fn.count('asset_faces.assetId'), '=', 0) + .leftJoin('asset_face', 'asset_face.personId', 'person.id') + .where('asset_face.deletedAt', 'is', null) + .having((eb) => eb.fn.count('asset_face.assetId'), '=', 0) .groupBy('person.id') .execute(); } @@ -212,12 +212,12 @@ export class PersonRepository { @GenerateSql({ params: [DummyValue.UUID] }) getFaces(assetId: string) { return this.db - .selectFrom('asset_faces') - .selectAll('asset_faces') + .selectFrom('asset_face') + .selectAll('asset_face') .select(withPerson) - .where('asset_faces.assetId', '=', assetId) - .where('asset_faces.deletedAt', 'is', null) - .orderBy('asset_faces.boundingBoxX1', 'asc') + .where('asset_face.assetId', '=', assetId) + .where('asset_face.deletedAt', 'is', null) + .orderBy('asset_face.boundingBoxX1', 'asc') .execute(); } @@ -225,30 +225,30 @@ export class PersonRepository { getFaceById(id: string) { // TODO return null instead of find or fail return this.db - .selectFrom('asset_faces') - .selectAll('asset_faces') + .selectFrom('asset_face') + .selectAll('asset_face') .select(withPerson) - .where('asset_faces.id', '=', id) - .where('asset_faces.deletedAt', 'is', null) + .where('asset_face.id', '=', id) + .where('asset_face.deletedAt', 'is', null) .executeTakeFirstOrThrow(); } @GenerateSql({ params: [DummyValue.UUID] }) getFaceForFacialRecognitionJob(id: string) { return this.db - .selectFrom('asset_faces') - .select(['asset_faces.id', 'asset_faces.personId', 'asset_faces.sourceType']) + .selectFrom('asset_face') + .select(['asset_face.id', 'asset_face.personId', 'asset_face.sourceType']) .select((eb) => jsonObjectFrom( eb - .selectFrom('assets') - .select(['assets.ownerId', 'assets.visibility', 'assets.fileCreatedAt']) - .whereRef('assets.id', '=', 'asset_faces.assetId'), + .selectFrom('asset') + .select(['asset.ownerId', 'asset.visibility', 'asset.fileCreatedAt']) + .whereRef('asset.id', '=', 'asset_face.assetId'), ).as('asset'), ) .select(withFaceSearch) - .where('asset_faces.id', '=', id) - .where('asset_faces.deletedAt', 'is', null) + .where('asset_face.id', '=', id) + .where('asset_face.deletedAt', 'is', null) .executeTakeFirst(); } @@ -256,40 +256,40 @@ export class PersonRepository { getDataForThumbnailGenerationJob(id: string) { return this.db .selectFrom('person') - .innerJoin('asset_faces', 'asset_faces.id', 'person.faceAssetId') - .innerJoin('assets', 'asset_faces.assetId', 'assets.id') - .leftJoin('exif', 'exif.assetId', 'assets.id') + .innerJoin('asset_face', 'asset_face.id', 'person.faceAssetId') + .innerJoin('asset', 'asset_face.assetId', 'asset.id') + .leftJoin('asset_exif', 'asset_exif.assetId', 'asset.id') .select([ 'person.ownerId', - 'asset_faces.boundingBoxX1 as x1', - 'asset_faces.boundingBoxY1 as y1', - 'asset_faces.boundingBoxX2 as x2', - 'asset_faces.boundingBoxY2 as y2', - 'asset_faces.imageWidth as oldWidth', - 'asset_faces.imageHeight as oldHeight', - 'assets.type', - 'assets.originalPath', - 'exif.orientation as exifOrientation', + 'asset_face.boundingBoxX1 as x1', + 'asset_face.boundingBoxY1 as y1', + 'asset_face.boundingBoxX2 as x2', + 'asset_face.boundingBoxY2 as y2', + 'asset_face.imageWidth as oldWidth', + 'asset_face.imageHeight as oldHeight', + 'asset.type', + 'asset.originalPath', + 'asset_exif.orientation as exifOrientation', ]) .select((eb) => eb - .selectFrom('asset_files') - .select('asset_files.path') - .whereRef('asset_files.assetId', '=', 'assets.id') - .where('asset_files.type', '=', sql.lit(AssetFileType.PREVIEW)) + .selectFrom('asset_file') + .select('asset_file.path') + .whereRef('asset_file.assetId', '=', 'asset.id') + .where('asset_file.type', '=', sql.lit(AssetFileType.PREVIEW)) .as('previewPath'), ) .where('person.id', '=', id) - .where('asset_faces.deletedAt', 'is', null) + .where('asset_face.deletedAt', 'is', null) .executeTakeFirst(); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) async reassignFace(assetFaceId: string, newPersonId: string): Promise { const result = await this.db - .updateTable('asset_faces') + .updateTable('asset_face') .set({ personId: newPersonId }) - .where('asset_faces.id', '=', assetFaceId) + .where('asset_face.id', '=', assetFaceId) .executeTakeFirst(); return Number(result.numChangedRows ?? 0); @@ -336,16 +336,16 @@ export class PersonRepository { @GenerateSql({ params: [DummyValue.UUID] }) async getStatistics(personId: string): Promise { const result = await this.db - .selectFrom('asset_faces') - .leftJoin('assets', (join) => + .selectFrom('asset_face') + .leftJoin('asset', (join) => join - .onRef('assets.id', '=', 'asset_faces.assetId') - .on('asset_faces.personId', '=', personId) - .on('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) - .on('assets.deletedAt', 'is', null), + .onRef('asset.id', '=', 'asset_face.assetId') + .on('asset_face.personId', '=', personId) + .on('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) + .on('asset.deletedAt', 'is', null), ) - .select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count')) - .where('asset_faces.deletedAt', 'is', null) + .select((eb) => eb.fn.count(eb.fn('distinct', ['asset.id'])).as('count')) + .where('asset_face.deletedAt', 'is', null) .executeTakeFirst(); return { @@ -361,16 +361,16 @@ export class PersonRepository { .where((eb) => eb.exists((eb) => eb - .selectFrom('asset_faces') - .whereRef('asset_faces.personId', '=', 'person.id') - .where('asset_faces.deletedAt', 'is', null) + .selectFrom('asset_face') + .whereRef('asset_face.personId', '=', 'person.id') + .where('asset_face.deletedAt', 'is', null) .where((eb) => eb.exists((eb) => eb - .selectFrom('assets') - .whereRef('assets.id', '=', 'asset_faces.assetId') - .where('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) - .where('assets.deletedAt', 'is', null), + .selectFrom('asset') + .whereRef('asset.id', '=', 'asset_face.assetId') + .where('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) + .where('asset.deletedAt', 'is', null), ), ), ), @@ -402,12 +402,12 @@ export class PersonRepository { ): Promise { let query = this.db; if (facesToAdd.length > 0) { - (query as any) = query.with('added', (db) => db.insertInto('asset_faces').values(facesToAdd)); + (query as any) = query.with('added', (db) => db.insertInto('asset_face').values(facesToAdd)); } if (faceIdsToRemove.length > 0) { (query as any) = query.with('removed', (db) => - db.deleteFrom('asset_faces').where('asset_faces.id', '=', (eb) => eb.fn.any(eb.val(faceIdsToRemove))), + db.deleteFrom('asset_face').where('asset_face.id', '=', (eb) => eb.fn.any(eb.val(faceIdsToRemove))), ); } @@ -469,23 +469,23 @@ export class PersonRepository { } return this.db - .selectFrom('asset_faces') - .selectAll('asset_faces') + .selectFrom('asset_face') + .selectAll('asset_face') .select(withAsset) .select(withPerson) - .where('asset_faces.assetId', 'in', assetIds) - .where('asset_faces.personId', 'in', personIds) - .where('asset_faces.deletedAt', 'is', null) + .where('asset_face.assetId', 'in', assetIds) + .where('asset_face.personId', 'in', personIds) + .where('asset_face.deletedAt', 'is', null) .execute(); } @GenerateSql({ params: [DummyValue.UUID] }) getRandomFace(personId: string) { return this.db - .selectFrom('asset_faces') - .selectAll('asset_faces') - .where('asset_faces.personId', '=', personId) - .where('asset_faces.deletedAt', 'is', null) + .selectFrom('asset_face') + .selectAll('asset_face') + .where('asset_face.personId', '=', personId) + .where('asset_face.deletedAt', 'is', null) .executeTakeFirst(); } @@ -500,22 +500,22 @@ export class PersonRepository { } async createAssetFace(face: Insertable): Promise { - await this.db.insertInto('asset_faces').values(face).execute(); + await this.db.insertInto('asset_face').values(face).execute(); } @GenerateSql({ params: [DummyValue.UUID] }) async deleteAssetFace(id: string): Promise { - await this.db.deleteFrom('asset_faces').where('asset_faces.id', '=', id).execute(); + await this.db.deleteFrom('asset_face').where('asset_face.id', '=', id).execute(); } @GenerateSql({ params: [DummyValue.UUID] }) async softDeleteAssetFaces(id: string): Promise { - await this.db.updateTable('asset_faces').set({ deletedAt: new Date() }).where('asset_faces.id', '=', id).execute(); + await this.db.updateTable('asset_face').set({ deletedAt: new Date() }).where('asset_face.id', '=', id).execute(); } async vacuum({ reindexVectors }: { reindexVectors: boolean }): Promise { - await sql`VACUUM ANALYZE asset_faces, face_search, person`.execute(this.db); - await sql`REINDEX TABLE asset_faces`.execute(this.db); + await sql`VACUUM ANALYZE asset_face, face_search, person`.execute(this.db); + await sql`REINDEX TABLE asset_face`.execute(this.db); await sql`REINDEX TABLE person`.execute(this.db); if (reindexVectors) { await sql`REINDEX TABLE face_search`.execute(this.db); diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index b354e33e57..fe8ad563bb 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -7,7 +7,7 @@ import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetStatus, AssetType, AssetVisibility, VectorIndex } from 'src/enum'; import { probes } from 'src/repositories/database.repository'; import { DB } from 'src/schema'; -import { ExifTable } from 'src/schema/tables/exif.table'; +import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { anyUuid, searchAssetBuilder } from 'src/utils/database'; import { paginationHelper } from 'src/utils/pagination'; import { isValidInteger } from 'src/validation'; @@ -183,8 +183,8 @@ export class SearchRepository { async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions) { const orderDirection = (options.orderDirection?.toLowerCase() || 'desc') as OrderByDirection; const items = await searchAssetBuilder(this.db, options) - .selectAll('assets') - .orderBy('assets.fileCreatedAt', orderDirection) + .selectAll('asset') + .orderBy('asset.fileCreatedAt', orderDirection) .limit(pagination.size + 1) .offset((pagination.page - 1) * pagination.size) .execute(); @@ -224,13 +224,13 @@ export class SearchRepository { const uuid = randomUUID(); const builder = searchAssetBuilder(this.db, options); const lessThan = builder - .selectAll('assets') - .where('assets.id', '<', uuid) + .selectAll('asset') + .where('asset.id', '<', uuid) .orderBy(sql`random()`) .limit(size); const greaterThan = builder - .selectAll('assets') - .where('assets.id', '>', uuid) + .selectAll('asset') + .where('asset.id', '>', uuid) .orderBy(sql`random()`) .limit(size); const { rows } = await sql`${lessThan} union all ${greaterThan} limit ${size}`.execute(this.db); @@ -258,8 +258,8 @@ export class SearchRepository { return this.db.transaction().execute(async (trx) => { await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.CLIP])}`.execute(trx); const items = await searchAssetBuilder(trx, options) - .selectAll('assets') - .innerJoin('smart_search', 'assets.id', 'smart_search.assetId') + .selectAll('asset') + .innerJoin('smart_search', 'asset.id', 'smart_search.assetId') .orderBy(sql`smart_search.embedding <=> ${options.embedding}`) .limit(pagination.size + 1) .offset((pagination.page - 1) * pagination.size) @@ -288,18 +288,18 @@ export class SearchRepository { return await trx .with('cte', (qb) => qb - .selectFrom('asset_faces') + .selectFrom('asset_face') .select([ - 'asset_faces.id', - 'asset_faces.personId', + 'asset_face.id', + 'asset_face.personId', sql`face_search.embedding <=> ${embedding}`.as('distance'), ]) - .innerJoin('assets', 'assets.id', 'asset_faces.assetId') - .innerJoin('face_search', 'face_search.faceId', 'asset_faces.id') - .leftJoin('person', 'person.id', 'asset_faces.personId') - .where('assets.ownerId', '=', anyUuid(userIds)) - .where('assets.deletedAt', 'is', null) - .$if(!!hasPerson, (qb) => qb.where('asset_faces.personId', 'is not', null)) + .innerJoin('asset', 'asset.id', 'asset_face.assetId') + .innerJoin('face_search', 'face_search.faceId', 'asset_face.id') + .leftJoin('person', 'person.id', 'asset_face.personId') + .where('asset.ownerId', '=', anyUuid(userIds)) + .where('asset.deletedAt', 'is', null) + .$if(!!hasPerson, (qb) => qb.where('asset_face.personId', 'is not', null)) .$if(!!minBirthDate, (qb) => qb.where((eb) => eb.or([eb('person.birthDate', 'is', null), eb('person.birthDate', '<=', minBirthDate!)]), @@ -347,13 +347,13 @@ export class SearchRepository { return this.db .withRecursive('cte', (qb) => { const base = qb - .selectFrom('exif') + .selectFrom('asset_exif') .select(['city', 'assetId']) - .innerJoin('assets', 'assets.id', 'exif.assetId') - .where('assets.ownerId', '=', anyUuid(userIds)) - .where('assets.visibility', '=', AssetVisibility.TIMELINE) - .where('assets.type', '=', AssetType.IMAGE) - .where('assets.deletedAt', 'is', null) + .innerJoin('asset', 'asset.id', 'asset_exif.assetId') + .where('asset.ownerId', '=', anyUuid(userIds)) + .where('asset.visibility', '=', AssetVisibility.TIMELINE) + .where('asset.type', '=', AssetType.IMAGE) + .where('asset.deletedAt', 'is', null) .orderBy('city') .limit(1); @@ -363,14 +363,14 @@ export class SearchRepository { .innerJoinLateral( (qb) => qb - .selectFrom('exif') + .selectFrom('asset_exif') .select(['city', 'assetId']) - .innerJoin('assets', 'assets.id', 'exif.assetId') - .where('assets.ownerId', '=', anyUuid(userIds)) - .where('assets.visibility', '=', AssetVisibility.TIMELINE) - .where('assets.type', '=', AssetType.IMAGE) - .where('assets.deletedAt', 'is', null) - .whereRef('exif.city', '>', 'cte.city') + .innerJoin('asset', 'asset.id', 'asset_exif.assetId') + .where('asset.ownerId', '=', anyUuid(userIds)) + .where('asset.visibility', '=', AssetVisibility.TIMELINE) + .where('asset.type', '=', AssetType.IMAGE) + .where('asset.deletedAt', 'is', null) + .whereRef('asset_exif.city', '>', 'cte.city') .orderBy('city') .limit(1) .as('l'), @@ -379,17 +379,17 @@ export class SearchRepository { return sql<{ city: string; assetId: string }>`(${base} union all ${recursive})`; }) - .selectFrom('assets') - .innerJoin('exif', 'assets.id', 'exif.assetId') - .innerJoin('cte', 'assets.id', 'cte.assetId') - .selectAll('assets') + .selectFrom('asset') + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .innerJoin('cte', 'asset.id', 'cte.assetId') + .selectAll('asset') .select((eb) => eb - .fn('to_jsonb', [eb.table('exif')]) - .$castTo>() + .fn('to_jsonb', [eb.table('asset_exif')]) + .$castTo>() .as('exifInfo'), ) - .orderBy('exif.city') + .orderBy('asset_exif.city') .execute(); } @@ -445,10 +445,10 @@ export class SearchRepository { private getExifField(field: K, userIds: string[]) { return this.db - .selectFrom('exif') + .selectFrom('asset_exif') .select(field) .distinctOn(field) - .innerJoin('assets', 'assets.id', 'exif.assetId') + .innerJoin('asset', 'asset.id', 'asset_exif.assetId') .where('ownerId', '=', anyUuid(userIds)) .where('visibility', '=', AssetVisibility.TIMELINE) .where('deletedAt', 'is', null) diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts index b9fa5458e4..edf999e265 100644 --- a/server/src/repositories/session.repository.ts +++ b/server/src/repositories/session.repository.ts @@ -17,7 +17,7 @@ export class SessionRepository { cleanup() { return this.db - .deleteFrom('sessions') + .deleteFrom('session') .where((eb) => eb.or([ eb('updatedAt', '<=', DateTime.now().minus({ days: 90 }).toJSDate()), @@ -31,7 +31,7 @@ export class SessionRepository { @GenerateSql({ params: [DummyValue.UUID] }) get(id: string) { return this.db - .selectFrom('sessions') + .selectFrom('session') .select(['id', 'expiresAt', 'pinExpiresAt']) .where('id', '=', id) .executeTakeFirst(); @@ -40,20 +40,20 @@ export class SessionRepository { @GenerateSql({ params: [DummyValue.STRING] }) getByToken(token: string) { return this.db - .selectFrom('sessions') + .selectFrom('session') .select((eb) => [ ...columns.authSession, jsonObjectFrom( eb - .selectFrom('users') + .selectFrom('user') .select(columns.authUser) - .whereRef('users.id', '=', 'sessions.userId') - .where('users.deletedAt', 'is', null), + .whereRef('user.id', '=', 'session.userId') + .where('user.deletedAt', 'is', null), ).as('user'), ]) - .where('sessions.token', '=', token) + .where('session.token', '=', token) .where((eb) => - eb.or([eb('sessions.expiresAt', 'is', null), eb('sessions.expiresAt', '>', DateTime.now().toJSDate())]), + eb.or([eb('session.expiresAt', 'is', null), eb('session.expiresAt', '>', DateTime.now().toJSDate())]), ) .executeTakeFirst(); } @@ -61,38 +61,48 @@ export class SessionRepository { @GenerateSql({ params: [DummyValue.UUID] }) getByUserId(userId: string) { return this.db - .selectFrom('sessions') - .innerJoin('users', (join) => join.onRef('users.id', '=', 'sessions.userId').on('users.deletedAt', 'is', null)) - .selectAll('sessions') - .where('sessions.userId', '=', userId) + .selectFrom('session') + .innerJoin('user', (join) => join.onRef('user.id', '=', 'session.userId').on('user.deletedAt', 'is', null)) + .selectAll('session') + .where('session.userId', '=', userId) .where((eb) => - eb.or([eb('sessions.expiresAt', 'is', null), eb('sessions.expiresAt', '>', DateTime.now().toJSDate())]), + eb.or([eb('session.expiresAt', 'is', null), eb('session.expiresAt', '>', DateTime.now().toJSDate())]), ) - .orderBy('sessions.updatedAt', 'desc') - .orderBy('sessions.createdAt', 'desc') + .orderBy('session.updatedAt', 'desc') + .orderBy('session.createdAt', 'desc') .execute(); } create(dto: Insertable) { - return this.db.insertInto('sessions').values(dto).returningAll().executeTakeFirstOrThrow(); + return this.db.insertInto('session').values(dto).returningAll().executeTakeFirstOrThrow(); } update(id: string, dto: Updateable) { return this.db - .updateTable('sessions') + .updateTable('session') .set(dto) - .where('sessions.id', '=', asUuid(id)) + .where('session.id', '=', asUuid(id)) .returningAll() .executeTakeFirstOrThrow(); } @GenerateSql({ params: [DummyValue.UUID] }) async delete(id: string) { - await this.db.deleteFrom('sessions').where('id', '=', asUuid(id)).execute(); + await this.db.deleteFrom('session').where('id', '=', asUuid(id)).execute(); } @GenerateSql({ params: [DummyValue.UUID] }) async lockAll(userId: string) { - await this.db.updateTable('sessions').set({ pinExpiresAt: null }).where('userId', '=', userId).execute(); + await this.db.updateTable('session').set({ pinExpiresAt: null }).where('userId', '=', userId).execute(); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + async resetSyncProgress(sessionId: string) { + await this.db.transaction().execute((tx) => { + return Promise.all([ + tx.updateTable('session').set({ isPendingSyncReset: false }).where('id', '=', sessionId).execute(), + tx.deleteFrom('session_sync_checkpoint').where('sessionId', '=', sessionId).execute(), + ]); + }); } } diff --git a/server/src/repositories/shared-link.repository.ts b/server/src/repositories/shared-link.repository.ts index 0875b535e2..d61333fcd6 100644 --- a/server/src/repositories/shared-link.repository.ts +++ b/server/src/repositories/shared-link.repository.ts @@ -22,61 +22,66 @@ export class SharedLinkRepository { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) get(userId: string, id: string) { return this.db - .selectFrom('shared_links') - .selectAll('shared_links') + .selectFrom('shared_link') + .selectAll('shared_link') .leftJoinLateral( (eb) => eb - .selectFrom('shared_link__asset') - .whereRef('shared_links.id', '=', 'shared_link__asset.sharedLinksId') - .innerJoin('assets', 'assets.id', 'shared_link__asset.assetsId') - .where('assets.deletedAt', 'is', null) - .selectAll('assets') + .selectFrom('shared_link_asset') + .whereRef('shared_link.id', '=', 'shared_link_asset.sharedLinksId') + .innerJoin('asset', 'asset.id', 'shared_link_asset.assetsId') + .where('asset.deletedAt', 'is', null) + .selectAll('asset') .innerJoinLateral( - (eb) => eb.selectFrom('exif').selectAll('exif').whereRef('exif.assetId', '=', 'assets.id').as('exifInfo'), + (eb) => + eb + .selectFrom('asset_exif') + .selectAll('asset_exif') + .whereRef('asset_exif.assetId', '=', 'asset.id') + .as('exifInfo'), (join) => join.onTrue(), ) .select((eb) => eb.fn.toJson('exifInfo').as('exifInfo')) - .orderBy('assets.fileCreatedAt', 'asc') + .orderBy('asset.fileCreatedAt', 'asc') .as('a'), (join) => join.onTrue(), ) .leftJoinLateral( (eb) => eb - .selectFrom('albums') - .selectAll('albums') - .whereRef('albums.id', '=', 'shared_links.albumId') - .where('albums.deletedAt', 'is', null) - .leftJoin('albums_assets_assets', 'albums_assets_assets.albumsId', 'albums.id') + .selectFrom('album') + .selectAll('album') + .whereRef('album.id', '=', 'shared_link.albumId') + .where('album.deletedAt', 'is', null) + .leftJoin('album_asset', 'album_asset.albumsId', 'album.id') .leftJoinLateral( (eb) => eb - .selectFrom('assets') - .selectAll('assets') - .whereRef('albums_assets_assets.assetsId', '=', 'assets.id') - .where('assets.deletedAt', 'is', null) + .selectFrom('asset') + .selectAll('asset') + .whereRef('album_asset.assetsId', '=', 'asset.id') + .where('asset.deletedAt', 'is', null) .innerJoinLateral( (eb) => eb - .selectFrom('exif') - .selectAll('exif') - .whereRef('exif.assetId', '=', 'assets.id') - .as('assets_exifInfo'), + .selectFrom('asset_exif') + .selectAll('asset_exif') + .whereRef('asset_exif.assetId', '=', 'asset.id') + .as('exifInfo'), (join) => join.onTrue(), ) - .select((eb) => eb.fn.toJson(eb.table('assets_exifInfo')).as('exifInfo')) - .orderBy('assets.fileCreatedAt', 'asc') + .select((eb) => eb.fn.toJson(eb.table('exifInfo')).as('exifInfo')) + .orderBy('asset.fileCreatedAt', 'asc') .as('assets'), (join) => join.onTrue(), ) .innerJoinLateral( (eb) => eb - .selectFrom('users') - .selectAll('users') - .whereRef('users.id', '=', 'albums.ownerId') - .where('users.deletedAt', 'is', null) + .selectFrom('user') + .selectAll('user') + .whereRef('user.id', '=', 'album.ownerId') + .where('user.deletedAt', 'is', null) .as('owner'), (join) => join.onTrue(), ) @@ -84,7 +89,7 @@ export class SharedLinkRepository { eb.fn.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`).as('assets'), ) .select((eb) => eb.fn.toJson('owner').as('owner')) - .groupBy(['albums.id', sql`"owner".*`]) + .groupBy(['album.id', sql`"owner".*`]) .as('album'), (join) => join.onTrue(), ) @@ -94,29 +99,29 @@ export class SharedLinkRepository { .$castTo() .as('assets'), ) - .groupBy(['shared_links.id', sql`"album".*`]) + .groupBy(['shared_link.id', sql`"album".*`]) .select((eb) => eb.fn.toJson('album').$castTo().as('album')) - .where('shared_links.id', '=', id) - .where('shared_links.userId', '=', userId) - .where((eb) => eb.or([eb('shared_links.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)])) - .orderBy('shared_links.createdAt', 'desc') + .where('shared_link.id', '=', id) + .where('shared_link.userId', '=', userId) + .where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)])) + .orderBy('shared_link.createdAt', 'desc') .executeTakeFirst(); } @GenerateSql({ params: [{ userId: DummyValue.UUID, albumId: DummyValue.UUID }] }) getAll({ userId, albumId }: SharedLinkSearchOptions) { return this.db - .selectFrom('shared_links') - .selectAll('shared_links') - .where('shared_links.userId', '=', userId) - .leftJoin('shared_link__asset', 'shared_link__asset.sharedLinksId', 'shared_links.id') + .selectFrom('shared_link') + .selectAll('shared_link') + .where('shared_link.userId', '=', userId) + .leftJoin('shared_link_asset', 'shared_link_asset.sharedLinksId', 'shared_link.id') .leftJoinLateral( (eb) => eb - .selectFrom('assets') - .select((eb) => eb.fn.jsonAgg('assets').as('assets')) - .whereRef('assets.id', '=', 'shared_link__asset.assetsId') - .where('assets.deletedAt', 'is', null) + .selectFrom('asset') + .select((eb) => eb.fn.jsonAgg('asset').as('assets')) + .whereRef('asset.id', '=', 'shared_link_asset.assetsId') + .where('asset.deletedAt', 'is', null) .as('assets'), (join) => join.onTrue(), ) @@ -125,75 +130,75 @@ export class SharedLinkRepository { .leftJoinLateral( (eb) => eb - .selectFrom('albums') - .selectAll('albums') - .whereRef('albums.id', '=', 'shared_links.albumId') + .selectFrom('album') + .selectAll('album') + .whereRef('album.id', '=', 'shared_link.albumId') .innerJoinLateral( (eb) => eb - .selectFrom('users') + .selectFrom('user') .select([ - 'users.id', - 'users.email', - 'users.createdAt', - 'users.profileImagePath', - 'users.isAdmin', - 'users.shouldChangePassword', - 'users.deletedAt', - 'users.oauthId', - 'users.updatedAt', - 'users.storageLabel', - 'users.name', - 'users.quotaSizeInBytes', - 'users.quotaUsageInBytes', - 'users.status', - 'users.profileChangedAt', + 'user.id', + 'user.email', + 'user.createdAt', + 'user.profileImagePath', + 'user.isAdmin', + 'user.shouldChangePassword', + 'user.deletedAt', + 'user.oauthId', + 'user.updatedAt', + 'user.storageLabel', + 'user.name', + 'user.quotaSizeInBytes', + 'user.quotaUsageInBytes', + 'user.status', + 'user.profileChangedAt', ]) - .whereRef('users.id', '=', 'albums.ownerId') - .where('users.deletedAt', 'is', null) + .whereRef('user.id', '=', 'album.ownerId') + .where('user.deletedAt', 'is', null) .as('owner'), (join) => join.onTrue(), ) .select((eb) => eb.fn.toJson('owner').as('owner')) - .where('albums.deletedAt', 'is', null) + .where('album.deletedAt', 'is', null) .as('album'), (join) => join.onTrue(), ) .select((eb) => eb.fn.toJson('album').$castTo().as('album')) - .where((eb) => eb.or([eb('shared_links.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)])) - .$if(!!albumId, (eb) => eb.where('shared_links.albumId', '=', albumId!)) - .orderBy('shared_links.createdAt', 'desc') - .distinctOn(['shared_links.createdAt']) + .where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)])) + .$if(!!albumId, (eb) => eb.where('shared_link.albumId', '=', albumId!)) + .orderBy('shared_link.createdAt', 'desc') + .distinctOn(['shared_link.createdAt']) .execute(); } @GenerateSql({ params: [DummyValue.BUFFER] }) async getByKey(key: Buffer) { return this.db - .selectFrom('shared_links') - .where('shared_links.key', '=', key) - .leftJoin('albums', 'albums.id', 'shared_links.albumId') - .where('albums.deletedAt', 'is', null) + .selectFrom('shared_link') + .where('shared_link.key', '=', key) + .leftJoin('album', 'album.id', 'shared_link.albumId') + .where('album.deletedAt', 'is', null) .select((eb) => [ ...columns.authSharedLink, jsonObjectFrom( - eb.selectFrom('users').select(columns.authUser).whereRef('users.id', '=', 'shared_links.userId'), + eb.selectFrom('user').select(columns.authUser).whereRef('user.id', '=', 'shared_link.userId'), ).as('user'), ]) - .where((eb) => eb.or([eb('shared_links.type', '=', SharedLinkType.INDIVIDUAL), eb('albums.id', 'is not', null)])) + .where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)])) .executeTakeFirst(); } async create(entity: Insertable & { assetIds?: string[] }) { const { id } = await this.db - .insertInto('shared_links') + .insertInto('shared_link') .values(_.omit(entity, 'assetIds')) .returningAll() .executeTakeFirstOrThrow(); if (entity.assetIds && entity.assetIds.length > 0) { await this.db - .insertInto('shared_link__asset') + .insertInto('shared_link_asset') .values(entity.assetIds!.map((assetsId) => ({ assetsId, sharedLinksId: id }))) .execute(); } @@ -203,15 +208,15 @@ export class SharedLinkRepository { async update(entity: Updateable & { id: string; assetIds?: string[] }) { const { id } = await this.db - .updateTable('shared_links') + .updateTable('shared_link') .set(_.omit(entity, 'assets', 'album', 'assetIds')) - .where('shared_links.id', '=', entity.id) + .where('shared_link.id', '=', entity.id) .returningAll() .executeTakeFirstOrThrow(); if (entity.assetIds && entity.assetIds.length > 0) { await this.db - .insertInto('shared_link__asset') + .insertInto('shared_link_asset') .values(entity.assetIds!.map((assetsId) => ({ assetsId, sharedLinksId: id }))) .execute(); } @@ -220,23 +225,24 @@ export class SharedLinkRepository { } async remove(id: string): Promise { - await this.db.deleteFrom('shared_links').where('shared_links.id', '=', id).execute(); + await this.db.deleteFrom('shared_link').where('shared_link.id', '=', id).execute(); } private getSharedLinks(id: string) { return this.db - .selectFrom('shared_links') - .selectAll('shared_links') - .where('shared_links.id', '=', id) - .leftJoin('shared_link__asset', 'shared_link__asset.sharedLinksId', 'shared_links.id') + .selectFrom('shared_link') + .selectAll('shared_link') + .where('shared_link.id', '=', id) + .leftJoin('shared_link_asset', 'shared_link_asset.sharedLinksId', 'shared_link.id') .leftJoinLateral( (eb) => eb - .selectFrom('assets') - .whereRef('assets.id', '=', 'shared_link__asset.assetsId') - .selectAll('assets') + .selectFrom('asset') + .whereRef('asset.id', '=', 'shared_link_asset.assetsId') + .selectAll('asset') .innerJoinLateral( - (eb) => eb.selectFrom('exif').whereRef('exif.assetId', '=', 'assets.id').selectAll().as('exif'), + (eb) => + eb.selectFrom('asset_exif').whereRef('asset_exif.assetId', '=', 'asset.id').selectAll().as('exif'), (join) => join.onTrue(), ) .as('assets'), @@ -248,7 +254,7 @@ export class SharedLinkRepository { .$castTo() .as('assets'), ) - .groupBy('shared_links.id') + .groupBy('shared_link.id') .executeTakeFirstOrThrow(); } } diff --git a/server/src/repositories/stack.repository.ts b/server/src/repositories/stack.repository.ts index c049a19373..fe16c8b5eb 100644 --- a/server/src/repositories/stack.repository.ts +++ b/server/src/repositories/stack.repository.ts @@ -13,29 +13,34 @@ export interface StackSearch { primaryAssetId?: string; } -const withAssets = (eb: ExpressionBuilder, withTags = false) => { +const withAssets = (eb: ExpressionBuilder, withTags = false) => { return jsonArrayFrom( eb - .selectFrom('assets') - .selectAll('assets') + .selectFrom('asset') + .selectAll('asset') .innerJoinLateral( - (eb) => eb.selectFrom('exif').select(columns.exif).whereRef('exif.assetId', '=', 'assets.id').as('exifInfo'), + (eb) => + eb + .selectFrom('asset_exif') + .select(columns.exif) + .whereRef('asset_exif.assetId', '=', 'asset.id') + .as('exifInfo'), (join) => join.onTrue(), ) .$if(withTags, (eb) => eb.select((eb) => jsonArrayFrom( eb - .selectFrom('tags') + .selectFrom('tag') .select(columns.tag) - .innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId') - .whereRef('tag_asset.assetsId', '=', 'assets.id'), + .innerJoin('tag_asset', 'tag.id', 'tag_asset.tagsId') + .whereRef('tag_asset.assetsId', '=', 'asset.id'), ).as('tags'), ), ) .select((eb) => eb.fn.toJson('exifInfo').as('exifInfo')) - .where('assets.deletedAt', 'is', null) - .whereRef('assets.stackId', '=', 'asset_stack.id') + .where('asset.deletedAt', 'is', null) + .whereRef('asset.stackId', '=', 'stack.id') .$call(withDefaultVisibility), ).as('assets'); }; @@ -47,28 +52,28 @@ export class StackRepository { @GenerateSql({ params: [{ ownerId: DummyValue.UUID }] }) search(query: StackSearch) { return this.db - .selectFrom('asset_stack') - .selectAll('asset_stack') + .selectFrom('stack') + .selectAll('stack') .select(withAssets) - .where('asset_stack.ownerId', '=', query.ownerId) - .$if(!!query.primaryAssetId, (eb) => eb.where('asset_stack.primaryAssetId', '=', query.primaryAssetId!)) + .where('stack.ownerId', '=', query.ownerId) + .$if(!!query.primaryAssetId, (eb) => eb.where('stack.primaryAssetId', '=', query.primaryAssetId!)) .execute(); } async create(entity: Omit, 'primaryAssetId'>, assetIds: string[]) { return this.db.transaction().execute(async (tx) => { const stacks = await tx - .selectFrom('asset_stack') - .where('asset_stack.ownerId', '=', entity.ownerId) - .where('asset_stack.primaryAssetId', 'in', assetIds) - .select('asset_stack.id') + .selectFrom('stack') + .where('stack.ownerId', '=', entity.ownerId) + .where('stack.primaryAssetId', 'in', assetIds) + .select('stack.id') .select((eb) => jsonArrayFrom( eb - .selectFrom('assets') - .select('assets.id') - .whereRef('assets.stackId', '=', 'asset_stack.id') - .where('assets.deletedAt', 'is', null), + .selectFrom('asset') + .select('asset.id') + .whereRef('asset.stackId', '=', 'stack.id') + .where('asset.deletedAt', 'is', null), ).as('assets'), ) .execute(); @@ -86,7 +91,7 @@ export class StackRepository { if (stacks.length > 0) { await tx - .deleteFrom('asset_stack') + .deleteFrom('stack') .where( 'id', 'in', @@ -96,13 +101,13 @@ export class StackRepository { } const newRecord = await tx - .insertInto('asset_stack') + .insertInto('stack') .values({ ...entity, primaryAssetId: assetIds[0] }) .returning('id') .executeTakeFirstOrThrow(); await tx - .updateTable('assets') + .updateTable('asset') .set({ stackId: newRecord.id, updatedAt: new Date(), @@ -111,8 +116,8 @@ export class StackRepository { .execute(); return tx - .selectFrom('asset_stack') - .selectAll('asset_stack') + .selectFrom('stack') + .selectAll('stack') .select(withAssets) .where('id', '=', newRecord.id) .executeTakeFirstOrThrow(); @@ -121,19 +126,19 @@ export class StackRepository { @GenerateSql({ params: [DummyValue.UUID] }) async delete(id: string): Promise { - await this.db.deleteFrom('asset_stack').where('id', '=', asUuid(id)).execute(); + await this.db.deleteFrom('stack').where('id', '=', asUuid(id)).execute(); } async deleteAll(ids: string[]): Promise { - await this.db.deleteFrom('asset_stack').where('id', 'in', ids).execute(); + await this.db.deleteFrom('stack').where('id', 'in', ids).execute(); } update(id: string, entity: Updateable) { return this.db - .updateTable('asset_stack') + .updateTable('stack') .set(entity) .where('id', '=', asUuid(id)) - .returningAll('asset_stack') + .returningAll('stack') .returning((eb) => withAssets(eb, true)) .executeTakeFirstOrThrow(); } @@ -141,7 +146,7 @@ export class StackRepository { @GenerateSql({ params: [DummyValue.UUID] }) getById(id: string) { return this.db - .selectFrom('asset_stack') + .selectFrom('stack') .selectAll() .select((eb) => withAssets(eb, true)) .where('id', '=', asUuid(id)) diff --git a/server/src/repositories/sync-checkpoint.repository.ts b/server/src/repositories/sync-checkpoint.repository.ts index 5133ba8904..65fd018136 100644 --- a/server/src/repositories/sync-checkpoint.repository.ts +++ b/server/src/repositories/sync-checkpoint.repository.ts @@ -13,7 +13,7 @@ export class SyncCheckpointRepository { @GenerateSql({ params: [DummyValue.UUID] }) getAll(sessionId: string) { return this.db - .selectFrom('session_sync_checkpoints') + .selectFrom('session_sync_checkpoint') .select(['type', 'ack']) .where('sessionId', '=', sessionId) .execute(); @@ -21,7 +21,7 @@ export class SyncCheckpointRepository { upsertAll(items: Insertable[]) { return this.db - .insertInto('session_sync_checkpoints') + .insertInto('session_sync_checkpoint') .values(items) .onConflict((oc) => oc.columns(['sessionId', 'type']).doUpdateSet((eb) => ({ @@ -34,7 +34,7 @@ export class SyncCheckpointRepository { @GenerateSql({ params: [DummyValue.UUID] }) deleteAll(sessionId: string, types?: SyncEntityType[]) { return this.db - .deleteFrom('session_sync_checkpoints') + .deleteFrom('session_sync_checkpoint') .where('sessionId', '=', sessionId) .$if(!!types, (qb) => qb.where('type', 'in', types!)) .execute(); diff --git a/server/src/repositories/sync.repository.ts b/server/src/repositories/sync.repository.ts index 699aaec83d..34c450d52d 100644 --- a/server/src/repositories/sync.repository.ts +++ b/server/src/repositories/sync.repository.ts @@ -7,25 +7,29 @@ import { DB } from 'src/schema'; import { SyncAck } from 'src/types'; type AuditTables = - | 'users_audit' - | 'partners_audit' - | 'assets_audit' - | 'albums_audit' - | 'album_users_audit' - | 'album_assets_audit' - | 'memories_audit' - | 'memory_assets_audit' - | 'stacks_audit'; + | 'user_audit' + | 'partner_audit' + | 'asset_audit' + | 'album_audit' + | 'album_user_audit' + | 'album_asset_audit' + | 'memory_audit' + | 'memory_asset_audit' + | 'stack_audit' + | 'person_audit' + | 'user_metadata_audit'; type UpsertTables = - | 'users' - | 'partners' - | 'assets' - | 'exif' - | 'albums' - | 'albums_shared_users_users' - | 'memories' - | 'memories_assets_assets' - | 'asset_stack'; + | 'user' + | 'partner' + | 'asset' + | 'asset_exif' + | 'album' + | 'album_user' + | 'memory' + | 'memory_asset' + | 'stack' + | 'person' + | 'user_metadata'; @Injectable() export class SyncRepository { @@ -42,8 +46,10 @@ export class SyncRepository { partnerAsset: PartnerAssetsSync; partnerAssetExif: PartnerAssetExifsSync; partnerStack: PartnerStackSync; + people: PersonSync; stack: StackSync; user: UserSync; + userMetadata: UserMetadataSync; constructor(@InjectKysely() private db: Kysely) { this.album = new AlbumSync(this.db); @@ -59,34 +65,34 @@ export class SyncRepository { this.partnerAsset = new PartnerAssetsSync(this.db); this.partnerAssetExif = new PartnerAssetExifsSync(this.db); this.partnerStack = new PartnerStackSync(this.db); + this.people = new PersonSync(this.db); this.stack = new StackSync(this.db); this.user = new UserSync(this.db); + this.userMetadata = new UserMetadataSync(this.db); } } class BaseSync { constructor(protected db: Kysely) {} - protected auditTableFilters, D>( - qb: SelectQueryBuilder, - ack?: SyncAck, - ) { - const builder = qb as SelectQueryBuilder; - return builder - .where('deletedAt', '<', sql.raw("now() - interval '1 millisecond'")) - .$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId)) - .orderBy('id', 'asc') as SelectQueryBuilder; + protected auditTableFilters(ack?: SyncAck) { + return , D>(qb: SelectQueryBuilder) => { + const builder = qb as SelectQueryBuilder; + return builder + .where('deletedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId)) + .orderBy('id', 'asc') as SelectQueryBuilder; + }; } - protected upsertTableFilters, D>( - qb: SelectQueryBuilder, - ack?: SyncAck, - ) { - const builder = qb as SelectQueryBuilder; - return builder - .where('updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) - .$if(!!ack, (qb) => qb.where('updateId', '>', ack!.updateId)) - .orderBy('updateId', 'asc') as SelectQueryBuilder; + protected upsertTableFilters(ack?: SyncAck) { + return , D>(qb: SelectQueryBuilder) => { + const builder = qb as SelectQueryBuilder; + return builder + .where('updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .$if(!!ack, (qb) => qb.where('updateId', '>', ack!.updateId)) + .orderBy('updateId', 'asc') as SelectQueryBuilder; + }; } } @@ -94,7 +100,7 @@ class AlbumSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) getCreatedAfter(userId: string, afterCreateId?: string) { return this.db - .selectFrom('albums_shared_users_users') + .selectFrom('album_user') .select(['albumsId as id', 'createId']) .where('usersId', '=', userId) .$if(!!afterCreateId, (qb) => qb.where('createId', '>=', afterCreateId!)) @@ -106,34 +112,34 @@ class AlbumSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID], stream: true }) getDeletes(userId: string, ack?: SyncAck) { return this.db - .selectFrom('albums_audit') + .selectFrom('album_audit') .select(['id', 'albumId']) .where('userId', '=', userId) - .$call((qb) => this.auditTableFilters(qb, ack)) + .$call(this.auditTableFilters(ack)) .stream(); } @GenerateSql({ params: [DummyValue.UUID], stream: true }) getUpserts(userId: string, ack?: SyncAck) { return this.db - .selectFrom('albums') - .distinctOn(['albums.id', 'albums.updateId']) - .where('albums.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) - .$if(!!ack, (qb) => qb.where('albums.updateId', '>', ack!.updateId)) - .orderBy('albums.updateId', 'asc') - .leftJoin('albums_shared_users_users as album_users', 'albums.id', 'album_users.albumsId') - .where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('album_users.usersId', '=', userId)])) + .selectFrom('album') + .distinctOn(['album.id', 'album.updateId']) + .where('album.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .$if(!!ack, (qb) => qb.where('album.updateId', '>', ack!.updateId)) + .orderBy('album.updateId', 'asc') + .leftJoin('album_user as album_users', 'album.id', 'album_users.albumsId') + .where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('album_users.usersId', '=', userId)])) .select([ - 'albums.id', - 'albums.ownerId', - 'albums.albumName as name', - 'albums.description', - 'albums.createdAt', - 'albums.updatedAt', - 'albums.albumThumbnailAssetId as thumbnailAssetId', - 'albums.isActivityEnabled', - 'albums.order', - 'albums.updateId', + 'album.id', + 'album.ownerId', + 'album.albumName as name', + 'album.description', + 'album.createdAt', + 'album.updatedAt', + 'album.albumThumbnailAssetId as thumbnailAssetId', + 'album.isActivityEnabled', + 'album.order', + 'album.updateId', ]) .stream(); } @@ -143,31 +149,31 @@ class AlbumAssetSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true }) getBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) { return this.db - .selectFrom('assets') - .innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'assets.id') + .selectFrom('asset') + .innerJoin('album_asset', 'album_asset.assetsId', 'asset.id') .select(columns.syncAsset) - .select('assets.updateId') - .where('album_assets.albumsId', '=', albumId) - .where('assets.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) - .where('assets.updateId', '<=', beforeUpdateId) - .$if(!!afterUpdateId, (eb) => eb.where('assets.updateId', '>=', afterUpdateId!)) - .orderBy('assets.updateId', 'asc') + .select('asset.updateId') + .where('album_asset.albumsId', '=', albumId) + .where('asset.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .where('asset.updateId', '<=', beforeUpdateId) + .$if(!!afterUpdateId, (eb) => eb.where('asset.updateId', '>=', afterUpdateId!)) + .orderBy('asset.updateId', 'asc') .stream(); } @GenerateSql({ params: [DummyValue.UUID], stream: true }) getUpserts(userId: string, ack?: SyncAck) { return this.db - .selectFrom('assets') - .innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'assets.id') + .selectFrom('asset') + .innerJoin('album_asset', 'album_asset.assetsId', 'asset.id') .select(columns.syncAsset) - .select('assets.updateId') - .where('assets.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) - .$if(!!ack, (qb) => qb.where('assets.updateId', '>', ack!.updateId)) - .orderBy('assets.updateId', 'asc') - .innerJoin('albums', 'albums.id', 'album_assets.albumsId') - .leftJoin('albums_shared_users_users as album_users', 'album_users.albumsId', 'album_assets.albumsId') - .where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('album_users.usersId', '=', userId)])) + .select('asset.updateId') + .where('asset.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .$if(!!ack, (qb) => qb.where('asset.updateId', '>', ack!.updateId)) + .orderBy('asset.updateId', 'asc') + .innerJoin('album', 'album.id', 'album_asset.albumsId') + .leftJoin('album_user', 'album_user.albumsId', 'album_asset.albumsId') + .where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('album_user.usersId', '=', userId)])) .stream(); } } @@ -176,31 +182,31 @@ class AlbumAssetExifSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true }) getBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) { return this.db - .selectFrom('exif') - .innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'exif.assetId') + .selectFrom('asset_exif') + .innerJoin('album_asset', 'album_asset.assetsId', 'asset_exif.assetId') .select(columns.syncAssetExif) - .select('exif.updateId') - .where('album_assets.albumsId', '=', albumId) - .where('exif.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) - .where('exif.updateId', '<=', beforeUpdateId) - .$if(!!afterUpdateId, (eb) => eb.where('exif.updateId', '>=', afterUpdateId!)) - .orderBy('exif.updateId', 'asc') + .select('asset_exif.updateId') + .where('album_asset.albumsId', '=', albumId) + .where('asset_exif.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .where('asset_exif.updateId', '<=', beforeUpdateId) + .$if(!!afterUpdateId, (eb) => eb.where('asset_exif.updateId', '>=', afterUpdateId!)) + .orderBy('asset_exif.updateId', 'asc') .stream(); } @GenerateSql({ params: [DummyValue.UUID], stream: true }) getUpserts(userId: string, ack?: SyncAck) { return this.db - .selectFrom('exif') - .innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'exif.assetId') + .selectFrom('asset_exif') + .innerJoin('album_asset', 'album_asset.assetsId', 'asset_exif.assetId') .select(columns.syncAssetExif) - .select('exif.updateId') - .where('exif.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) - .$if(!!ack, (qb) => qb.where('exif.updateId', '>', ack!.updateId)) - .orderBy('exif.updateId', 'asc') - .innerJoin('albums', 'albums.id', 'album_assets.albumsId') - .leftJoin('albums_shared_users_users as album_users', 'album_users.albumsId', 'album_assets.albumsId') - .where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('album_users.usersId', '=', userId)])) + .select('asset_exif.updateId') + .where('asset_exif.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .$if(!!ack, (qb) => qb.where('asset_exif.updateId', '>', ack!.updateId)) + .orderBy('asset_exif.updateId', 'asc') + .innerJoin('album', 'album.id', 'album_asset.albumsId') + .leftJoin('album_user', 'album_user.albumsId', 'album_asset.albumsId') + .where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('album_user.usersId', '=', userId)])) .stream(); } } @@ -209,7 +215,7 @@ class AlbumToAssetSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true }) getBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) { return this.db - .selectFrom('albums_assets_assets as album_assets') + .selectFrom('album_asset as album_assets') .select(['album_assets.assetsId as assetId', 'album_assets.albumsId as albumId', 'album_assets.updateId']) .where('album_assets.albumsId', '=', albumId) .where('album_assets.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) @@ -222,41 +228,41 @@ class AlbumToAssetSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID], stream: true }) getDeletes(userId: string, ack?: SyncAck) { return this.db - .selectFrom('album_assets_audit') + .selectFrom('album_asset_audit') .select(['id', 'assetId', 'albumId']) .where((eb) => eb( 'albumId', 'in', eb - .selectFrom('albums') + .selectFrom('album') .select(['id']) .where('ownerId', '=', userId) .union((eb) => eb.parens( eb - .selectFrom('albums_shared_users_users as albumUsers') - .select(['albumUsers.albumsId as id']) - .where('albumUsers.usersId', '=', userId), + .selectFrom('album_user') + .select(['album_user.albumsId as id']) + .where('album_user.usersId', '=', userId), ), ), ), ) - .$call((qb) => this.auditTableFilters(qb, ack)) + .$call(this.auditTableFilters(ack)) .stream(); } @GenerateSql({ params: [DummyValue.UUID], stream: true }) getUpserts(userId: string, ack?: SyncAck) { return this.db - .selectFrom('albums_assets_assets as album_assets') - .select(['album_assets.assetsId as assetId', 'album_assets.albumsId as albumId', 'album_assets.updateId']) - .where('album_assets.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) - .$if(!!ack, (qb) => qb.where('album_assets.updateId', '>', ack!.updateId)) - .orderBy('album_assets.updateId', 'asc') - .innerJoin('albums', 'albums.id', 'album_assets.albumsId') - .leftJoin('albums_shared_users_users as album_users', 'album_users.albumsId', 'album_assets.albumsId') - .where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('album_users.usersId', '=', userId)])) + .selectFrom('album_asset') + .select(['album_asset.assetsId as assetId', 'album_asset.albumsId as albumId', 'album_asset.updateId']) + .where('album_asset.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .$if(!!ack, (qb) => qb.where('album_asset.updateId', '>', ack!.updateId)) + .orderBy('album_asset.updateId', 'asc') + .innerJoin('album', 'album.id', 'album_asset.albumsId') + .leftJoin('album_user', 'album_user.albumsId', 'album_asset.albumsId') + .where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('album_user.usersId', '=', userId)])) .stream(); } } @@ -265,9 +271,9 @@ class AlbumUserSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true }) getBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) { return this.db - .selectFrom('albums_shared_users_users as album_users') + .selectFrom('album_user') .select(columns.syncAlbumUser) - .select('album_users.updateId') + .select('album_user.updateId') .where('albumsId', '=', albumId) .where('updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) .where('updateId', '<=', beforeUpdateId) @@ -279,51 +285,51 @@ class AlbumUserSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID], stream: true }) getDeletes(userId: string, ack?: SyncAck) { return this.db - .selectFrom('album_users_audit') + .selectFrom('album_user_audit') .select(['id', 'userId', 'albumId']) .where((eb) => eb( 'albumId', 'in', eb - .selectFrom('albums') + .selectFrom('album') .select(['id']) .where('ownerId', '=', userId) .union((eb) => eb.parens( eb - .selectFrom('albums_shared_users_users as albumUsers') - .select(['albumUsers.albumsId as id']) - .where('albumUsers.usersId', '=', userId), + .selectFrom('album_user') + .select(['album_user.albumsId as id']) + .where('album_user.usersId', '=', userId), ), ), ), ) - .$call((qb) => this.auditTableFilters(qb, ack)) + .$call(this.auditTableFilters(ack)) .stream(); } @GenerateSql({ params: [DummyValue.UUID], stream: true }) getUpserts(userId: string, ack?: SyncAck) { return this.db - .selectFrom('albums_shared_users_users as album_users') + .selectFrom('album_user') .select(columns.syncAlbumUser) - .select('album_users.updateId') - .where('album_users.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) - .$if(!!ack, (qb) => qb.where('album_users.updateId', '>', ack!.updateId)) - .orderBy('album_users.updateId', 'asc') + .select('album_user.updateId') + .where('album_user.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .$if(!!ack, (qb) => qb.where('album_user.updateId', '>', ack!.updateId)) + .orderBy('album_user.updateId', 'asc') .where((eb) => eb( - 'album_users.albumsId', + 'album_user.albumsId', 'in', eb - .selectFrom('albums') + .selectFrom('album') .select(['id']) .where('ownerId', '=', userId) .union((eb) => eb.parens( eb - .selectFrom('albums_shared_users_users as albumUsers') + .selectFrom('album_user as albumUsers') .select(['albumUsers.albumsId as id']) .where('albumUsers.usersId', '=', userId), ), @@ -338,21 +344,56 @@ class AssetSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID], stream: true }) getDeletes(userId: string, ack?: SyncAck) { return this.db - .selectFrom('assets_audit') + .selectFrom('asset_audit') .select(['id', 'assetId']) .where('ownerId', '=', userId) - .$call((qb) => this.auditTableFilters(qb, ack)) + .$call(this.auditTableFilters(ack)) .stream(); } @GenerateSql({ params: [DummyValue.UUID], stream: true }) getUpserts(userId: string, ack?: SyncAck) { return this.db - .selectFrom('assets') + .selectFrom('asset') .select(columns.syncAsset) - .select('assets.updateId') + .select('asset.updateId') .where('ownerId', '=', userId) - .$call((qb) => this.upsertTableFilters(qb, ack)) + .$call(this.upsertTableFilters(ack)) + .stream(); + } +} + +class PersonSync extends BaseSync { + @GenerateSql({ params: [DummyValue.UUID], stream: true }) + getDeletes(userId: string, ack?: SyncAck) { + return this.db + .selectFrom('person_audit') + .select(['id', 'personId']) + .where('ownerId', '=', userId) + .$call(this.auditTableFilters(ack)) + .stream(); + } + + @GenerateSql({ params: [DummyValue.UUID], stream: true }) + getUpserts(userId: string, ack?: SyncAck) { + return this.db + .selectFrom('person') + .select([ + 'id', + 'createdAt', + 'updatedAt', + 'ownerId', + 'name', + 'birthDate', + 'thumbnailPath', + 'isHidden', + 'isFavorite', + 'color', + 'updateId', + 'faceAssetId', + ]) + .where('ownerId', '=', userId) + .$call(this.upsertTableFilters(ack)) .stream(); } } @@ -361,11 +402,11 @@ class AssetExifSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID], stream: true }) getUpserts(userId: string, ack?: SyncAck) { return this.db - .selectFrom('exif') + .selectFrom('asset_exif') .select(columns.syncAssetExif) - .select('exif.updateId') - .where('assetId', 'in', (eb) => eb.selectFrom('assets').select('id').where('ownerId', '=', userId)) - .$call((qb) => this.upsertTableFilters(qb, ack)) + .select('asset_exif.updateId') + .where('assetId', 'in', (eb) => eb.selectFrom('asset').select('id').where('ownerId', '=', userId)) + .$call(this.upsertTableFilters(ack)) .stream(); } } @@ -374,17 +415,17 @@ class MemorySync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID], stream: true }) getDeletes(userId: string, ack?: SyncAck) { return this.db - .selectFrom('memories_audit') + .selectFrom('memory_audit') .select(['id', 'memoryId']) .where('userId', '=', userId) - .$call((qb) => this.auditTableFilters(qb, ack)) + .$call(this.auditTableFilters(ack)) .stream(); } @GenerateSql({ params: [DummyValue.UUID], stream: true }) getUpserts(userId: string, ack?: SyncAck) { return this.db - .selectFrom('memories') + .selectFrom('memory') .select([ 'id', 'createdAt', @@ -401,7 +442,7 @@ class MemorySync extends BaseSync { ]) .select('updateId') .where('ownerId', '=', userId) - .$call((qb) => this.upsertTableFilters(qb, ack)) + .$call(this.upsertTableFilters(ack)) .stream(); } } @@ -410,21 +451,21 @@ class MemoryToAssetSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID], stream: true }) getDeletes(userId: string, ack?: SyncAck) { return this.db - .selectFrom('memory_assets_audit') + .selectFrom('memory_asset_audit') .select(['id', 'memoryId', 'assetId']) - .where('memoryId', 'in', (eb) => eb.selectFrom('memories').select('id').where('ownerId', '=', userId)) - .$call((qb) => this.auditTableFilters(qb, ack)) + .where('memoryId', 'in', (eb) => eb.selectFrom('memory').select('id').where('ownerId', '=', userId)) + .$call(this.auditTableFilters(ack)) .stream(); } @GenerateSql({ params: [DummyValue.UUID], stream: true }) getUpserts(userId: string, ack?: SyncAck) { return this.db - .selectFrom('memories_assets_assets') + .selectFrom('memory_asset') .select(['memoriesId as memoryId', 'assetsId as assetId']) .select('updateId') - .where('memoriesId', 'in', (eb) => eb.selectFrom('memories').select('id').where('ownerId', '=', userId)) - .$call((qb) => this.upsertTableFilters(qb, ack)) + .where('memoriesId', 'in', (eb) => eb.selectFrom('memory').select('id').where('ownerId', '=', userId)) + .$call(this.upsertTableFilters(ack)) .stream(); } } @@ -433,32 +474,32 @@ class PartnerSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) getCreatedAfter(userId: string, afterCreateId?: string) { return this.db - .selectFrom('partners') + .selectFrom('partner') .select(['sharedById', 'createId']) .where('sharedWithId', '=', userId) .$if(!!afterCreateId, (qb) => qb.where('createId', '>=', afterCreateId!)) .where('createdAt', '<', sql.raw("now() - interval '1 millisecond'")) - .orderBy('partners.createId', 'asc') + .orderBy('partner.createId', 'asc') .execute(); } @GenerateSql({ params: [DummyValue.UUID], stream: true }) getDeletes(userId: string, ack?: SyncAck) { return this.db - .selectFrom('partners_audit') + .selectFrom('partner_audit') .select(['id', 'sharedById', 'sharedWithId']) .where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)])) - .$call((qb) => this.auditTableFilters(qb, ack)) + .$call(this.auditTableFilters(ack)) .stream(); } @GenerateSql({ params: [DummyValue.UUID], stream: true }) getUpserts(userId: string, ack?: SyncAck) { return this.db - .selectFrom('partners') + .selectFrom('partner') .select(['sharedById', 'sharedWithId', 'inTimeline', 'updateId']) .where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)])) - .$call((qb) => this.upsertTableFilters(qb, ack)) + .$call(this.upsertTableFilters(ack)) .stream(); } } @@ -467,9 +508,9 @@ class PartnerAssetsSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true }) getBackfill(partnerId: string, afterUpdateId: string | undefined, beforeUpdateId: string) { return this.db - .selectFrom('assets') + .selectFrom('asset') .select(columns.syncAsset) - .select('assets.updateId') + .select('asset.updateId') .where('ownerId', '=', partnerId) .where('updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) .where('updateId', '<=', beforeUpdateId) @@ -481,25 +522,25 @@ class PartnerAssetsSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID], stream: true }) getDeletes(userId: string, ack?: SyncAck) { return this.db - .selectFrom('assets_audit') + .selectFrom('asset_audit') .select(['id', 'assetId']) .where('ownerId', 'in', (eb) => - eb.selectFrom('partners').select(['sharedById']).where('sharedWithId', '=', userId), + eb.selectFrom('partner').select(['sharedById']).where('sharedWithId', '=', userId), ) - .$call((qb) => this.auditTableFilters(qb, ack)) + .$call(this.auditTableFilters(ack)) .stream(); } @GenerateSql({ params: [DummyValue.UUID], stream: true }) getUpserts(userId: string, ack?: SyncAck) { return this.db - .selectFrom('assets') + .selectFrom('asset') .select(columns.syncAsset) - .select('assets.updateId') + .select('asset.updateId') .where('ownerId', 'in', (eb) => - eb.selectFrom('partners').select(['sharedById']).where('sharedWithId', '=', userId), + eb.selectFrom('partner').select(['sharedById']).where('sharedWithId', '=', userId), ) - .$call((qb) => this.upsertTableFilters(qb, ack)) + .$call(this.upsertTableFilters(ack)) .stream(); } } @@ -508,33 +549,33 @@ class PartnerAssetExifsSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true }) getBackfill(partnerId: string, afterUpdateId: string | undefined, beforeUpdateId: string) { return this.db - .selectFrom('exif') + .selectFrom('asset_exif') .select(columns.syncAssetExif) - .select('exif.updateId') - .innerJoin('assets', 'assets.id', 'exif.assetId') - .where('assets.ownerId', '=', partnerId) - .where('exif.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) - .where('exif.updateId', '<=', beforeUpdateId) - .$if(!!afterUpdateId, (eb) => eb.where('exif.updateId', '>=', afterUpdateId!)) - .orderBy('exif.updateId', 'asc') + .select('asset_exif.updateId') + .innerJoin('asset', 'asset.id', 'asset_exif.assetId') + .where('asset.ownerId', '=', partnerId) + .where('asset_exif.updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .where('asset_exif.updateId', '<=', beforeUpdateId) + .$if(!!afterUpdateId, (eb) => eb.where('asset_exif.updateId', '>=', afterUpdateId!)) + .orderBy('asset_exif.updateId', 'asc') .stream(); } @GenerateSql({ params: [DummyValue.UUID], stream: true }) getUpserts(userId: string, ack?: SyncAck) { return this.db - .selectFrom('exif') + .selectFrom('asset_exif') .select(columns.syncAssetExif) - .select('exif.updateId') + .select('asset_exif.updateId') .where('assetId', 'in', (eb) => eb - .selectFrom('assets') + .selectFrom('asset') .select('id') .where('ownerId', 'in', (eb) => - eb.selectFrom('partners').select(['sharedById']).where('sharedWithId', '=', userId), + eb.selectFrom('partner').select(['sharedById']).where('sharedWithId', '=', userId), ), ) - .$call((qb) => this.upsertTableFilters(qb, ack)) + .$call(this.upsertTableFilters(ack)) .stream(); } } @@ -543,21 +584,21 @@ class StackSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID], stream: true }) getDeletes(userId: string, ack?: SyncAck) { return this.db - .selectFrom('stacks_audit') + .selectFrom('stack_audit') .select(['id', 'stackId']) .where('userId', '=', userId) - .$call((qb) => this.auditTableFilters(qb, ack)) + .$call(this.auditTableFilters(ack)) .stream(); } @GenerateSql({ params: [DummyValue.UUID], stream: true }) getUpserts(userId: string, ack?: SyncAck) { return this.db - .selectFrom('asset_stack') + .selectFrom('stack') .select(columns.syncStack) .select('updateId') .where('ownerId', '=', userId) - .$call((qb) => this.upsertTableFilters(qb, ack)) + .$call(this.upsertTableFilters(ack)) .stream(); } } @@ -566,19 +607,17 @@ class PartnerStackSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID], stream: true }) getDeletes(userId: string, ack?: SyncAck) { return this.db - .selectFrom('stacks_audit') + .selectFrom('stack_audit') .select(['id', 'stackId']) - .where('userId', 'in', (eb) => - eb.selectFrom('partners').select(['sharedById']).where('sharedWithId', '=', userId), - ) - .$call((qb) => this.auditTableFilters(qb, ack)) + .where('userId', 'in', (eb) => eb.selectFrom('partner').select(['sharedById']).where('sharedWithId', '=', userId)) + .$call(this.auditTableFilters(ack)) .stream(); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true }) getBackfill(partnerId: string, afterUpdateId: string | undefined, beforeUpdateId: string) { return this.db - .selectFrom('asset_stack') + .selectFrom('stack') .select(columns.syncStack) .select('updateId') .where('ownerId', '=', partnerId) @@ -592,32 +631,51 @@ class PartnerStackSync extends BaseSync { @GenerateSql({ params: [DummyValue.UUID], stream: true }) getUpserts(userId: string, ack?: SyncAck) { return this.db - .selectFrom('asset_stack') + .selectFrom('stack') .select(columns.syncStack) .select('updateId') .where('ownerId', 'in', (eb) => - eb.selectFrom('partners').select(['sharedById']).where('sharedWithId', '=', userId), + eb.selectFrom('partner').select(['sharedById']).where('sharedWithId', '=', userId), ) - .$call((qb) => this.upsertTableFilters(qb, ack)) + .$call(this.upsertTableFilters(ack)) .stream(); } } + class UserSync extends BaseSync { @GenerateSql({ params: [], stream: true }) getDeletes(ack?: SyncAck) { - return this.db - .selectFrom('users_audit') - .select(['id', 'userId']) - .$call((qb) => this.auditTableFilters(qb, ack)) - .stream(); + return this.db.selectFrom('user_audit').select(['id', 'userId']).$call(this.auditTableFilters(ack)).stream(); } @GenerateSql({ params: [], stream: true }) getUpserts(ack?: SyncAck) { return this.db - .selectFrom('users') + .selectFrom('user') .select(['id', 'name', 'email', 'deletedAt', 'updateId']) - .$call((qb) => this.upsertTableFilters(qb, ack)) + .$call(this.upsertTableFilters(ack)) + .stream(); + } +} + +class UserMetadataSync extends BaseSync { + @GenerateSql({ params: [DummyValue.UUID], stream: true }) + getDeletes(userId: string, ack?: SyncAck) { + return this.db + .selectFrom('user_metadata_audit') + .select(['id', 'userId', 'key']) + .where('userId', '=', userId) + .$call(this.auditTableFilters(ack)) + .stream(); + } + + @GenerateSql({ params: [DummyValue.UUID], stream: true }) + getUpserts(userId: string, ack?: SyncAck) { + return this.db + .selectFrom('user_metadata') + .select(['userId', 'key', 'value', 'updateId']) + .where('userId', '=', userId) + .$call(this.upsertTableFilters(ack)) .stream(); } } diff --git a/server/src/repositories/tag.repository.ts b/server/src/repositories/tag.repository.ts index 1c38242f53..9bbb62bd8b 100644 --- a/server/src/repositories/tag.repository.ts +++ b/server/src/repositories/tag.repository.ts @@ -19,13 +19,13 @@ export class TagRepository { @GenerateSql({ params: [DummyValue.UUID] }) get(id: string) { - return this.db.selectFrom('tags').select(columns.tag).where('id', '=', id).executeTakeFirst(); + return this.db.selectFrom('tag').select(columns.tag).where('id', '=', id).executeTakeFirst(); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) getByValue(userId: string, value: string) { return this.db - .selectFrom('tags') + .selectFrom('tag') .select(columns.tag) .where('userId', '=', userId) .where('value', '=', value) @@ -37,7 +37,7 @@ export class TagRepository { const parentId = _parentId ?? null; return this.db.transaction().execute(async (tx) => { const tag = await this.db - .insertInto('tags') + .insertInto('tag') .values({ userId, value, parentId }) .onConflict((oc) => oc.columns(['userId', 'value']).doUpdateSet({ parentId })) .returning(columns.tag) @@ -45,18 +45,18 @@ export class TagRepository { // update closure table await tx - .insertInto('tags_closure') + .insertInto('tag_closure') .values({ id_ancestor: tag.id, id_descendant: tag.id }) .onConflict((oc) => oc.doNothing()) .execute(); if (parentId) { await tx - .insertInto('tags_closure') + .insertInto('tag_closure') .columns(['id_ancestor', 'id_descendant']) .expression( this.db - .selectFrom('tags_closure') + .selectFrom('tag_closure') .select(['id_ancestor', sql.raw(`'${tag.id}'`).as('id_descendant')]) .where('id_descendant', '=', parentId), ) @@ -70,22 +70,22 @@ export class TagRepository { @GenerateSql({ params: [DummyValue.UUID] }) getAll(userId: string) { - return this.db.selectFrom('tags').select(columns.tag).where('userId', '=', userId).orderBy('value').execute(); + return this.db.selectFrom('tag').select(columns.tag).where('userId', '=', userId).orderBy('value').execute(); } @GenerateSql({ params: [{ userId: DummyValue.UUID, color: DummyValue.STRING, value: DummyValue.STRING }] }) create(tag: Insertable) { - return this.db.insertInto('tags').values(tag).returningAll().executeTakeFirstOrThrow(); + return this.db.insertInto('tag').values(tag).returningAll().executeTakeFirstOrThrow(); } @GenerateSql({ params: [DummyValue.UUID, { color: DummyValue.STRING }] }) update(id: string, dto: Updateable) { - return this.db.updateTable('tags').set(dto).where('id', '=', id).returningAll().executeTakeFirstOrThrow(); + return this.db.updateTable('tag').set(dto).where('id', '=', id).returningAll().executeTakeFirstOrThrow(); } @GenerateSql({ params: [DummyValue.UUID] }) async delete(id: string) { - await this.db.deleteFrom('tags').where('id', '=', id).execute(); + await this.db.deleteFrom('tag').where('id', '=', id).execute(); } @ChunkedSet({ paramIndex: 1 }) @@ -166,17 +166,17 @@ export class TagRepository { // TODO rewrite as a single statement await this.db.transaction().execute(async (tx) => { const result = await tx - .selectFrom('assets') - .innerJoin('tag_asset', 'tag_asset.assetsId', 'assets.id') - .innerJoin('tags_closure', 'tags_closure.id_descendant', 'tag_asset.tagsId') - .innerJoin('tags', 'tags.id', 'tags_closure.id_descendant') - .select((eb) => ['tags.id', eb.fn.count('assets.id').as('count')]) - .groupBy('tags.id') + .selectFrom('asset') + .innerJoin('tag_asset', 'tag_asset.assetsId', 'asset.id') + .innerJoin('tag_closure', 'tag_closure.id_descendant', 'tag_asset.tagsId') + .innerJoin('tag', 'tag.id', 'tag_closure.id_descendant') + .select((eb) => ['tag.id', eb.fn.count('asset.id').as('count')]) + .groupBy('tag.id') .execute(); const ids = result.filter(({ count }) => count === 0).map(({ id }) => id); if (ids.length > 0) { - await this.db.deleteFrom('tags').where('id', 'in', ids).execute(); + await this.db.deleteFrom('tag').where('id', 'in', ids).execute(); this.logger.log(`Deleted ${ids.length} empty tags`); } }); diff --git a/server/src/repositories/trash.repository.ts b/server/src/repositories/trash.repository.ts index 3389678c50..ee6fe3ace1 100644 --- a/server/src/repositories/trash.repository.ts +++ b/server/src/repositories/trash.repository.ts @@ -8,13 +8,13 @@ export class TrashRepository { constructor(@InjectKysely() private db: Kysely) {} getDeletedIds(): AsyncIterableIterator<{ id: string }> { - return this.db.selectFrom('assets').select(['id']).where('status', '=', AssetStatus.DELETED).stream(); + return this.db.selectFrom('asset').select(['id']).where('status', '=', AssetStatus.DELETED).stream(); } @GenerateSql({ params: [DummyValue.UUID] }) async restore(userId: string): Promise { const { numUpdatedRows } = await this.db - .updateTable('assets') + .updateTable('asset') .where('ownerId', '=', userId) .where('status', '=', AssetStatus.TRASHED) .set({ status: AssetStatus.ACTIVE, deletedAt: null }) @@ -26,7 +26,7 @@ export class TrashRepository { @GenerateSql({ params: [DummyValue.UUID] }) async empty(userId: string): Promise { const { numUpdatedRows } = await this.db - .updateTable('assets') + .updateTable('asset') .where('ownerId', '=', userId) .where('status', '=', AssetStatus.TRASHED) .set({ status: AssetStatus.DELETED }) @@ -42,7 +42,7 @@ export class TrashRepository { } const { numUpdatedRows } = await this.db - .updateTable('assets') + .updateTable('asset') .where('status', '=', AssetStatus.TRASHED) .where('id', 'in', ids) .set({ status: AssetStatus.ACTIVE, deletedAt: null }) diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index 91c4b71139..f809280d86 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -34,12 +34,12 @@ export interface UserFindOptions { withDeleted?: boolean; } -const withMetadata = (eb: ExpressionBuilder) => { +const withMetadata = (eb: ExpressionBuilder) => { return jsonArrayFrom( eb .selectFrom('user_metadata') .select(['user_metadata.key', 'user_metadata.value']) - .whereRef('users.id', '=', 'user_metadata.userId'), + .whereRef('user.id', '=', 'user_metadata.userId'), ).as('metadata'); }; @@ -52,11 +52,11 @@ export class UserRepository { options = options || {}; return this.db - .selectFrom('users') + .selectFrom('user') .select(columns.userAdmin) .select(withMetadata) - .where('users.id', '=', userId) - .$if(!options.withDeleted, (eb) => eb.where('users.deletedAt', 'is', null)) + .where('user.id', '=', userId) + .$if(!options.withDeleted, (eb) => eb.where('user.deletedAt', 'is', null)) .executeTakeFirst(); } @@ -71,21 +71,21 @@ export class UserRepository { @GenerateSql() getAdmin() { return this.db - .selectFrom('users') + .selectFrom('user') .select(columns.userAdmin) .select(withMetadata) - .where('users.isAdmin', '=', true) - .where('users.deletedAt', 'is', null) + .where('user.isAdmin', '=', true) + .where('user.deletedAt', 'is', null) .executeTakeFirst(); } @GenerateSql() async hasAdmin(): Promise { const admin = await this.db - .selectFrom('users') - .select('users.id') - .where('users.isAdmin', '=', true) - .where('users.deletedAt', 'is', null) + .selectFrom('user') + .select('user.id') + .where('user.isAdmin', '=', true) + .where('user.deletedAt', 'is', null) .executeTakeFirst(); return !!admin; @@ -94,59 +94,59 @@ export class UserRepository { @GenerateSql({ params: [DummyValue.UUID] }) getForPinCode(id: string) { return this.db - .selectFrom('users') - .select(['users.pinCode', 'users.password']) - .where('users.id', '=', id) - .where('users.deletedAt', 'is', null) + .selectFrom('user') + .select(['user.pinCode', 'user.password']) + .where('user.id', '=', id) + .where('user.deletedAt', 'is', null) .executeTakeFirstOrThrow(); } @GenerateSql({ params: [DummyValue.UUID] }) getForChangePassword(id: string) { return this.db - .selectFrom('users') - .select(['users.id', 'users.password']) - .where('users.id', '=', id) - .where('users.deletedAt', 'is', null) + .selectFrom('user') + .select(['user.id', 'user.password']) + .where('user.id', '=', id) + .where('user.deletedAt', 'is', null) .executeTakeFirstOrThrow(); } @GenerateSql({ params: [DummyValue.EMAIL] }) getByEmail(email: string, options?: { withPassword?: boolean }) { return this.db - .selectFrom('users') + .selectFrom('user') .select(columns.userAdmin) .select(withMetadata) .$if(!!options?.withPassword, (eb) => eb.select('password')) .where('email', '=', email) - .where('users.deletedAt', 'is', null) + .where('user.deletedAt', 'is', null) .executeTakeFirst(); } @GenerateSql({ params: [DummyValue.STRING] }) getByStorageLabel(storageLabel: string) { return this.db - .selectFrom('users') + .selectFrom('user') .select(columns.userAdmin) - .where('users.storageLabel', '=', storageLabel) - .where('users.deletedAt', 'is', null) + .where('user.storageLabel', '=', storageLabel) + .where('user.deletedAt', 'is', null) .executeTakeFirst(); } @GenerateSql({ params: [DummyValue.STRING] }) getByOAuthId(oauthId: string) { return this.db - .selectFrom('users') + .selectFrom('user') .select(columns.userAdmin) .select(withMetadata) - .where('users.oauthId', '=', oauthId) - .where('users.deletedAt', 'is', null) + .where('user.oauthId', '=', oauthId) + .where('user.deletedAt', 'is', null) .executeTakeFirst(); } @GenerateSql({ params: [DateTime.now().minus({ years: 1 })] }) getDeletedAfter(target: DateTime) { - return this.db.selectFrom('users').select(['id']).where('users.deletedAt', '<', target.toJSDate()).execute(); + return this.db.selectFrom('user').select(['id']).where('user.deletedAt', '<', target.toJSDate()).execute(); } @GenerateSql( @@ -155,18 +155,18 @@ export class UserRepository { ) getList({ id, withDeleted }: UserListFilter = {}) { return this.db - .selectFrom('users') + .selectFrom('user') .select(columns.userAdmin) .select(withMetadata) - .$if(!withDeleted, (eb) => eb.where('users.deletedAt', 'is', null)) - .$if(!!id, (eb) => eb.where('users.id', '=', id!)) + .$if(!withDeleted, (eb) => eb.where('user.deletedAt', 'is', null)) + .$if(!!id, (eb) => eb.where('user.id', '=', id!)) .orderBy('createdAt', 'desc') .execute(); } async create(dto: Insertable) { return this.db - .insertInto('users') + .insertInto('user') .values(dto) .returning(columns.userAdmin) .returning(withMetadata) @@ -175,10 +175,10 @@ export class UserRepository { update(id: string, dto: Updateable) { return this.db - .updateTable('users') + .updateTable('user') .set(dto) - .where('users.id', '=', asUuid(id)) - .where('users.deletedAt', 'is', null) + .where('user.id', '=', asUuid(id)) + .where('user.deletedAt', 'is', null) .returning(columns.userAdmin) .returning(withMetadata) .executeTakeFirstOrThrow(); @@ -186,9 +186,9 @@ export class UserRepository { restore(id: string) { return this.db - .updateTable('users') + .updateTable('user') .set({ status: UserStatus.ACTIVE, deletedAt: null }) - .where('users.id', '=', asUuid(id)) + .where('user.id', '=', asUuid(id)) .returning(columns.userAdmin) .returning(withMetadata) .executeTakeFirstOrThrow(); @@ -213,24 +213,24 @@ export class UserRepository { delete(user: { id: string }, hard?: boolean) { return hard - ? this.db.deleteFrom('users').where('id', '=', user.id).execute() - : this.db.updateTable('users').set({ deletedAt: new Date() }).where('id', '=', user.id).execute(); + ? this.db.deleteFrom('user').where('id', '=', user.id).execute() + : this.db.updateTable('user').set({ deletedAt: new Date() }).where('id', '=', user.id).execute(); } @GenerateSql() getUserStats() { return this.db - .selectFrom('users') - .leftJoin('assets', (join) => join.onRef('assets.ownerId', '=', 'users.id').on('assets.deletedAt', 'is', null)) - .leftJoin('exif', 'exif.assetId', 'assets.id') - .select(['users.id as userId', 'users.name as userName', 'users.quotaSizeInBytes']) + .selectFrom('user') + .leftJoin('asset', (join) => join.onRef('asset.ownerId', '=', 'user.id').on('asset.deletedAt', 'is', null)) + .leftJoin('asset_exif', 'asset_exif.assetId', 'asset.id') + .select(['user.id as userId', 'user.name as userName', 'user.quotaSizeInBytes']) .select((eb) => [ eb.fn .countAll() .filterWhere((eb) => eb.and([ - eb('assets.type', '=', sql.lit(AssetType.IMAGE)), - eb('assets.visibility', '!=', sql.lit(AssetVisibility.HIDDEN)), + eb('asset.type', '=', sql.lit(AssetType.IMAGE)), + eb('asset.visibility', '!=', sql.lit(AssetVisibility.HIDDEN)), ]), ) .as('photos'), @@ -238,20 +238,23 @@ export class UserRepository { .countAll() .filterWhere((eb) => eb.and([ - eb('assets.type', '=', sql.lit(AssetType.VIDEO)), - eb('assets.visibility', '!=', sql.lit(AssetVisibility.HIDDEN)), + eb('asset.type', '=', sql.lit(AssetType.VIDEO)), + eb('asset.visibility', '!=', sql.lit(AssetVisibility.HIDDEN)), ]), ) .as('videos'), eb.fn - .coalesce(eb.fn.sum('exif.fileSizeInByte').filterWhere('assets.libraryId', 'is', null), eb.lit(0)) + .coalesce( + eb.fn.sum('asset_exif.fileSizeInByte').filterWhere('asset.libraryId', 'is', null), + eb.lit(0), + ) .as('usage'), eb.fn .coalesce( eb.fn - .sum('exif.fileSizeInByte') + .sum('asset_exif.fileSizeInByte') .filterWhere((eb) => - eb.and([eb('assets.libraryId', 'is', null), eb('assets.type', '=', sql.lit(AssetType.IMAGE))]), + eb.and([eb('asset.libraryId', 'is', null), eb('asset.type', '=', sql.lit(AssetType.IMAGE))]), ), eb.lit(0), ) @@ -259,45 +262,45 @@ export class UserRepository { eb.fn .coalesce( eb.fn - .sum('exif.fileSizeInByte') + .sum('asset_exif.fileSizeInByte') .filterWhere((eb) => - eb.and([eb('assets.libraryId', 'is', null), eb('assets.type', '=', sql.lit(AssetType.VIDEO))]), + eb.and([eb('asset.libraryId', 'is', null), eb('asset.type', '=', sql.lit(AssetType.VIDEO))]), ), eb.lit(0), ) .as('usageVideos'), ]) - .groupBy('users.id') - .orderBy('users.createdAt', 'asc') + .groupBy('user.id') + .orderBy('user.createdAt', 'asc') .execute(); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.NUMBER] }) async updateUsage(id: string, delta: number): Promise { await this.db - .updateTable('users') + .updateTable('user') .set({ quotaUsageInBytes: sql`"quotaUsageInBytes" + ${delta}`, updatedAt: new Date() }) .where('id', '=', asUuid(id)) - .where('users.deletedAt', 'is', null) + .where('user.deletedAt', 'is', null) .execute(); } @GenerateSql({ params: [DummyValue.UUID] }) async syncUsage(id?: string) { const query = this.db - .updateTable('users') + .updateTable('user') .set({ quotaUsageInBytes: (eb) => eb - .selectFrom('assets') - .leftJoin('exif', 'exif.assetId', 'assets.id') - .select((eb) => eb.fn.coalesce(eb.fn.sum('exif.fileSizeInByte'), eb.lit(0)).as('usage')) - .where('assets.libraryId', 'is', null) - .where('assets.ownerId', '=', eb.ref('users.id')), + .selectFrom('asset') + .leftJoin('asset_exif', 'asset_exif.assetId', 'asset.id') + .select((eb) => eb.fn.coalesce(eb.fn.sum('asset_exif.fileSizeInByte'), eb.lit(0)).as('usage')) + .where('asset.libraryId', 'is', null) + .where('asset.ownerId', '=', eb.ref('user.id')), updatedAt: new Date(), }) - .where('users.deletedAt', 'is', null) - .$if(id != undefined, (eb) => eb.where('users.id', '=', asUuid(id!))); + .where('user.deletedAt', 'is', null) + .$if(id != undefined, (eb) => eb.where('user.id', '=', asUuid(id!))); await query.execute(); } diff --git a/server/src/repositories/view-repository.ts b/server/src/repositories/view-repository.ts index d0d4983e09..0fd74d299f 100644 --- a/server/src/repositories/view-repository.ts +++ b/server/src/repositories/view-repository.ts @@ -11,8 +11,8 @@ export class ViewRepository { @GenerateSql({ params: [DummyValue.UUID] }) async getUniqueOriginalPaths(userId: string) { const results = await this.db - .selectFrom('assets') - .select((eb) => eb.fn('substring', ['assets.originalPath', eb.val('^(.*/)[^/]*$')]).as('directoryPath')) + .selectFrom('asset') + .select((eb) => eb.fn('substring', ['asset.originalPath', eb.val('^(.*/)[^/]*$')]).as('directoryPath')) .distinct() .where('ownerId', '=', asUuid(userId)) .where('visibility', '=', AssetVisibility.TIMELINE) @@ -30,8 +30,8 @@ export class ViewRepository { const normalizedPath = partialPath.replaceAll(/\/$/g, ''); return this.db - .selectFrom('assets') - .selectAll('assets') + .selectFrom('asset') + .selectAll('asset') .$call(withExif) .where('ownerId', '=', asUuid(userId)) .where('visibility', '=', AssetVisibility.TIMELINE) @@ -42,7 +42,7 @@ export class ViewRepository { .where('originalPath', 'like', `%${normalizedPath}/%`) .where('originalPath', 'not like', `%${normalizedPath}/%/%`) .orderBy( - (eb) => eb.fn('regexp_replace', ['assets.originalPath', eb.val('.*/(.+)'), eb.val(String.raw`\1`)]), + (eb) => eb.fn('regexp_replace', ['asset.originalPath', eb.val('.*/(.+)'), eb.val(String.raw`\1`)]), 'asc', ) .execute(); diff --git a/server/src/schema/functions.ts b/server/src/schema/functions.ts index 1b7a4f884c..5577169227 100644 --- a/server/src/schema/functions.ts +++ b/server/src/schema/functions.ts @@ -28,7 +28,7 @@ export const album_user_after_insert = registerFunction({ language: 'PLPGSQL', body: ` BEGIN - UPDATE albums SET "updatedAt" = clock_timestamp(), "updateId" = immich_uuid_v7(clock_timestamp()) + UPDATE album SET "updatedAt" = clock_timestamp(), "updateId" = immich_uuid_v7(clock_timestamp()) WHERE "id" IN (SELECT DISTINCT "albumsId" FROM inserted_rows); RETURN NULL; END`, @@ -80,83 +80,83 @@ export const ll_to_earth_public = registerFunction({ body: `SELECT public.cube(public.cube(public.cube(public.earth()*cos(radians(latitude))*cos(radians(longitude))),public.earth()*cos(radians(latitude))*sin(radians(longitude))),public.earth()*sin(radians(latitude)))::public.earth`, }); -export const users_delete_audit = registerFunction({ - name: 'users_delete_audit', +export const user_delete_audit = registerFunction({ + name: 'user_delete_audit', returnType: 'TRIGGER', language: 'PLPGSQL', body: ` BEGIN - INSERT INTO users_audit ("userId") + INSERT INTO user_audit ("userId") SELECT "id" FROM OLD; RETURN NULL; END`, }); -export const partners_delete_audit = registerFunction({ - name: 'partners_delete_audit', +export const partner_delete_audit = registerFunction({ + name: 'partner_delete_audit', returnType: 'TRIGGER', language: 'PLPGSQL', body: ` BEGIN - INSERT INTO partners_audit ("sharedById", "sharedWithId") + INSERT INTO partner_audit ("sharedById", "sharedWithId") SELECT "sharedById", "sharedWithId" FROM OLD; RETURN NULL; END`, }); -export const assets_delete_audit = registerFunction({ - name: 'assets_delete_audit', +export const asset_delete_audit = registerFunction({ + name: 'asset_delete_audit', returnType: 'TRIGGER', language: 'PLPGSQL', body: ` BEGIN - INSERT INTO assets_audit ("assetId", "ownerId") + INSERT INTO asset_audit ("assetId", "ownerId") SELECT "id", "ownerId" FROM OLD; RETURN NULL; END`, }); -export const albums_delete_audit = registerFunction({ - name: 'albums_delete_audit', +export const album_delete_audit = registerFunction({ + name: 'album_delete_audit', returnType: 'TRIGGER', language: 'PLPGSQL', body: ` BEGIN - INSERT INTO albums_audit ("albumId", "userId") + INSERT INTO album_audit ("albumId", "userId") SELECT "id", "ownerId" FROM OLD; RETURN NULL; END`, }); -export const album_assets_delete_audit = registerFunction({ - name: 'album_assets_delete_audit', +export const album_asset_delete_audit = registerFunction({ + name: 'album_asset_delete_audit', returnType: 'TRIGGER', language: 'PLPGSQL', body: ` BEGIN - INSERT INTO album_assets_audit ("albumId", "assetId") + INSERT INTO album_asset_audit ("albumId", "assetId") SELECT "albumsId", "assetsId" FROM OLD - WHERE "albumsId" IN (SELECT "id" FROM albums WHERE "id" IN (SELECT "albumsId" FROM OLD)); + WHERE "albumsId" IN (SELECT "id" FROM album WHERE "id" IN (SELECT "albumsId" FROM OLD)); RETURN NULL; END`, }); -export const album_users_delete_audit = registerFunction({ - name: 'album_users_delete_audit', +export const album_user_delete_audit = registerFunction({ + name: 'album_user_delete_audit', returnType: 'TRIGGER', language: 'PLPGSQL', body: ` BEGIN - INSERT INTO albums_audit ("albumId", "userId") + INSERT INTO album_audit ("albumId", "userId") SELECT "albumsId", "usersId" FROM OLD; IF pg_trigger_depth() = 1 THEN - INSERT INTO album_users_audit ("albumId", "userId") + INSERT INTO album_user_audit ("albumId", "userId") SELECT "albumsId", "usersId" FROM OLD; END IF; @@ -165,41 +165,67 @@ export const album_users_delete_audit = registerFunction({ END`, }); -export const memories_delete_audit = registerFunction({ - name: 'memories_delete_audit', +export const memory_delete_audit = registerFunction({ + name: 'memory_delete_audit', returnType: 'TRIGGER', language: 'PLPGSQL', body: ` BEGIN - INSERT INTO memories_audit ("memoryId", "userId") + INSERT INTO memory_audit ("memoryId", "userId") SELECT "id", "ownerId" FROM OLD; RETURN NULL; END`, }); -export const memory_assets_delete_audit = registerFunction({ - name: 'memory_assets_delete_audit', +export const memory_asset_delete_audit = registerFunction({ + name: 'memory_asset_delete_audit', returnType: 'TRIGGER', language: 'PLPGSQL', body: ` BEGIN - INSERT INTO memory_assets_audit ("memoryId", "assetId") + INSERT INTO memory_asset_audit ("memoryId", "assetId") SELECT "memoriesId", "assetsId" FROM OLD - WHERE "memoriesId" IN (SELECT "id" FROM memories WHERE "id" IN (SELECT "memoriesId" FROM OLD)); + WHERE "memoriesId" IN (SELECT "id" FROM memory WHERE "id" IN (SELECT "memoriesId" FROM OLD)); RETURN NULL; END`, }); -export const stacks_delete_audit = registerFunction({ - name: 'stacks_delete_audit', +export const stack_delete_audit = registerFunction({ + name: 'stack_delete_audit', returnType: 'TRIGGER', language: 'PLPGSQL', body: ` BEGIN - INSERT INTO stacks_audit ("stackId", "userId") + INSERT INTO stack_audit ("stackId", "userId") SELECT "id", "ownerId" FROM OLD; RETURN NULL; END`, }); + +export const person_delete_audit = registerFunction({ + name: 'person_delete_audit', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + INSERT INTO person_audit ("personId", "ownerId") + SELECT "id", "ownerId" + FROM OLD; + RETURN NULL; + END`, +}); + +export const user_metadata_audit = registerFunction({ + name: 'user_metadata_audit', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + INSERT INTO user_metadata_audit ("userId", "key") + SELECT "userId", "key" + FROM OLD; + RETURN NULL; + END`, +}); diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index 6512ccc225..ba25a65d4d 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -1,19 +1,21 @@ import { asset_face_source_type, asset_visibility_enum, assets_status_enum } from 'src/schema/enums'; import { + album_delete_audit, album_user_after_insert, - album_users_delete_audit, - albums_delete_audit, - assets_delete_audit, + album_user_delete_audit, + asset_delete_audit, f_concat_ws, f_unaccent, immich_uuid_v7, ll_to_earth_public, - memories_delete_audit, - memory_assets_delete_audit, - partners_delete_audit, - stacks_delete_audit, + memory_asset_delete_audit, + memory_delete_audit, + partner_delete_audit, + person_delete_audit, + stack_delete_audit, updated_at, - users_delete_audit, + user_delete_audit, + user_metadata_audit, } from 'src/schema/functions'; import { ActivityTable } from 'src/schema/tables/activity.table'; import { AlbumAssetAuditTable } from 'src/schema/tables/album-asset-audit.table'; @@ -24,12 +26,12 @@ import { AlbumUserTable } from 'src/schema/tables/album-user.table'; import { AlbumTable } from 'src/schema/tables/album.table'; import { ApiKeyTable } from 'src/schema/tables/api-key.table'; import { AssetAuditTable } from 'src/schema/tables/asset-audit.table'; +import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; -import { AssetFileTable } from 'src/schema/tables/asset-files.table'; +import { AssetFileTable } from 'src/schema/tables/asset-file.table'; import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; import { AssetTable } from 'src/schema/tables/asset.table'; import { AuditTable } from 'src/schema/tables/audit.table'; -import { ExifTable } from 'src/schema/tables/exif.table'; import { FaceSearchTable } from 'src/schema/tables/face-search.table'; import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table'; import { LibraryTable } from 'src/schema/tables/library.table'; @@ -42,6 +44,7 @@ import { NaturalEarthCountriesTable } from 'src/schema/tables/natural-earth-coun import { NotificationTable } from 'src/schema/tables/notification.table'; import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table'; import { PartnerTable } from 'src/schema/tables/partner.table'; +import { PersonAuditTable } from 'src/schema/tables/person-audit.table'; import { PersonTable } from 'src/schema/tables/person.table'; import { SessionTable } from 'src/schema/tables/session.table'; import { SharedLinkAssetTable } from 'src/schema/tables/shared-link-asset.table'; @@ -55,6 +58,7 @@ import { TagAssetTable } from 'src/schema/tables/tag-asset.table'; import { TagClosureTable } from 'src/schema/tables/tag-closure.table'; import { TagTable } from 'src/schema/tables/tag.table'; import { UserAuditTable } from 'src/schema/tables/user-audit.table'; +import { UserMetadataAuditTable } from 'src/schema/tables/user-metadata-audit.table'; 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'; @@ -78,7 +82,7 @@ export class ImmichDatabase { AssetTable, AssetFileTable, AuditTable, - ExifTable, + AssetExifTable, FaceSearchTable, GeodataPlacesTable, LibraryTable, @@ -92,6 +96,7 @@ export class ImmichDatabase { PartnerAuditTable, PartnerTable, PersonTable, + PersonAuditTable, SessionTable, SharedLinkAssetTable, SharedLinkTable, @@ -105,6 +110,7 @@ export class ImmichDatabase { TagClosureTable, UserAuditTable, UserMetadataTable, + UserMetadataAuditTable, UserTable, VersionHistoryTable, ]; @@ -115,15 +121,17 @@ export class ImmichDatabase { f_concat_ws, f_unaccent, ll_to_earth_public, - users_delete_audit, - partners_delete_audit, - assets_delete_audit, - albums_delete_audit, + user_delete_audit, + partner_delete_audit, + asset_delete_audit, + album_delete_audit, album_user_after_insert, - album_users_delete_audit, - memories_delete_audit, - memory_assets_delete_audit, - stacks_delete_audit, + album_user_delete_audit, + memory_delete_audit, + memory_asset_delete_audit, + stack_delete_audit, + person_delete_audit, + user_metadata_audit, ]; enum = [assets_status_enum, asset_face_source_type, asset_visibility_enum]; @@ -137,47 +145,71 @@ export interface Migrations { export interface DB { activity: ActivityTable; - albums: AlbumTable; - albums_audit: AlbumAuditTable; - albums_assets_assets: AlbumAssetTable; - album_assets_audit: AlbumAssetAuditTable; - albums_shared_users_users: AlbumUserTable; - album_users_audit: AlbumUserAuditTable; - api_keys: ApiKeyTable; - asset_faces: AssetFaceTable; - asset_files: AssetFileTable; + + album: AlbumTable; + album_audit: AlbumAuditTable; + album_asset: AlbumAssetTable; + album_asset_audit: AlbumAssetAuditTable; + album_user: AlbumUserTable; + album_user_audit: AlbumUserAuditTable; + + api_key: ApiKeyTable; + + asset: AssetTable; + asset_exif: AssetExifTable; + asset_face: AssetFaceTable; + asset_file: AssetFileTable; asset_job_status: AssetJobStatusTable; - asset_stack: StackTable; - assets: AssetTable; - assets_audit: AssetAuditTable; + asset_audit: AssetAuditTable; + audit: AuditTable; - exif: ExifTable; + face_search: FaceSearchTable; + geodata_places: GeodataPlacesTable; - libraries: LibraryTable; - memories: MemoryTable; - memories_audit: MemoryAuditTable; - memories_assets_assets: MemoryAssetTable; - memory_assets_audit: MemoryAssetAuditTable; + + library: LibraryTable; + + memory: MemoryTable; + memory_audit: MemoryAuditTable; + memory_asset: MemoryAssetTable; + memory_asset_audit: MemoryAssetAuditTable; + migrations: Migrations; - notifications: NotificationTable; + + notification: NotificationTable; + move_history: MoveTable; + naturalearth_countries: NaturalEarthCountriesTable; - partners_audit: PartnerAuditTable; - partners: PartnerTable; + + partner: PartnerTable; + partner_audit: PartnerAuditTable; + person: PersonTable; - sessions: SessionTable; - session_sync_checkpoints: SessionSyncCheckpointTable; - shared_link__asset: SharedLinkAssetTable; - shared_links: SharedLinkTable; + person_audit: PersonAuditTable; + + session: SessionTable; + session_sync_checkpoint: SessionSyncCheckpointTable; + + shared_link: SharedLinkTable; + shared_link_asset: SharedLinkAssetTable; + smart_search: SmartSearchTable; - stacks_audit: StackAuditTable; + + stack: StackTable; + stack_audit: StackAuditTable; + system_metadata: SystemMetadataTable; + + tag: TagTable; tag_asset: TagAssetTable; - tags: TagTable; - tags_closure: TagClosureTable; + tag_closure: TagClosureTable; + + user: UserTable; + user_audit: UserAuditTable; user_metadata: UserMetadataTable; - users: UserTable; - users_audit: UserAuditTable; + user_metadata_audit: UserMetadataAuditTable; + version_history: VersionHistoryTable; } diff --git a/server/src/schema/migrations/1752152941084-PeopleAuditTable.ts b/server/src/schema/migrations/1752152941084-PeopleAuditTable.ts new file mode 100644 index 0000000000..3a3da0ded5 --- /dev/null +++ b/server/src/schema/migrations/1752152941084-PeopleAuditTable.ts @@ -0,0 +1,41 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION person_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO person_audit ("personId", "ownerId") + SELECT "id", "ownerId" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE TABLE "person_audit" ( + "id" uuid NOT NULL DEFAULT immich_uuid_v7(), + "personId" uuid NOT NULL, + "ownerId" uuid NOT NULL, + "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), + CONSTRAINT "PK_46c1ad23490b9312ffaa052aa59" PRIMARY KEY ("id") +);`.execute(db); + await sql`CREATE INDEX "IDX_person_audit_person_id" ON "person_audit" ("personId");`.execute(db); + await sql`CREATE INDEX "IDX_person_audit_owner_id" ON "person_audit" ("ownerId");`.execute(db); + await sql`CREATE INDEX "IDX_person_audit_deleted_at" ON "person_audit" ("deletedAt");`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "person_delete_audit" + AFTER DELETE ON "person" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION person_delete_audit();`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_person_delete_audit', '{"type":"function","name":"person_delete_audit","sql":"CREATE OR REPLACE FUNCTION person_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO person_audit (\\"personId\\", \\"ownerId\\")\\n SELECT \\"id\\", \\"ownerId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_person_delete_audit', '{"type":"trigger","name":"person_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"person_delete_audit\\"\\n AFTER DELETE ON \\"person\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION person_delete_audit();"}'::jsonb);`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP TRIGGER "person_delete_audit" ON "person";`.execute(db); + await sql`DROP TABLE "person_audit";`.execute(db); + await sql`DROP FUNCTION person_delete_audit;`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_person_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_person_delete_audit';`.execute(db); +} diff --git a/server/src/schema/migrations/1752161055253-RenameGeodataPKConstraint.ts b/server/src/schema/migrations/1752161055253-RenameGeodataPKConstraint.ts new file mode 100644 index 0000000000..686791e201 --- /dev/null +++ b/server/src/schema/migrations/1752161055253-RenameGeodataPKConstraint.ts @@ -0,0 +1,9 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "geodata_places" DROP CONSTRAINT IF EXISTS "PK_c29918988912ef4036f3d7fbff4";`.execute(db); + await sql`ALTER TABLE "geodata_places" DROP CONSTRAINT IF EXISTS "geodata_places_pkey"`.execute(db); + await sql`ALTER TABLE "geodata_places" ADD CONSTRAINT "geodata_places_pkey" PRIMARY KEY ("id");`.execute(db); +} + +export async function down(): Promise {} diff --git a/server/src/schema/migrations/1752161055254-AddActivityAssetFk.ts b/server/src/schema/migrations/1752161055254-AddActivityAssetFk.ts new file mode 100644 index 0000000000..938c716014 --- /dev/null +++ b/server/src/schema/migrations/1752161055254-AddActivityAssetFk.ts @@ -0,0 +1,19 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`DELETE FROM activity AS a + WHERE a."assetId" IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM albums_assets_assets AS aaa + WHERE a."albumId" = aaa."albumsId" + AND a."assetId" = aaa."assetsId" + );`.execute(db); + await sql`ALTER TABLE "activity" ADD CONSTRAINT "fk_activity_album_asset_composite" FOREIGN KEY ("albumId", "assetId") REFERENCES "albums_assets_assets" ("albumsId", "assetsId") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); + await sql`CREATE INDEX "IDX_86102d85cfa7f196073aebff68" ON "activity" ("albumId", "assetId")`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP INDEX "IDX_86102d85cfa7f196073aebff68";`.execute(db); + await sql`ALTER TABLE "activity" DROP CONSTRAINT "fk_activity_album_asset_composite";`.execute(db); +} diff --git a/server/src/schema/migrations/1752169992364-AddIsPendingSyncReset.ts b/server/src/schema/migrations/1752169992364-AddIsPendingSyncReset.ts new file mode 100644 index 0000000000..6264831181 --- /dev/null +++ b/server/src/schema/migrations/1752169992364-AddIsPendingSyncReset.ts @@ -0,0 +1,9 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "sessions" ADD "isPendingSyncReset" boolean NOT NULL DEFAULT false;`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "sessions" DROP COLUMN "isPendingSyncReset";`.execute(db); +} diff --git a/server/src/schema/migrations/1752250924342-UserMetadataSync.ts b/server/src/schema/migrations/1752250924342-UserMetadataSync.ts new file mode 100644 index 0000000000..20778d8010 --- /dev/null +++ b/server/src/schema/migrations/1752250924342-UserMetadataSync.ts @@ -0,0 +1,56 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION user_metadata_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO user_metadata_audit ("userId", "key") + SELECT "userId", "key" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE TABLE "user_metadata_audit" ( + "id" uuid NOT NULL DEFAULT immich_uuid_v7(), + "userId" uuid NOT NULL, + "key" character varying NOT NULL, + "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), + CONSTRAINT "PK_15d5cc4d65ac966233b9921acac" PRIMARY KEY ("id") +);`.execute(db); + await sql`CREATE INDEX "IDX_user_metadata_audit_user_id" ON "user_metadata_audit" ("userId");`.execute(db); + await sql`CREATE INDEX "IDX_user_metadata_audit_key" ON "user_metadata_audit" ("key");`.execute(db); + await sql`CREATE INDEX "IDX_user_metadata_audit_deleted_at" ON "user_metadata_audit" ("deletedAt");`.execute(db); + await sql`ALTER TABLE "user_metadata" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db); + await sql`ALTER TABLE "user_metadata" ADD "updatedAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db); + await sql`CREATE INDEX "IDX_user_metadata_update_id" ON "user_metadata" ("updateId");`.execute(db); + await sql`CREATE INDEX "IDX_user_metadata_updated_at" ON "user_metadata" ("updatedAt");`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "user_metadata_audit" + AFTER DELETE ON "user_metadata" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION user_metadata_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "user_metadata_updated_at" + BEFORE UPDATE ON "user_metadata" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_user_metadata_audit', '{"type":"function","name":"user_metadata_audit","sql":"CREATE OR REPLACE FUNCTION user_metadata_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO user_metadata_audit (\\"userId\\", \\"key\\")\\n SELECT \\"userId\\", \\"key\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_user_metadata_audit', '{"type":"trigger","name":"user_metadata_audit","sql":"CREATE OR REPLACE TRIGGER \\"user_metadata_audit\\"\\n AFTER DELETE ON \\"user_metadata\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION user_metadata_audit();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_user_metadata_updated_at', '{"type":"trigger","name":"user_metadata_updated_at","sql":"CREATE OR REPLACE TRIGGER \\"user_metadata_updated_at\\"\\n BEFORE UPDATE ON \\"user_metadata\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP TRIGGER "user_metadata_audit" ON "user_metadata";`.execute(db); + await sql`DROP TRIGGER "user_metadata_updated_at" ON "user_metadata";`.execute(db); + await sql`DROP INDEX "IDX_user_metadata_update_id";`.execute(db); + await sql`DROP INDEX "IDX_user_metadata_updated_at";`.execute(db); + await sql`ALTER TABLE "user_metadata" DROP COLUMN "updateId";`.execute(db); + await sql`ALTER TABLE "user_metadata" DROP COLUMN "updatedAt";`.execute(db); + await sql`DROP TABLE "user_metadata_audit";`.execute(db); + await sql`DROP FUNCTION user_metadata_audit;`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_user_metadata_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_user_metadata_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_user_metadata_updated_at';`.execute(db); +} diff --git a/server/src/schema/migrations/1752267649968-StandardizeNames.ts b/server/src/schema/migrations/1752267649968-StandardizeNames.ts new file mode 100644 index 0000000000..991c9d3ce2 --- /dev/null +++ b/server/src/schema/migrations/1752267649968-StandardizeNames.ts @@ -0,0 +1,1200 @@ +import { Kysely, sql } from 'kysely'; + +type RenameItem = { oldName: string; newName: string }; + +const tables: RenameItem[] = [ + { oldName: 'album_assets_audit', newName: 'album_asset_audit' }, + { oldName: 'albums_assets_assets', newName: 'album_asset' }, + { oldName: 'albums_audit', newName: 'album_audit' }, + { oldName: 'album_users_audit', newName: 'album_user_audit' }, + { oldName: 'albums_shared_users_users', newName: 'album_user' }, + { oldName: 'albums', newName: 'album' }, + { oldName: 'api_keys', newName: 'api_key' }, + { oldName: 'assets_audit', newName: 'asset_audit' }, + { oldName: 'assets', newName: 'asset' }, + { oldName: 'asset_faces', newName: 'asset_face' }, + { oldName: 'asset_files', newName: 'asset_file' }, + { oldName: 'exif', newName: 'asset_exif' }, + { oldName: 'libraries', newName: 'library' }, + { oldName: 'memory_assets_audit', newName: 'memory_asset_audit' }, + { oldName: 'memories_assets_assets', newName: 'memory_asset' }, + { oldName: 'memories_audit', newName: 'memory_audit' }, + { oldName: 'memories', newName: 'memory' }, + { oldName: 'notifications', newName: 'notification' }, + { oldName: 'partners_audit', newName: 'partner_audit' }, + { oldName: 'partners', newName: 'partner' }, + { oldName: 'sessions', newName: 'session' }, + { oldName: 'shared_link__asset', newName: 'shared_link_asset' }, + { oldName: 'shared_links', newName: 'shared_link' }, + { oldName: 'stacks_audit', newName: 'stack_audit' }, + { oldName: 'asset_stack', newName: 'stack' }, + { oldName: 'session_sync_checkpoints', newName: 'session_sync_checkpoint' }, + { oldName: 'tags_closure', newName: 'tag_closure' }, + { oldName: 'tags', newName: 'tag' }, + { oldName: 'users_audit', newName: 'user_audit' }, + { oldName: 'users', newName: 'user' }, +]; + +export async function up(db: Kysely): Promise { + for (const { oldName, newName } of tables) { + await sql.raw(`ALTER TABLE "${oldName}" RENAME TO "${newName}"`).execute(db); + } + + await sql`CREATE OR REPLACE FUNCTION album_user_after_insert() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + UPDATE album SET "updatedAt" = clock_timestamp(), "updateId" = immich_uuid_v7(clock_timestamp()) + WHERE "id" IN (SELECT DISTINCT "albumsId" FROM inserted_rows); + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION user_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO user_audit ("userId") + SELECT "id" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION partner_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO partner_audit ("sharedById", "sharedWithId") + SELECT "sharedById", "sharedWithId" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION asset_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO asset_audit ("assetId", "ownerId") + SELECT "id", "ownerId" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION album_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO album_audit ("albumId", "userId") + SELECT "id", "ownerId" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION album_asset_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO album_asset_audit ("albumId", "assetId") + SELECT "albumsId", "assetsId" FROM OLD + WHERE "albumsId" IN (SELECT "id" FROM album WHERE "id" IN (SELECT "albumsId" FROM OLD)); + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION album_user_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO album_audit ("albumId", "userId") + SELECT "albumsId", "usersId" + FROM OLD; + + IF pg_trigger_depth() = 1 THEN + INSERT INTO album_user_audit ("albumId", "userId") + SELECT "albumsId", "usersId" + FROM OLD; + END IF; + + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION memory_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO memory_audit ("memoryId", "userId") + SELECT "id", "ownerId" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION memory_asset_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO memory_asset_audit ("memoryId", "assetId") + SELECT "memoriesId", "assetsId" FROM OLD + WHERE "memoriesId" IN (SELECT "id" FROM memory WHERE "id" IN (SELECT "memoriesId" FROM OLD)); + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION stack_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO stack_audit ("stackId", "userId") + SELECT "id", "ownerId" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`DROP TRIGGER "users_delete_audit" ON "user";`.execute(db); + await sql`DROP TRIGGER "users_updated_at" ON "user";`.execute(db); + await sql`DROP TRIGGER "libraries_updated_at" ON "library";`.execute(db); + await sql`DROP TRIGGER "stacks_delete_audit" ON "stack";`.execute(db); + await sql`DROP TRIGGER "stacks_updated_at" ON "stack";`.execute(db); + await sql`DROP TRIGGER "assets_delete_audit" ON "asset";`.execute(db); + await sql`DROP TRIGGER "assets_updated_at" ON "asset";`.execute(db); + await sql`DROP TRIGGER "albums_updated_at" ON "album";`.execute(db); + await sql`DROP TRIGGER "albums_delete_audit" ON "album";`.execute(db); + await sql`DROP TRIGGER "album_assets_updated_at" ON "album_asset";`.execute(db); + await sql`DROP TRIGGER "album_assets_delete_audit" ON "album_asset";`.execute(db); + await sql`DROP TRIGGER "activity_updated_at" ON "activity";`.execute(db); + await sql`DROP TRIGGER "album_users_delete_audit" ON "album_user";`.execute(db); + await sql`DROP TRIGGER "album_users_updated_at" ON "album_user";`.execute(db); + await sql`DROP TRIGGER "api_keys_updated_at" ON "api_key";`.execute(db); + await sql`DROP TRIGGER "asset_exif_updated_at" ON "asset_exif";`.execute(db); + await sql`DROP TRIGGER "person_updated_at" ON "person";`.execute(db); + await sql`DROP TRIGGER "asset_files_updated_at" ON "asset_file";`.execute(db); + await sql`DROP TRIGGER "memories_updated_at" ON "memory";`.execute(db); + await sql`DROP TRIGGER "memories_delete_audit" ON "memory";`.execute(db); + await sql`DROP TRIGGER "memory_assets_updated_at" ON "memory_asset";`.execute(db); + await sql`DROP TRIGGER "memory_assets_delete_audit" ON "memory_asset";`.execute(db); + await sql`DROP TRIGGER "notifications_updated_at" ON "notification";`.execute(db); + await sql`DROP TRIGGER "partners_delete_audit" ON "partner";`.execute(db); + await sql`DROP TRIGGER "partners_updated_at" ON "partner";`.execute(db); + await sql`DROP TRIGGER "sessions_updated_at" ON "session";`.execute(db); + await sql`DROP TRIGGER "session_sync_checkpoints_updated_at" ON "session_sync_checkpoint";`.execute(db); + await sql`DROP TRIGGER "tags_updated_at" ON "tag";`.execute(db); + await sql`ALTER TABLE "user_metadata_audit" RENAME CONSTRAINT "PK_15d5cc4d65ac966233b9921acac" TO "user_metadata_audit_pkey";`.execute(db); + await sql`ALTER TABLE "user" RENAME CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" TO "user_pkey";`.execute(db); + await sql`ALTER TABLE "user" RENAME CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" TO "user_email_uq";`.execute(db); + await sql`ALTER TABLE "user" RENAME CONSTRAINT "UQ_b309cf34fa58137c416b32cea3a" TO "user_storageLabel_uq";`.execute(db); + await sql`ALTER TABLE "library" RENAME CONSTRAINT "PK_505fedfcad00a09b3734b4223de" TO "library_pkey";`.execute(db); + await sql`ALTER TABLE "library" RENAME CONSTRAINT "FK_0f6fc2fb195f24d19b0fb0d57c1" TO "library_ownerId_fkey";`.execute(db); + await sql`ALTER TABLE "stack" RENAME CONSTRAINT "PK_74a27e7fcbd5852463d0af3034b" TO "stack_pkey";`.execute(db); + await sql`ALTER TABLE "stack" RENAME CONSTRAINT "FK_91704e101438fd0653f582426dc" TO "stack_primaryAssetId_fkey";`.execute(db); + await sql`ALTER TABLE "stack" RENAME CONSTRAINT "FK_c05079e542fd74de3b5ecb5c1c8" TO "stack_ownerId_fkey";`.execute(db); + await sql`ALTER TABLE "stack" RENAME CONSTRAINT "REL_91704e101438fd0653f582426d" TO "stack_primaryAssetId_uq";`.execute(db); + await sql`ALTER TABLE "asset" RENAME CONSTRAINT "PK_da96729a8b113377cfb6a62439c" TO "asset_pkey";`.execute(db); + await sql`ALTER TABLE "asset" RENAME CONSTRAINT "FK_2c5ac0d6fb58b238fd2068de67d" TO "asset_ownerId_fkey";`.execute(db); + await sql`ALTER TABLE "asset" RENAME CONSTRAINT "FK_16294b83fa8c0149719a1f631ef" TO "asset_livePhotoVideoId_fkey";`.execute(db); + await sql`ALTER TABLE "asset" RENAME CONSTRAINT "FK_9977c3c1de01c3d848039a6b90c" TO "asset_libraryId_fkey";`.execute(db); + await sql`ALTER TABLE "asset" RENAME CONSTRAINT "FK_f15d48fa3ea5e4bda05ca8ab207" TO "asset_stackId_fkey";`.execute(db); + await sql`ALTER TABLE "album" RENAME CONSTRAINT "PK_7f71c7b5bc7c87b8f94c9a93a00" TO "album_pkey";`.execute(db); + await sql`ALTER TABLE "album" RENAME CONSTRAINT "FK_b22c53f35ef20c28c21637c85f4" TO "album_ownerId_fkey";`.execute(db); + await sql`ALTER TABLE "album" RENAME CONSTRAINT "FK_05895aa505a670300d4816debce" TO "album_albumThumbnailAssetId_fkey";`.execute(db); + await sql`ALTER TABLE "album_asset" RENAME CONSTRAINT "PK_c67bc36fa845fb7b18e0e398180" TO "album_asset_pkey";`.execute(db); + await sql`ALTER TABLE "album_asset" RENAME CONSTRAINT "FK_e590fa396c6898fcd4a50e40927" TO "album_asset_albumsId_fkey";`.execute(db); + await sql`ALTER TABLE "album_asset" RENAME CONSTRAINT "FK_4bd1303d199f4e72ccdf998c621" TO "album_asset_assetsId_fkey";`.execute(db); + await sql`ALTER TABLE "activity" RENAME CONSTRAINT "PK_24625a1d6b1b089c8ae206fe467" TO "activity_pkey";`.execute(db); + await sql`ALTER TABLE "activity" RENAME CONSTRAINT "FK_1af8519996fbfb3684b58df280b" TO "activity_albumId_fkey";`.execute(db); + await sql`ALTER TABLE "activity" RENAME CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea" TO "activity_userId_fkey";`.execute(db); + await sql`ALTER TABLE "activity" RENAME CONSTRAINT "FK_8091ea76b12338cb4428d33d782" TO "activity_assetId_fkey";`.execute(db); + await sql`ALTER TABLE "activity" RENAME CONSTRAINT "CHK_2ab1e70f113f450eb40c1e3ec8" TO "activity_like_check";`.execute(db); + await sql`ALTER TABLE "activity" RENAME CONSTRAINT "fk_activity_album_asset_composite" TO "activity_albumId_assetId_fkey";`.execute(db); + await sql`ALTER TABLE "album_asset_audit" RENAME CONSTRAINT "PK_32969b576ec8f78d84f37c2eb2d" TO "album_asset_audit_pkey";`.execute(db); + await sql`ALTER TABLE "album_asset_audit" RENAME CONSTRAINT "FK_8047b44b812619a3c75a2839b0d" TO "album_asset_audit_albumId_fkey";`.execute(db); + await sql`ALTER TABLE "album_audit" RENAME CONSTRAINT "PK_c75efea8d4dce316ad29b851a8b" TO "album_audit_pkey";`.execute(db); + await sql`ALTER TABLE "album_user_audit" RENAME CONSTRAINT "PK_f479a2e575b7ebc9698362c1688" TO "album_user_audit_pkey";`.execute(db); + await sql`ALTER TABLE "album_user" RENAME CONSTRAINT "PK_7df55657e0b2e8b626330a0ebc8" TO "album_user_pkey";`.execute(db); + await sql`ALTER TABLE "album_user" RENAME CONSTRAINT "FK_427c350ad49bd3935a50baab737" TO "album_user_albumsId_fkey";`.execute(db); + await sql`ALTER TABLE "album_user" RENAME CONSTRAINT "FK_f48513bf9bccefd6ff3ad30bd06" TO "album_user_usersId_fkey";`.execute(db); + await sql`ALTER TABLE "api_key" RENAME CONSTRAINT "PK_5c8a79801b44bd27b79228e1dad" TO "api_key_pkey";`.execute(db); + await sql`ALTER TABLE "api_key" RENAME CONSTRAINT "FK_6c2e267ae764a9413b863a29342" TO "api_key_userId_fkey";`.execute(db); + await sql`ALTER TABLE "asset_audit" RENAME CONSTRAINT "PK_99bd5c015f81a641927a32b4212" TO "asset_audit_pkey";`.execute(db); + await sql`ALTER TABLE "asset_exif" RENAME CONSTRAINT "PK_c0117fdbc50b917ef9067740c44" TO "asset_exif_pkey";`.execute(db); + await sql`ALTER TABLE "asset_exif" RENAME CONSTRAINT "FK_c0117fdbc50b917ef9067740c44" TO "asset_exif_assetId_fkey";`.execute(db); + await sql`ALTER TABLE "person" RENAME CONSTRAINT "PK_5fdaf670315c4b7e70cce85daa3" TO "person_pkey";`.execute(db); + await sql`ALTER TABLE "person" RENAME CONSTRAINT "FK_5527cc99f530a547093f9e577b6" TO "person_ownerId_fkey";`.execute(db); + await sql`ALTER TABLE "person" RENAME CONSTRAINT "FK_2bbabe31656b6778c6b87b61023" TO "person_faceAssetId_fkey";`.execute(db); + await sql`ALTER TABLE "person" RENAME CONSTRAINT "CHK_b0f82b0ed662bfc24fbb58bb45" TO "person_birthDate_chk";`.execute(db); + await sql`ALTER TABLE "asset_face" RENAME CONSTRAINT "PK_6df76ab2eb6f5b57b7c2f1fc684" TO "asset_face_pkey";`.execute(db); + await sql`ALTER TABLE "asset_face" RENAME CONSTRAINT "FK_02a43fd0b3c50fb6d7f0cb7282c" TO "asset_face_assetId_fkey";`.execute(db); + await sql`ALTER TABLE "asset_face" RENAME CONSTRAINT "FK_95ad7106dd7b484275443f580f9" TO "asset_face_personId_fkey";`.execute(db); + await sql`ALTER TABLE "asset_file" RENAME CONSTRAINT "PK_c41dc3e9ef5e1c57ca5a08a0004" TO "asset_file_pkey";`.execute(db); + await sql`ALTER TABLE "asset_file" RENAME CONSTRAINT "FK_e3e103a5f1d8bc8402999286040" TO "asset_file_assetId_fkey";`.execute(db); + await sql`ALTER TABLE "asset_file" RENAME CONSTRAINT "UQ_assetId_type" TO "asset_file_assetId_type_uq";`.execute(db); + await sql`ALTER TABLE "asset_job_status" RENAME CONSTRAINT "PK_420bec36fc02813bddf5c8b73d4" TO "asset_job_status_pkey";`.execute(db); + await sql`ALTER TABLE "asset_job_status" RENAME CONSTRAINT "FK_420bec36fc02813bddf5c8b73d4" TO "asset_job_status_assetId_fkey";`.execute(db); + await sql`ALTER TABLE "audit" RENAME CONSTRAINT "PK_1d3d120ddaf7bc9b1ed68ed463a" TO "audit_pkey";`.execute(db); + await sql`ALTER TABLE "memory" RENAME CONSTRAINT "PK_aaa0692d9496fe827b0568612f8" TO "memory_pkey";`.execute(db); + await sql`ALTER TABLE "memory" RENAME CONSTRAINT "FK_575842846f0c28fa5da46c99b19" TO "memory_ownerId_fkey";`.execute(db); + await sql`ALTER TABLE "memory_asset_audit" RENAME CONSTRAINT "PK_35ef16910228f980e0766dcc59b" TO "memory_asset_audit_pkey";`.execute(db); + await sql`ALTER TABLE "memory_asset_audit" RENAME CONSTRAINT "FK_225a204afcb0bd6de015080fb03" TO "memory_asset_audit_memoryId_fkey";`.execute(db); + await sql`ALTER TABLE "memory_asset" RENAME CONSTRAINT "PK_fcaf7112a013d1703c011c6793d" TO "memory_asset_pkey";`.execute(db); + await sql`ALTER TABLE "memory_asset" RENAME CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e" TO "memory_asset_memoriesId_fkey";`.execute(db); + await sql`ALTER TABLE "memory_asset" RENAME CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f" TO "memory_asset_assetsId_fkey";`.execute(db); + await sql`ALTER TABLE "memory_audit" RENAME CONSTRAINT "PK_19de798c033a710dcfa5c72f81b" TO "memory_audit_pkey";`.execute(db); + await sql`ALTER TABLE "move_history" RENAME CONSTRAINT "PK_af608f132233acf123f2949678d" TO "move_history_pkey";`.execute(db); + await sql`ALTER TABLE "notification" RENAME CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a" TO "notification_pkey";`.execute(db); + await sql`ALTER TABLE "notification" RENAME CONSTRAINT "FK_692a909ee0fa9383e7859f9b406" TO "notification_userId_fkey";`.execute(db); + await sql`ALTER TABLE "partner_audit" RENAME CONSTRAINT "PK_952b50217ff78198a7e380f0359" TO "partner_audit_pkey";`.execute(db); + await sql`ALTER TABLE "partner" RENAME CONSTRAINT "PK_f1cc8f73d16b367f426261a8736" TO "partner_pkey";`.execute(db); + await sql`ALTER TABLE "partner" RENAME CONSTRAINT "FK_7e077a8b70b3530138610ff5e04" TO "partner_sharedById_fkey";`.execute(db); + await sql`ALTER TABLE "partner" RENAME CONSTRAINT "FK_d7e875c6c60e661723dbf372fd3" TO "partner_sharedWithId_fkey";`.execute(db); + await sql`ALTER TABLE "person_audit" RENAME CONSTRAINT "PK_46c1ad23490b9312ffaa052aa59" TO "person_audit_pkey";`.execute(db); + await sql`ALTER TABLE "session" RENAME CONSTRAINT "PK_48cb6b5c20faa63157b3c1baf7f" TO "session_pkey";`.execute(db); + await sql`ALTER TABLE "session" RENAME CONSTRAINT "FK_57de40bc620f456c7311aa3a1e6" TO "session_userId_fkey";`.execute(db); + await sql`ALTER TABLE "session" RENAME CONSTRAINT "FK_afbbabbd7daf5b91de4dca84de8" TO "session_parentId_fkey";`.execute(db); + await sql`ALTER TABLE "shared_link" RENAME CONSTRAINT "PK_642e2b0f619e4876e5f90a43465" TO "shared_link_pkey";`.execute(db); + await sql`ALTER TABLE "shared_link" RENAME CONSTRAINT "FK_66fe3837414c5a9f1c33ca49340" TO "shared_link_userId_fkey";`.execute(db); + await sql`ALTER TABLE "shared_link" RENAME CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66" TO "shared_link_albumId_fkey";`.execute(db); + await sql`ALTER TABLE "shared_link" RENAME CONSTRAINT "UQ_sharedlink_key" TO "shared_link_key_uq";`.execute(db); + await sql`ALTER TABLE "shared_link_asset" RENAME CONSTRAINT "PK_9b4f3687f9b31d1e311336b05e3" TO "shared_link_asset_pkey";`.execute(db); + await sql`ALTER TABLE "shared_link_asset" RENAME CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66" TO "shared_link_asset_assetsId_fkey";`.execute(db); + await sql`ALTER TABLE "shared_link_asset" RENAME CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab" TO "shared_link_asset_sharedLinksId_fkey";`.execute(db); + await sql`ALTER TABLE "stack_audit" RENAME CONSTRAINT "PK_dbe4ec648fa032e8973297de07e" TO "stack_audit_pkey";`.execute(db); + await sql`ALTER TABLE "session_sync_checkpoint" RENAME CONSTRAINT "PK_b846ab547a702863ef7cd9412fb" TO "session_sync_checkpoint_pkey";`.execute(db); + await sql`ALTER TABLE "session_sync_checkpoint" RENAME CONSTRAINT "FK_d8ddd9d687816cc490432b3d4bc" TO "session_sync_checkpoint_sessionId_fkey";`.execute(db); + await sql`ALTER TABLE "system_metadata" RENAME CONSTRAINT "PK_fa94f6857470fb5b81ec6084465" TO "system_metadata_pkey";`.execute(db); + await sql`ALTER TABLE "tag" RENAME CONSTRAINT "PK_e7dc17249a1148a1970748eda99" TO "tag_pkey";`.execute(db); + await sql`ALTER TABLE "tag" RENAME CONSTRAINT "FK_92e67dc508c705dd66c94615576" TO "tag_userId_fkey";`.execute(db); + await sql`ALTER TABLE "tag" RENAME CONSTRAINT "FK_9f9590cc11561f1f48ff034ef99" TO "tag_parentId_fkey";`.execute(db); + await sql`ALTER TABLE "tag" RENAME CONSTRAINT "UQ_79d6f16e52bb2c7130375246793" TO "tag_userId_value_uq";`.execute(db); + await sql`ALTER TABLE "tag_asset" RENAME CONSTRAINT "PK_ef5346fe522b5fb3bc96454747e" TO "tag_asset_pkey";`.execute(db); + await sql`ALTER TABLE "tag_asset" RENAME CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9" TO "tag_asset_assetsId_fkey";`.execute(db); + await sql`ALTER TABLE "tag_asset" RENAME CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42" TO "tag_asset_tagsId_fkey";`.execute(db); + await sql`ALTER TABLE "tag_closure" RENAME CONSTRAINT "PK_eab38eb12a3ec6df8376c95477c" TO "tag_closure_pkey";`.execute(db); + await sql`ALTER TABLE "tag_closure" RENAME CONSTRAINT "FK_15fbcbc67663c6bfc07b354c22c" TO "tag_closure_id_ancestor_fkey";`.execute(db); + await sql`ALTER TABLE "tag_closure" RENAME CONSTRAINT "FK_b1a2a7ed45c29179b5ad51548a1" TO "tag_closure_id_descendant_fkey";`.execute(db); + await sql`ALTER TABLE "user_audit" RENAME CONSTRAINT "PK_e9b2bdfd90e7eb5961091175180" TO "user_audit_pkey";`.execute(db); + await sql`ALTER TABLE "user_metadata" RENAME CONSTRAINT "PK_5931462150b3438cbc83277fe5a" TO "user_metadata_pkey";`.execute(db); + await sql`ALTER TABLE "user_metadata" RENAME CONSTRAINT "FK_6afb43681a21cf7815932bc38ac" TO "user_metadata_userId_fkey";`.execute(db); + await sql`ALTER TABLE "version_history" RENAME CONSTRAINT "PK_5db259cbb09ce82c0d13cfd1b23" TO "version_history_pkey";`.execute(db); + await sql`ALTER INDEX "IDX_users_updated_at_asc_id_asc" RENAME TO "user_updatedAt_id_idx";`.execute(db); + await sql`ALTER INDEX "IDX_users_update_id" RENAME TO "user_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_0f6fc2fb195f24d19b0fb0d57c" RENAME TO "library_ownerId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_libraries_update_id" RENAME TO "library_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_c05079e542fd74de3b5ecb5c1c" RENAME TO "stack_ownerId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_91704e101438fd0653f582426d" RENAME TO "stack_primaryAssetId_idx";`.execute(db); + await sql`ALTER INDEX "idx_local_date_time" RENAME TO "asset_localDateTime_idx";`.execute(db); + await sql`ALTER INDEX "IDX_9977c3c1de01c3d848039a6b90" RENAME TO "asset_libraryId_idx";`.execute(db); + await sql`ALTER INDEX "UQ_assets_owner_library_checksum" RENAME TO "asset_ownerId_libraryId_checksum_idx";`.execute(db); + await sql`ALTER INDEX "IDX_asset_id_stackId" RENAME TO "asset_id_stackId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_f15d48fa3ea5e4bda05ca8ab20" RENAME TO "asset_stackId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_originalPath_libraryId" RENAME TO "asset_originalPath_libraryId_idx";`.execute(db); + await sql`ALTER INDEX "idx_local_date_time_month" RENAME TO "asset_localDateTime_month_idx";`.execute(db); + await sql`ALTER INDEX "IDX_4d66e76dada1ca180f67a205dc" RENAME TO "asset_originalFileName_idx";`.execute(db); + await sql`ALTER INDEX "idx_originalfilename_trigram" RENAME TO "asset_originalFilename_trigram_idx";`.execute(db); + await sql`ALTER INDEX "IDX_16294b83fa8c0149719a1f631e" RENAME TO "asset_livePhotoVideoId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_2c5ac0d6fb58b238fd2068de67" RENAME TO "asset_ownerId_idx";`.execute(db); + await sql`ALTER INDEX "idx_asset_file_created_at" RENAME TO "asset_fileCreatedAt_idx";`.execute(db); + await sql`ALTER INDEX "IDX_8d3efe36c0755849395e6ea866" RENAME TO "asset_checksum_idx";`.execute(db); + await sql`ALTER INDEX "IDX_assets_duplicateId" RENAME TO "asset_duplicateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_assets_update_id" RENAME TO "asset_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_albums_update_id" RENAME TO "album_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_b22c53f35ef20c28c21637c85f" RENAME TO "album_ownerId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_05895aa505a670300d4816debc" RENAME TO "album_albumThumbnailAssetId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_album_assets_update_id" RENAME TO "album_asset_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_e590fa396c6898fcd4a50e4092" RENAME TO "album_asset_albumsId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_4bd1303d199f4e72ccdf998c62" RENAME TO "album_asset_assetsId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_activity_like" RENAME TO "activity_like_idx";`.execute(db); + await sql`ALTER INDEX "IDX_86102d85cfa7f196073aebff68" RENAME TO "activity_albumId_assetId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_3571467bcbe021f66e2bdce96e" RENAME TO "activity_userId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_1af8519996fbfb3684b58df280" RENAME TO "activity_albumId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_8091ea76b12338cb4428d33d78" RENAME TO "activity_assetId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_activity_update_id" RENAME TO "activity_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_album_assets_audit_album_id" RENAME TO "album_asset_audit_albumId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_album_assets_audit_deleted_at" RENAME TO "album_asset_audit_deletedAt_idx";`.execute(db); + await sql`ALTER INDEX "IDX_album_assets_audit_asset_id" RENAME TO "album_asset_audit_assetId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_albums_audit_deleted_at" RENAME TO "album_audit_deletedAt_idx";`.execute(db); + await sql`ALTER INDEX "IDX_albums_audit_album_id" RENAME TO "album_audit_albumId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_albums_audit_user_id" RENAME TO "album_audit_userId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_album_users_audit_user_id" RENAME TO "album_user_audit_userId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_album_users_audit_album_id" RENAME TO "album_user_audit_albumId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_album_users_audit_deleted_at" RENAME TO "album_user_audit_deletedAt_idx";`.execute(db); + await sql`ALTER INDEX "IDX_album_users_update_id" RENAME TO "album_user_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_album_users_create_id" RENAME TO "album_user_createId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_f48513bf9bccefd6ff3ad30bd0" RENAME TO "album_user_usersId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_427c350ad49bd3935a50baab73" RENAME TO "album_user_albumsId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_6c2e267ae764a9413b863a2934" RENAME TO "api_key_userId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_api_keys_update_id" RENAME TO "api_key_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_assets_audit_asset_id" RENAME TO "asset_audit_assetId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_assets_audit_deleted_at" RENAME TO "asset_audit_deletedAt_idx";`.execute(db); + await sql`ALTER INDEX "IDX_assets_audit_owner_id" RENAME TO "asset_audit_ownerId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_asset_exif_update_id" RENAME TO "asset_exif_updateId_idx";`.execute(db); + await sql`ALTER INDEX "exif_city" RENAME TO "asset_exif_city_idx";`.execute(db); + await sql`ALTER INDEX "IDX_auto_stack_id" RENAME TO "asset_exif_autoStackId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_live_photo_cid" RENAME TO "asset_exif_livePhotoCID_idx";`.execute(db); + await sql`ALTER INDEX "IDX_person_update_id" RENAME TO "person_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_5527cc99f530a547093f9e577b" RENAME TO "person_ownerId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_2bbabe31656b6778c6b87b6102" RENAME TO "person_faceAssetId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_bf339a24070dac7e71304ec530" RENAME TO "asset_face_personId_assetId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_asset_faces_assetId_personId" RENAME TO "asset_face_assetId_personId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_asset_files_assetId" RENAME TO "asset_file_assetId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_asset_files_update_id" RENAME TO "asset_file_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_ownerId_createdAt" RENAME TO "audit_ownerId_createdAt_idx";`.execute(db); + await sql`ALTER INDEX "IDX_memories_update_id" RENAME TO "memory_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_575842846f0c28fa5da46c99b1" RENAME TO "memory_ownerId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_memory_assets_audit_memory_id" RENAME TO "memory_asset_audit_memoryId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_memory_assets_audit_asset_id" RENAME TO "memory_asset_audit_assetId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_memory_assets_audit_deleted_at" RENAME TO "memory_asset_audit_deletedAt_idx";`.execute(db); + await sql`ALTER INDEX "IDX_6942ecf52d75d4273de19d2c16" RENAME TO "memory_asset_assetsId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_memory_assets_update_id" RENAME TO "memory_asset_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_984e5c9ab1f04d34538cd32334" RENAME TO "memory_asset_memoriesId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_memories_audit_deleted_at" RENAME TO "memory_audit_deletedAt_idx";`.execute(db); + await sql`ALTER INDEX "IDX_memories_audit_user_id" RENAME TO "memory_audit_userId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_memories_audit_memory_id" RENAME TO "memory_audit_memoryId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_notifications_update_id" RENAME TO "notification_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_692a909ee0fa9383e7859f9b40" RENAME TO "notification_userId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_partners_audit_shared_with_id" RENAME TO "partner_audit_sharedWithId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_partners_audit_deleted_at" RENAME TO "partner_audit_deletedAt_idx";`.execute(db); + await sql`ALTER INDEX "IDX_partners_audit_shared_by_id" RENAME TO "partner_audit_sharedById_idx";`.execute(db); + await sql`ALTER INDEX "IDX_d7e875c6c60e661723dbf372fd" RENAME TO "partner_sharedWithId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_partners_create_id" RENAME TO "partner_createId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_partners_update_id" RENAME TO "partner_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_person_audit_owner_id" RENAME TO "person_audit_ownerId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_person_audit_deleted_at" RENAME TO "person_audit_deletedAt_idx";`.execute(db); + await sql`ALTER INDEX "IDX_person_audit_person_id" RENAME TO "person_audit_personId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_sessions_update_id" RENAME TO "session_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_57de40bc620f456c7311aa3a1e" RENAME TO "session_userId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_afbbabbd7daf5b91de4dca84de" RENAME TO "session_parentId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_sharedlink_key" RENAME TO "shared_link_key_idx";`.execute(db); + await sql`ALTER INDEX "IDX_sharedlink_albumId" RENAME TO "shared_link_albumId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_66fe3837414c5a9f1c33ca4934" RENAME TO "shared_link_userId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_c9fab4aa97ffd1b034f3d6581a" RENAME TO "shared_link_asset_sharedLinksId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_5b7decce6c8d3db9593d6111a6" RENAME TO "shared_link_asset_assetsId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_stacks_audit_deleted_at" RENAME TO "stack_audit_deletedAt_idx";`.execute(db); + await sql`ALTER INDEX "IDX_d8ddd9d687816cc490432b3d4b" RENAME TO "session_sync_checkpoint_sessionId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_session_sync_checkpoints_update_id" RENAME TO "session_sync_checkpoint_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_9f9590cc11561f1f48ff034ef9" RENAME TO "tag_parentId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_tags_update_id" RENAME TO "tag_updateId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_f8e8a9e893cb5c54907f1b798e" RENAME TO "tag_asset_assetsId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_e99f31ea4cdf3a2c35c7287eb4" RENAME TO "tag_asset_tagsId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_tag_asset_assetsId_tagsId" RENAME TO "tag_asset_assetsId_tagsId_idx";`.execute(db); + await sql`ALTER INDEX "IDX_15fbcbc67663c6bfc07b354c22" RENAME TO "tag_closure_id_ancestor_idx";`.execute(db); + await sql`ALTER INDEX "IDX_b1a2a7ed45c29179b5ad51548a" RENAME TO "tag_closure_id_descendant_idx";`.execute(db); + await sql`ALTER INDEX "IDX_users_audit_deleted_at" RENAME TO "user_audit_deletedAt_idx";`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "user_delete_audit" + AFTER DELETE ON "user" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION user_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "user_updatedAt" + BEFORE UPDATE ON "user" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "library_updatedAt" + BEFORE UPDATE ON "library" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "stack_delete_audit" + AFTER DELETE ON "stack" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION stack_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "stack_updatedAt" + BEFORE UPDATE ON "stack" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_delete_audit" + AFTER DELETE ON "asset" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION asset_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_updatedAt" + BEFORE UPDATE ON "asset" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "album_delete_audit" + AFTER DELETE ON "album" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION album_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "album_updatedAt" + BEFORE UPDATE ON "album" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "album_asset_delete_audit" + AFTER DELETE ON "album_asset" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() <= 1) + EXECUTE FUNCTION album_asset_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "album_asset_updatedAt" + BEFORE UPDATE ON "album_asset" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "activity_updatedAt" + BEFORE UPDATE ON "activity" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "album_user_delete_audit" + AFTER DELETE ON "album_user" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() <= 1) + EXECUTE FUNCTION album_user_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "album_user_updatedAt" + BEFORE UPDATE ON "album_user" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "api_key_updatedAt" + BEFORE UPDATE ON "api_key" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_exif_updatedAt" + BEFORE UPDATE ON "asset_exif" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "person_updatedAt" + BEFORE UPDATE ON "person" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_file_updatedAt" + BEFORE UPDATE ON "asset_file" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "memory_delete_audit" + AFTER DELETE ON "memory" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION memory_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "memory_updatedAt" + BEFORE UPDATE ON "memory" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "memory_asset_delete_audit" + AFTER DELETE ON "memory_asset" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() <= 1) + EXECUTE FUNCTION memory_asset_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "memory_asset_updatedAt" + BEFORE UPDATE ON "memory_asset" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "notification_updatedAt" + BEFORE UPDATE ON "notification" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "partner_delete_audit" + AFTER DELETE ON "partner" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION partner_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "partner_updatedAt" + BEFORE UPDATE ON "partner" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "session_updatedAt" + BEFORE UPDATE ON "session" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "session_sync_checkpoint_updatedAt" + BEFORE UPDATE ON "session_sync_checkpoint" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "tag_updatedAt" + BEFORE UPDATE ON "tag" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`DROP FUNCTION users_delete_audit;`.execute(db); + await sql`DROP FUNCTION partners_delete_audit;`.execute(db); + await sql`DROP FUNCTION assets_delete_audit;`.execute(db); + await sql`DROP FUNCTION albums_delete_audit;`.execute(db); + await sql`DROP FUNCTION album_users_delete_audit;`.execute(db); + await sql`DROP FUNCTION album_assets_delete_audit;`.execute(db); + await sql`DROP FUNCTION memories_delete_audit;`.execute(db); + await sql`DROP FUNCTION memory_assets_delete_audit;`.execute(db); + await sql`DROP FUNCTION stacks_delete_audit;`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_user_delete_audit', '{"type":"function","name":"user_delete_audit","sql":"CREATE OR REPLACE FUNCTION user_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO user_audit (\\"userId\\")\\n SELECT \\"id\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_partner_delete_audit', '{"type":"function","name":"partner_delete_audit","sql":"CREATE OR REPLACE FUNCTION partner_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO partner_audit (\\"sharedById\\", \\"sharedWithId\\")\\n SELECT \\"sharedById\\", \\"sharedWithId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_asset_delete_audit', '{"type":"function","name":"asset_delete_audit","sql":"CREATE OR REPLACE FUNCTION asset_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO asset_audit (\\"assetId\\", \\"ownerId\\")\\n SELECT \\"id\\", \\"ownerId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_album_delete_audit', '{"type":"function","name":"album_delete_audit","sql":"CREATE OR REPLACE FUNCTION album_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO album_audit (\\"albumId\\", \\"userId\\")\\n SELECT \\"id\\", \\"ownerId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_album_asset_delete_audit', '{"type":"function","name":"album_asset_delete_audit","sql":"CREATE OR REPLACE FUNCTION album_asset_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO album_asset_audit (\\"albumId\\", \\"assetId\\")\\n SELECT \\"albumsId\\", \\"assetsId\\" FROM OLD\\n WHERE \\"albumsId\\" IN (SELECT \\"id\\" FROM album WHERE \\"id\\" IN (SELECT \\"albumsId\\" FROM OLD));\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_album_user_delete_audit', '{"type":"function","name":"album_user_delete_audit","sql":"CREATE OR REPLACE FUNCTION album_user_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO album_audit (\\"albumId\\", \\"userId\\")\\n SELECT \\"albumsId\\", \\"usersId\\"\\n FROM OLD;\\n\\n IF pg_trigger_depth() = 1 THEN\\n INSERT INTO album_user_audit (\\"albumId\\", \\"userId\\")\\n SELECT \\"albumsId\\", \\"usersId\\"\\n FROM OLD;\\n END IF;\\n\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_memory_delete_audit', '{"type":"function","name":"memory_delete_audit","sql":"CREATE OR REPLACE FUNCTION memory_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO memory_audit (\\"memoryId\\", \\"userId\\")\\n SELECT \\"id\\", \\"ownerId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_memory_asset_delete_audit', '{"type":"function","name":"memory_asset_delete_audit","sql":"CREATE OR REPLACE FUNCTION memory_asset_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO memory_asset_audit (\\"memoryId\\", \\"assetId\\")\\n SELECT \\"memoriesId\\", \\"assetsId\\" FROM OLD\\n WHERE \\"memoriesId\\" IN (SELECT \\"id\\" FROM memory WHERE \\"id\\" IN (SELECT \\"memoriesId\\" FROM OLD));\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_stack_delete_audit', '{"type":"function","name":"stack_delete_audit","sql":"CREATE OR REPLACE FUNCTION stack_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO stack_audit (\\"stackId\\", \\"userId\\")\\n SELECT \\"id\\", \\"ownerId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_user_delete_audit', '{"type":"trigger","name":"user_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"user_delete_audit\\"\\n AFTER DELETE ON \\"user\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION user_delete_audit();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_user_updatedAt', '{"type":"trigger","name":"user_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"user_updatedAt\\"\\n BEFORE UPDATE ON \\"user\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_library_updatedAt', '{"type":"trigger","name":"library_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"library_updatedAt\\"\\n BEFORE UPDATE ON \\"library\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_stack_delete_audit', '{"type":"trigger","name":"stack_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"stack_delete_audit\\"\\n AFTER DELETE ON \\"stack\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION stack_delete_audit();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_stack_updatedAt', '{"type":"trigger","name":"stack_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"stack_updatedAt\\"\\n BEFORE UPDATE ON \\"stack\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_delete_audit', '{"type":"trigger","name":"asset_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"asset_delete_audit\\"\\n AFTER DELETE ON \\"asset\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_delete_audit();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_updatedAt', '{"type":"trigger","name":"asset_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"asset_updatedAt\\"\\n BEFORE UPDATE ON \\"asset\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_asset_originalFilename_trigram_idx', '{"type":"index","name":"asset_originalFilename_trigram_idx","sql":"CREATE INDEX \\"asset_originalFilename_trigram_idx\\" ON \\"asset\\" USING gin (f_unaccent(\\"originalFileName\\") gin_trgm_ops);"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_asset_localDateTime_month_idx', '{"type":"index","name":"asset_localDateTime_month_idx","sql":"CREATE INDEX \\"asset_localDateTime_month_idx\\" ON \\"asset\\" ((date_trunc(''MONTH''::text, (\\"localDateTime\\" AT TIME ZONE ''UTC''::text)) AT TIME ZONE ''UTC''::text));"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_asset_localDateTime_idx', '{"type":"index","name":"asset_localDateTime_idx","sql":"CREATE INDEX \\"asset_localDateTime_idx\\" ON \\"asset\\" (((\\"localDateTime\\" at time zone ''UTC'')::date));"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_asset_ownerId_libraryId_checksum_idx', '{"type":"index","name":"asset_ownerId_libraryId_checksum_idx","sql":"CREATE UNIQUE INDEX \\"asset_ownerId_libraryId_checksum_idx\\" ON \\"asset\\" (\\"ownerId\\", \\"libraryId\\", \\"checksum\\") WHERE (\\"libraryId\\" IS NOT NULL);"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_delete_audit', '{"type":"trigger","name":"album_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"album_delete_audit\\"\\n AFTER DELETE ON \\"album\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION album_delete_audit();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_updatedAt', '{"type":"trigger","name":"album_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"album_updatedAt\\"\\n BEFORE UPDATE ON \\"album\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_asset_delete_audit', '{"type":"trigger","name":"album_asset_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"album_asset_delete_audit\\"\\n AFTER DELETE ON \\"album_asset\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() <= 1)\\n EXECUTE FUNCTION album_asset_delete_audit();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_asset_updatedAt', '{"type":"trigger","name":"album_asset_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"album_asset_updatedAt\\"\\n BEFORE UPDATE ON \\"album_asset\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_activity_updatedAt', '{"type":"trigger","name":"activity_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"activity_updatedAt\\"\\n BEFORE UPDATE ON \\"activity\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_activity_like_idx', '{"type":"index","name":"activity_like_idx","sql":"CREATE UNIQUE INDEX \\"activity_like_idx\\" ON \\"activity\\" (\\"assetId\\", \\"userId\\", \\"albumId\\") WHERE (\\"isLiked\\" = true);"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_user_delete_audit', '{"type":"trigger","name":"album_user_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"album_user_delete_audit\\"\\n AFTER DELETE ON \\"album_user\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() <= 1)\\n EXECUTE FUNCTION album_user_delete_audit();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_user_updatedAt', '{"type":"trigger","name":"album_user_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"album_user_updatedAt\\"\\n BEFORE UPDATE ON \\"album_user\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_api_key_updatedAt', '{"type":"trigger","name":"api_key_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"api_key_updatedAt\\"\\n BEFORE UPDATE ON \\"api_key\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_exif_updatedAt', '{"type":"trigger","name":"asset_exif_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"asset_exif_updatedAt\\"\\n BEFORE UPDATE ON \\"asset_exif\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_person_updatedAt', '{"type":"trigger","name":"person_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"person_updatedAt\\"\\n BEFORE UPDATE ON \\"person\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_file_updatedAt', '{"type":"trigger","name":"asset_file_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"asset_file_updatedAt\\"\\n BEFORE UPDATE ON \\"asset_file\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_memory_delete_audit', '{"type":"trigger","name":"memory_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"memory_delete_audit\\"\\n AFTER DELETE ON \\"memory\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION memory_delete_audit();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_memory_updatedAt', '{"type":"trigger","name":"memory_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"memory_updatedAt\\"\\n BEFORE UPDATE ON \\"memory\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_memory_asset_delete_audit', '{"type":"trigger","name":"memory_asset_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"memory_asset_delete_audit\\"\\n AFTER DELETE ON \\"memory_asset\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() <= 1)\\n EXECUTE FUNCTION memory_asset_delete_audit();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_memory_asset_updatedAt', '{"type":"trigger","name":"memory_asset_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"memory_asset_updatedAt\\"\\n BEFORE UPDATE ON \\"memory_asset\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_notification_updatedAt', '{"type":"trigger","name":"notification_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"notification_updatedAt\\"\\n BEFORE UPDATE ON \\"notification\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_partner_delete_audit', '{"type":"trigger","name":"partner_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"partner_delete_audit\\"\\n AFTER DELETE ON \\"partner\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION partner_delete_audit();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_partner_updatedAt', '{"type":"trigger","name":"partner_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"partner_updatedAt\\"\\n BEFORE UPDATE ON \\"partner\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_session_updatedAt', '{"type":"trigger","name":"session_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"session_updatedAt\\"\\n BEFORE UPDATE ON \\"session\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_session_sync_checkpoint_updatedAt', '{"type":"trigger","name":"session_sync_checkpoint_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"session_sync_checkpoint_updatedAt\\"\\n BEFORE UPDATE ON \\"session_sync_checkpoint\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_tag_updatedAt', '{"type":"trigger","name":"tag_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"tag_updatedAt\\"\\n BEFORE UPDATE ON \\"tag\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"function","name":"album_user_after_insert","sql":"CREATE OR REPLACE FUNCTION album_user_after_insert()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE album SET \\"updatedAt\\" = clock_timestamp(), \\"updateId\\" = immich_uuid_v7(clock_timestamp())\\n WHERE \\"id\\" IN (SELECT DISTINCT \\"albumsId\\" FROM inserted_rows);\\n RETURN NULL;\\n END\\n $$;"}'::jsonb WHERE "name" = 'function_album_user_after_insert';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"index","name":"UQ_assets_owner_checksum","sql":"CREATE UNIQUE INDEX \\"UQ_assets_owner_checksum\\" ON \\"asset\\" (\\"ownerId\\", \\"checksum\\") WHERE (\\"libraryId\\" IS NULL);"}'::jsonb WHERE "name" = 'index_UQ_assets_owner_checksum';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"trigger","name":"album_user_after_insert","sql":"CREATE OR REPLACE TRIGGER \\"album_user_after_insert\\"\\n AFTER INSERT ON \\"album_user\\"\\n REFERENCING NEW TABLE AS \\"inserted_rows\\"\\n FOR EACH STATEMENT\\n EXECUTE FUNCTION album_user_after_insert();"}'::jsonb WHERE "name" = 'trigger_album_user_after_insert';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_users_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_partners_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_assets_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_albums_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_album_assets_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_album_users_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_memories_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_memory_assets_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_stacks_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_users_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_users_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_libraries_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_stacks_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_stacks_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_assets_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_assets_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_albums_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_albums_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_activity_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_assets_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_assets_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_users_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_users_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_api_keys_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_person_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_files_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_exif_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_memories_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_memories_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_memory_assets_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_memory_assets_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_notifications_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_partners_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_partners_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_sessions_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_session_sync_checkpoints_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_tags_updated_at';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_idx_originalfilename_trigram';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_idx_local_date_time_month';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_idx_local_date_time';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_UQ_assets_owner_library_checksum';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_IDX_activity_like';`.execute(db); +} + +export async function down(db: Kysely): Promise { + for (const { oldName, newName } of tables) { + await sql.raw(`ALTER TABLE "${newName}" RENAME TO "${oldName}"`).execute(db); + } + await sql`CREATE OR REPLACE FUNCTION public.album_user_after_insert() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ + BEGIN + UPDATE albums SET "updatedAt" = clock_timestamp(), "updateId" = immich_uuid_v7(clock_timestamp()) + WHERE "id" IN (SELECT DISTINCT "albumsId" FROM inserted_rows); + RETURN NULL; + END + $function$ +`.execute(db); + await sql`CREATE OR REPLACE FUNCTION public.users_delete_audit() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ + BEGIN + INSERT INTO users_audit ("userId") + SELECT "id" + FROM OLD; + RETURN NULL; + END; + $function$ +`.execute(db); + await sql`CREATE OR REPLACE FUNCTION public.partners_delete_audit() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ + BEGIN + INSERT INTO partners_audit ("sharedById", "sharedWithId") + SELECT "sharedById", "sharedWithId" + FROM OLD; + RETURN NULL; + END; + $function$ +`.execute(db); + await sql`CREATE OR REPLACE FUNCTION public.assets_delete_audit() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ + BEGIN + INSERT INTO assets_audit ("assetId", "ownerId") + SELECT "id", "ownerId" + FROM OLD; + RETURN NULL; + END; + $function$ +`.execute(db); + await sql`CREATE OR REPLACE FUNCTION public.albums_delete_audit() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ + BEGIN + INSERT INTO albums_audit ("albumId", "userId") + SELECT "id", "ownerId" + FROM OLD; + RETURN NULL; + END + $function$ +`.execute(db); + await sql`CREATE OR REPLACE FUNCTION public.album_users_delete_audit() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ + BEGIN + INSERT INTO albums_audit ("albumId", "userId") + SELECT "albumsId", "usersId" + FROM OLD; + + IF pg_trigger_depth() = 1 THEN + INSERT INTO album_users_audit ("albumId", "userId") + SELECT "albumsId", "usersId" + FROM OLD; + END IF; + + RETURN NULL; + END + $function$ +`.execute(db); + await sql`CREATE OR REPLACE FUNCTION public.album_assets_delete_audit() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ + BEGIN + INSERT INTO album_assets_audit ("albumId", "assetId") + SELECT "albumsId", "assetsId" FROM OLD + WHERE "albumsId" IN (SELECT "id" FROM albums WHERE "id" IN (SELECT "albumsId" FROM OLD)); + RETURN NULL; + END + $function$ +`.execute(db); + await sql`CREATE OR REPLACE FUNCTION public.memories_delete_audit() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ + BEGIN + INSERT INTO memories_audit ("memoryId", "userId") + SELECT "id", "ownerId" + FROM OLD; + RETURN NULL; + END + $function$ +`.execute(db); + await sql`CREATE OR REPLACE FUNCTION public.memory_assets_delete_audit() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ + BEGIN + INSERT INTO memory_assets_audit ("memoryId", "assetId") + SELECT "memoriesId", "assetsId" FROM OLD + WHERE "memoriesId" IN (SELECT "id" FROM memories WHERE "id" IN (SELECT "memoriesId" FROM OLD)); + RETURN NULL; + END + $function$ +`.execute(db); + await sql`CREATE OR REPLACE FUNCTION public.stacks_delete_audit() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ + BEGIN + INSERT INTO stacks_audit ("stackId", "userId") + SELECT "id", "ownerId" + FROM OLD; + RETURN NULL; + END + $function$ +`.execute(db); + await sql`DROP TRIGGER "activity_updatedAt" ON "activity";`.execute(db); + await sql`DROP TRIGGER "person_updatedAt" ON "person";`.execute(db); + await sql`DROP TRIGGER "album_asset_delete_audit" ON "album_asset";`.execute(db); + await sql`DROP TRIGGER "album_asset_updatedAt" ON "album_asset";`.execute(db); + await sql`DROP TRIGGER "album_user_delete_audit" ON "album_user";`.execute(db); + await sql`DROP TRIGGER "album_user_updatedAt" ON "album_user";`.execute(db); + await sql`DROP TRIGGER "album_delete_audit" ON "album";`.execute(db); + await sql`DROP TRIGGER "album_updatedAt" ON "album";`.execute(db); + await sql`DROP TRIGGER "api_key_updatedAt" ON "api_key";`.execute(db); + await sql`DROP TRIGGER "asset_delete_audit" ON "asset";`.execute(db); + await sql`DROP TRIGGER "asset_updatedAt" ON "asset";`.execute(db); + await sql`DROP TRIGGER "asset_file_updatedAt" ON "asset_file";`.execute(db); + await sql`DROP TRIGGER "asset_exif_updatedAt" ON "asset_exif";`.execute(db); + await sql`DROP TRIGGER "library_updatedAt" ON "library";`.execute(db); + await sql`DROP TRIGGER "memory_asset_delete_audit" ON "memory_asset";`.execute(db); + await sql`DROP TRIGGER "memory_asset_updatedAt" ON "memory_asset";`.execute(db); + await sql`DROP TRIGGER "memory_delete_audit" ON "memory";`.execute(db); + await sql`DROP TRIGGER "memory_updatedAt" ON "memory";`.execute(db); + await sql`DROP TRIGGER "notification_updatedAt" ON "notification";`.execute(db); + await sql`DROP TRIGGER "partner_delete_audit" ON "partner";`.execute(db); + await sql`DROP TRIGGER "partner_updatedAt" ON "partner";`.execute(db); + await sql`DROP TRIGGER "session_updatedAt" ON "session";`.execute(db); + await sql`DROP TRIGGER "stack_delete_audit" ON "stack";`.execute(db); + await sql`DROP TRIGGER "stack_updatedAt" ON "stack";`.execute(db); + await sql`DROP TRIGGER "session_sync_checkpoint_updatedAt" ON "session_sync_checkpoint";`.execute(db); + await sql`DROP TRIGGER "tag_updatedAt" ON "tag";`.execute(db); + await sql`DROP TRIGGER "user_delete_audit" ON "user";`.execute(db); + await sql`DROP TRIGGER "user_updatedAt" ON "user";`.execute(db); + await sql`ALTER TABLE "asset_audit" RENAME CONSTRAINT "asset_audit_pkey" TO "PK_99bd5c015f81a641927a32b4212";`.execute(db); + await sql`ALTER TABLE "audit" RENAME CONSTRAINT "audit_pkey" TO "PK_1d3d120ddaf7bc9b1ed68ed463a";`.execute(db); + await sql`ALTER TABLE "move_history" RENAME CONSTRAINT "move_history_pkey" TO "PK_af608f132233acf123f2949678d";`.execute(db); + await sql`ALTER TABLE "partner_audit" RENAME CONSTRAINT "partner_audit_pkey" TO "PK_952b50217ff78198a7e380f0359";`.execute(db); + await sql`ALTER TABLE "user_audit" RENAME CONSTRAINT "user_audit_pkey" TO "PK_e9b2bdfd90e7eb5961091175180";`.execute(db); + await sql`ALTER TABLE "system_metadata" RENAME CONSTRAINT "system_metadata_pkey" TO "PK_fa94f6857470fb5b81ec6084465";`.execute(db); + await sql`ALTER TABLE "version_history" RENAME CONSTRAINT "version_history_pkey" TO "PK_5db259cbb09ce82c0d13cfd1b23";`.execute(db); + await sql`ALTER TABLE "asset_job_status" RENAME CONSTRAINT "asset_job_status_assetId_fkey" TO "FK_420bec36fc02813bddf5c8b73d4";`.execute(db); + await sql`ALTER TABLE "asset_job_status" RENAME CONSTRAINT "asset_job_status_pkey" TO "PK_420bec36fc02813bddf5c8b73d4";`.execute(db); + await sql`ALTER TABLE "tag_asset" RENAME CONSTRAINT "tag_asset_assetsId_fkey" TO "FK_f8e8a9e893cb5c54907f1b798e9";`.execute(db); + await sql`ALTER TABLE "tag_asset" RENAME CONSTRAINT "tag_asset_tagsId_fkey" TO "FK_e99f31ea4cdf3a2c35c7287eb42";`.execute(db); + await sql`ALTER TABLE "tag_asset" RENAME CONSTRAINT "tag_asset_pkey" TO "PK_ef5346fe522b5fb3bc96454747e";`.execute(db); + await sql`ALTER TABLE "user_metadata" RENAME CONSTRAINT "user_metadata_userId_fkey" TO "FK_6afb43681a21cf7815932bc38ac";`.execute(db); + await sql`ALTER TABLE "user_metadata" RENAME CONSTRAINT "user_metadata_pkey" TO "PK_5931462150b3438cbc83277fe5a";`.execute(db); + await sql`ALTER TABLE "activity" RENAME CONSTRAINT "activity_albumId_fkey" TO "FK_1af8519996fbfb3684b58df280b";`.execute(db); + await sql`ALTER TABLE "activity" RENAME CONSTRAINT "activity_userId_fkey" TO "FK_3571467bcbe021f66e2bdce96ea";`.execute(db); + await sql`ALTER TABLE "activity" RENAME CONSTRAINT "activity_assetId_fkey" TO "FK_8091ea76b12338cb4428d33d782";`.execute(db); + await sql`ALTER TABLE "activity" RENAME CONSTRAINT "activity_albumId_assetId_fkey" TO "fk_activity_album_asset_composite";`.execute(db); + await sql`ALTER TABLE "activity" RENAME CONSTRAINT "activity_like_check" TO "CHK_2ab1e70f113f450eb40c1e3ec8";`.execute(db); + await sql`ALTER TABLE "activity" RENAME CONSTRAINT "activity_pkey" TO "PK_24625a1d6b1b089c8ae206fe467";`.execute(db); + await sql`ALTER TABLE "person" RENAME CONSTRAINT "person_ownerId_fkey" TO "FK_5527cc99f530a547093f9e577b6";`.execute(db); + await sql`ALTER TABLE "person" RENAME CONSTRAINT "person_faceAssetId_fkey" TO "FK_2bbabe31656b6778c6b87b61023";`.execute(db); + await sql`ALTER TABLE "person" RENAME CONSTRAINT "person_birthDate_chk" TO "CHK_b0f82b0ed662bfc24fbb58bb45";`.execute(db); + await sql`ALTER TABLE "person" RENAME CONSTRAINT "person_pkey" TO "PK_5fdaf670315c4b7e70cce85daa3";`.execute(db); + await sql`ALTER TABLE "person_audit" RENAME CONSTRAINT "person_audit_pkey" TO "PK_46c1ad23490b9312ffaa052aa59";`.execute(db); + await sql`ALTER TABLE "album_asset_audit" RENAME CONSTRAINT "album_asset_audit_albumId_fkey" TO "FK_8047b44b812619a3c75a2839b0d";`.execute(db); + await sql`ALTER TABLE "album_asset_audit" RENAME CONSTRAINT "album_asset_audit_pkey" TO "PK_32969b576ec8f78d84f37c2eb2d";`.execute(db); + await sql`ALTER TABLE "album_asset" RENAME CONSTRAINT "album_asset_albumsId_fkey" TO "FK_e590fa396c6898fcd4a50e40927";`.execute(db); + await sql`ALTER TABLE "album_asset" RENAME CONSTRAINT "album_asset_assetsId_fkey" TO "FK_4bd1303d199f4e72ccdf998c621";`.execute(db); + await sql`ALTER TABLE "album_asset" RENAME CONSTRAINT "album_asset_pkey" TO "PK_c67bc36fa845fb7b18e0e398180";`.execute(db); + await sql`ALTER TABLE "album_audit" RENAME CONSTRAINT "album_audit_pkey" TO "PK_c75efea8d4dce316ad29b851a8b";`.execute(db); + await sql`ALTER TABLE "album_user_audit" RENAME CONSTRAINT "album_user_audit_pkey" TO "PK_f479a2e575b7ebc9698362c1688";`.execute(db); + await sql`ALTER TABLE "album_user" RENAME CONSTRAINT "album_user_albumsId_fkey" TO "FK_427c350ad49bd3935a50baab737";`.execute(db); + await sql`ALTER TABLE "album_user" RENAME CONSTRAINT "album_user_usersId_fkey" TO "FK_f48513bf9bccefd6ff3ad30bd06";`.execute(db); + await sql`ALTER TABLE "album_user" RENAME CONSTRAINT "album_user_pkey" TO "PK_7df55657e0b2e8b626330a0ebc8";`.execute(db); + await sql`ALTER TABLE "album" RENAME CONSTRAINT "album_ownerId_fkey" TO "FK_b22c53f35ef20c28c21637c85f4";`.execute(db); + await sql`ALTER TABLE "album" RENAME CONSTRAINT "album_albumThumbnailAssetId_fkey" TO "FK_05895aa505a670300d4816debce";`.execute(db); + await sql`ALTER TABLE "album" RENAME CONSTRAINT "album_pkey" TO "PK_7f71c7b5bc7c87b8f94c9a93a00";`.execute(db); + await sql`ALTER TABLE "api_key" RENAME CONSTRAINT "api_key_userId_fkey" TO "FK_6c2e267ae764a9413b863a29342";`.execute(db); + await sql`ALTER TABLE "api_key" RENAME CONSTRAINT "api_key_pkey" TO "PK_5c8a79801b44bd27b79228e1dad";`.execute(db); + await sql`ALTER TABLE "asset" RENAME CONSTRAINT "asset_ownerId_fkey" TO "FK_2c5ac0d6fb58b238fd2068de67d";`.execute(db); + await sql`ALTER TABLE "asset" RENAME CONSTRAINT "asset_livePhotoVideoId_fkey" TO "FK_16294b83fa8c0149719a1f631ef";`.execute(db); + await sql`ALTER TABLE "asset" RENAME CONSTRAINT "asset_libraryId_fkey" TO "FK_9977c3c1de01c3d848039a6b90c";`.execute(db); + await sql`ALTER TABLE "asset" RENAME CONSTRAINT "asset_stackId_fkey" TO "FK_f15d48fa3ea5e4bda05ca8ab207";`.execute(db); + await sql`ALTER TABLE "asset" RENAME CONSTRAINT "asset_pkey" TO "PK_da96729a8b113377cfb6a62439c";`.execute(db); + await sql`ALTER TABLE "asset_face" RENAME CONSTRAINT "asset_face_assetId_fkey" TO "FK_02a43fd0b3c50fb6d7f0cb7282c";`.execute(db); + await sql`ALTER TABLE "asset_face" RENAME CONSTRAINT "asset_face_personId_fkey" TO "FK_95ad7106dd7b484275443f580f9";`.execute(db); + await sql`ALTER TABLE "asset_face" RENAME CONSTRAINT "asset_face_pkey" TO "PK_6df76ab2eb6f5b57b7c2f1fc684";`.execute(db); + await sql`ALTER TABLE "asset_file" RENAME CONSTRAINT "asset_file_assetId_fkey" TO "FK_e3e103a5f1d8bc8402999286040";`.execute(db); + await sql`ALTER TABLE "asset_file" RENAME CONSTRAINT "asset_file_assetId_type_uq" TO "UQ_assetId_type";`.execute(db); + await sql`ALTER TABLE "asset_file" RENAME CONSTRAINT "asset_file_pkey" TO "PK_c41dc3e9ef5e1c57ca5a08a0004";`.execute(db); + await sql`ALTER TABLE "asset_exif" RENAME CONSTRAINT "asset_exif_assetId_fkey" TO "FK_c0117fdbc50b917ef9067740c44";`.execute(db); + await sql`ALTER TABLE "asset_exif" RENAME CONSTRAINT "asset_exif_pkey" TO "PK_c0117fdbc50b917ef9067740c44";`.execute(db); + await sql`ALTER TABLE "library" RENAME CONSTRAINT "library_ownerId_fkey" TO "FK_0f6fc2fb195f24d19b0fb0d57c1";`.execute(db); + await sql`ALTER TABLE "library" RENAME CONSTRAINT "library_pkey" TO "PK_505fedfcad00a09b3734b4223de";`.execute(db); + await sql`ALTER TABLE "memory_asset_audit" RENAME CONSTRAINT "memory_asset_audit_memoryId_fkey" TO "FK_225a204afcb0bd6de015080fb03";`.execute(db); + await sql`ALTER TABLE "memory_asset_audit" RENAME CONSTRAINT "memory_asset_audit_pkey" TO "PK_35ef16910228f980e0766dcc59b";`.execute(db); + await sql`ALTER TABLE "memory_asset" RENAME CONSTRAINT "memory_asset_memoriesId_fkey" TO "FK_984e5c9ab1f04d34538cd32334e";`.execute(db); + await sql`ALTER TABLE "memory_asset" RENAME CONSTRAINT "memory_asset_assetsId_fkey" TO "FK_6942ecf52d75d4273de19d2c16f";`.execute(db); + await sql`ALTER TABLE "memory_asset" RENAME CONSTRAINT "memory_asset_pkey" TO "PK_fcaf7112a013d1703c011c6793d";`.execute(db); + await sql`ALTER TABLE "memory_audit" RENAME CONSTRAINT "memory_audit_pkey" TO "PK_19de798c033a710dcfa5c72f81b";`.execute(db); + await sql`ALTER TABLE "memory" RENAME CONSTRAINT "memory_ownerId_fkey" TO "FK_575842846f0c28fa5da46c99b19";`.execute(db); + await sql`ALTER TABLE "memory" RENAME CONSTRAINT "memory_pkey" TO "PK_aaa0692d9496fe827b0568612f8";`.execute(db); + await sql`ALTER TABLE "notification" RENAME CONSTRAINT "notification_userId_fkey" TO "FK_692a909ee0fa9383e7859f9b406";`.execute(db); + await sql`ALTER TABLE "notification" RENAME CONSTRAINT "notification_pkey" TO "PK_6a72c3c0f683f6462415e653c3a";`.execute(db); + await sql`ALTER TABLE "partner" RENAME CONSTRAINT "partner_sharedById_fkey" TO "FK_7e077a8b70b3530138610ff5e04";`.execute(db); + await sql`ALTER TABLE "partner" RENAME CONSTRAINT "partner_sharedWithId_fkey" TO "FK_d7e875c6c60e661723dbf372fd3";`.execute(db); + await sql`ALTER TABLE "partner" RENAME CONSTRAINT "partner_pkey" TO "PK_f1cc8f73d16b367f426261a8736";`.execute(db); + await sql`ALTER TABLE "session" RENAME CONSTRAINT "session_userId_fkey" TO "FK_57de40bc620f456c7311aa3a1e6";`.execute(db); + await sql`ALTER TABLE "session" RENAME CONSTRAINT "session_parentId_fkey" TO "FK_afbbabbd7daf5b91de4dca84de8";`.execute(db); + await sql`ALTER TABLE "session" RENAME CONSTRAINT "session_pkey" TO "PK_48cb6b5c20faa63157b3c1baf7f";`.execute(db); + await sql`ALTER TABLE "shared_link_asset" RENAME CONSTRAINT "shared_link_asset_assetsId_fkey" TO "FK_5b7decce6c8d3db9593d6111a66";`.execute(db); + await sql`ALTER TABLE "shared_link_asset" RENAME CONSTRAINT "shared_link_asset_sharedLinksId_fkey" TO "FK_c9fab4aa97ffd1b034f3d6581ab";`.execute(db); + await sql`ALTER TABLE "shared_link_asset" RENAME CONSTRAINT "shared_link_asset_pkey" TO "PK_9b4f3687f9b31d1e311336b05e3";`.execute(db); + await sql`ALTER TABLE "shared_link" RENAME CONSTRAINT "shared_link_userId_fkey" TO "FK_66fe3837414c5a9f1c33ca49340";`.execute(db); + await sql`ALTER TABLE "shared_link" RENAME CONSTRAINT "shared_link_albumId_fkey" TO "FK_0c6ce9058c29f07cdf7014eac66";`.execute(db); + await sql`ALTER TABLE "shared_link" RENAME CONSTRAINT "shared_link_key_uq" TO "UQ_sharedlink_key";`.execute(db); + await sql`ALTER TABLE "shared_link" RENAME CONSTRAINT "shared_link_pkey" TO "PK_642e2b0f619e4876e5f90a43465";`.execute(db); + await sql`ALTER TABLE "stack_audit" RENAME CONSTRAINT "stack_audit_pkey" TO "PK_dbe4ec648fa032e8973297de07e";`.execute(db); + await sql`ALTER TABLE "stack" RENAME CONSTRAINT "stack_primaryAssetId_fkey" TO "FK_91704e101438fd0653f582426dc";`.execute(db); + await sql`ALTER TABLE "stack" RENAME CONSTRAINT "stack_primaryAssetId_uq" TO "REL_91704e101438fd0653f582426d";`.execute(db); + await sql`ALTER TABLE "stack" RENAME CONSTRAINT "stack_ownerId_fkey" TO "FK_c05079e542fd74de3b5ecb5c1c8";`.execute(db); + await sql`ALTER TABLE "stack" RENAME CONSTRAINT "stack_pkey" TO "PK_74a27e7fcbd5852463d0af3034b";`.execute(db); + await sql`ALTER TABLE "session_sync_checkpoint" RENAME CONSTRAINT "session_sync_checkpoint_sessionId_fkey" TO "FK_d8ddd9d687816cc490432b3d4bc";`.execute(db); + await sql`ALTER TABLE "session_sync_checkpoint" RENAME CONSTRAINT "session_sync_checkpoint_pkey" TO "PK_b846ab547a702863ef7cd9412fb";`.execute(db); + await sql`ALTER TABLE "tag_closure" RENAME CONSTRAINT "tag_closure_id_ancestor_fkey" TO "FK_15fbcbc67663c6bfc07b354c22c";`.execute(db); + await sql`ALTER TABLE "tag_closure" RENAME CONSTRAINT "tag_closure_id_descendant_fkey" TO "FK_b1a2a7ed45c29179b5ad51548a1";`.execute(db); + await sql`ALTER TABLE "tag_closure" RENAME CONSTRAINT "tag_closure_pkey" TO "PK_eab38eb12a3ec6df8376c95477c";`.execute(db); + await sql`ALTER TABLE "tag" RENAME CONSTRAINT "tag_userId_fkey" TO "FK_92e67dc508c705dd66c94615576";`.execute(db); + await sql`ALTER TABLE "tag" RENAME CONSTRAINT "tag_parentId_fkey" TO "FK_9f9590cc11561f1f48ff034ef99";`.execute(db); + await sql`ALTER TABLE "tag" RENAME CONSTRAINT "tag_userId_value_uq" TO "UQ_79d6f16e52bb2c7130375246793";`.execute(db); + await sql`ALTER TABLE "tag" RENAME CONSTRAINT "tag_pkey" TO "PK_e7dc17249a1148a1970748eda99";`.execute(db); + await sql`ALTER TABLE "user" RENAME CONSTRAINT "user_email_uq" TO "UQ_97672ac88f789774dd47f7c8be3";`.execute(db); + await sql`ALTER TABLE "user" RENAME CONSTRAINT "user_storageLabel_uq" TO "UQ_b309cf34fa58137c416b32cea3a";`.execute(db); + await sql`ALTER TABLE "user" RENAME CONSTRAINT "user_pkey" TO "PK_a3ffb1c0c8416b9fc6f907b7433";`.execute(db); + await sql`ALTER TABLE "user_metadata_audit" RENAME CONSTRAINT "user_metadata_audit_pkey" TO "PK_15d5cc4d65ac966233b9921acac";`.execute(db); + await sql`ALTER INDEX "asset_audit_assetId_idx" RENAME TO "IDX_assets_audit_asset_id";`.execute(db); + await sql`ALTER INDEX "asset_audit_ownerId_idx" RENAME TO "IDX_assets_audit_owner_id";`.execute(db); + await sql`ALTER INDEX "asset_audit_deletedAt_idx" RENAME TO "IDX_assets_audit_deleted_at";`.execute(db); + await sql`ALTER INDEX "audit_ownerId_createdAt_idx" RENAME TO "IDX_ownerId_createdAt";`.execute(db); + await sql`ALTER INDEX "partner_audit_sharedById_idx" RENAME TO "IDX_partners_audit_shared_by_id";`.execute(db); + await sql`ALTER INDEX "partner_audit_sharedWithId_idx" RENAME TO "IDX_partners_audit_shared_with_id";`.execute(db); + await sql`ALTER INDEX "partner_audit_deletedAt_idx" RENAME TO "IDX_partners_audit_deleted_at";`.execute(db); + await sql`ALTER INDEX "user_audit_deletedAt_idx" RENAME TO "IDX_users_audit_deleted_at";`.execute(db); + await sql`ALTER INDEX "tag_asset_assetsId_tagsId_idx" RENAME TO "IDX_tag_asset_assetsId_tagsId";`.execute(db); + await sql`ALTER INDEX "tag_asset_assetsId_idx" RENAME TO "IDX_f8e8a9e893cb5c54907f1b798e";`.execute(db); + await sql`ALTER INDEX "tag_asset_tagsId_idx" RENAME TO "IDX_e99f31ea4cdf3a2c35c7287eb4";`.execute(db); + await sql`ALTER INDEX "activity_albumId_assetId_idx" RENAME TO "IDX_86102d85cfa7f196073aebff68";`.execute(db); + await sql`ALTER INDEX "activity_like_idx" RENAME TO "IDX_activity_like";`.execute(db); + await sql`ALTER INDEX "activity_albumId_idx" RENAME TO "IDX_1af8519996fbfb3684b58df280";`.execute(db); + await sql`ALTER INDEX "activity_userId_idx" RENAME TO "IDX_3571467bcbe021f66e2bdce96e";`.execute(db); + await sql`ALTER INDEX "activity_assetId_idx" RENAME TO "IDX_8091ea76b12338cb4428d33d78";`.execute(db); + await sql`ALTER INDEX "activity_updateId_idx" RENAME TO "IDX_activity_update_id";`.execute(db); + await sql`ALTER INDEX "person_ownerId_idx" RENAME TO "IDX_5527cc99f530a547093f9e577b";`.execute(db); + await sql`ALTER INDEX "person_faceAssetId_idx" RENAME TO "IDX_2bbabe31656b6778c6b87b6102";`.execute(db); + await sql`ALTER INDEX "person_updateId_idx" RENAME TO "IDX_person_update_id";`.execute(db); + await sql`ALTER INDEX "person_audit_personId_idx" RENAME TO "IDX_person_audit_person_id";`.execute(db); + await sql`ALTER INDEX "person_audit_ownerId_idx" RENAME TO "IDX_person_audit_owner_id";`.execute(db); + await sql`ALTER INDEX "person_audit_deletedAt_idx" RENAME TO "IDX_person_audit_deleted_at";`.execute(db); + await sql`ALTER INDEX "album_asset_audit_albumId_idx" RENAME TO "IDX_album_assets_audit_album_id";`.execute(db); + await sql`ALTER INDEX "album_asset_audit_assetId_idx" RENAME TO "IDX_album_assets_audit_asset_id";`.execute(db); + await sql`ALTER INDEX "album_asset_audit_deletedAt_idx" RENAME TO "IDX_album_assets_audit_deleted_at";`.execute(db); + await sql`ALTER INDEX "album_asset_albumsId_idx" RENAME TO "IDX_e590fa396c6898fcd4a50e4092";`.execute(db); + await sql`ALTER INDEX "album_asset_assetsId_idx" RENAME TO "IDX_4bd1303d199f4e72ccdf998c62";`.execute(db); + await sql`ALTER INDEX "album_asset_updateId_idx" RENAME TO "IDX_album_assets_update_id";`.execute(db); + await sql`ALTER INDEX "album_audit_albumId_idx" RENAME TO "IDX_albums_audit_album_id";`.execute(db); + await sql`ALTER INDEX "album_audit_userId_idx" RENAME TO "IDX_albums_audit_user_id";`.execute(db); + await sql`ALTER INDEX "album_audit_deletedAt_idx" RENAME TO "IDX_albums_audit_deleted_at";`.execute(db); + await sql`ALTER INDEX "album_user_audit_albumId_idx" RENAME TO "IDX_album_users_audit_album_id";`.execute(db); + await sql`ALTER INDEX "album_user_audit_userId_idx" RENAME TO "IDX_album_users_audit_user_id";`.execute(db); + await sql`ALTER INDEX "album_user_audit_deletedAt_idx" RENAME TO "IDX_album_users_audit_deleted_at";`.execute(db); + await sql`ALTER INDEX "album_user_albumsId_idx" RENAME TO "IDX_427c350ad49bd3935a50baab73";`.execute(db); + await sql`ALTER INDEX "album_user_usersId_idx" RENAME TO "IDX_f48513bf9bccefd6ff3ad30bd0";`.execute(db); + await sql`ALTER INDEX "album_user_createId_idx" RENAME TO "IDX_album_users_create_id";`.execute(db); + await sql`ALTER INDEX "album_user_updateId_idx" RENAME TO "IDX_album_users_update_id";`.execute(db); + await sql`ALTER INDEX "album_ownerId_idx" RENAME TO "IDX_b22c53f35ef20c28c21637c85f";`.execute(db); + await sql`ALTER INDEX "album_albumThumbnailAssetId_idx" RENAME TO "IDX_05895aa505a670300d4816debc";`.execute(db); + await sql`ALTER INDEX "album_updateId_idx" RENAME TO "IDX_albums_update_id";`.execute(db); + await sql`ALTER INDEX "api_key_userId_idx" RENAME TO "IDX_6c2e267ae764a9413b863a2934";`.execute(db); + await sql`ALTER INDEX "api_key_updateId_idx" RENAME TO "IDX_api_keys_update_id";`.execute(db); + await sql`ALTER INDEX "asset_originalFilename_trigram_idx" RENAME TO "idx_originalfilename_trigram";`.execute(db); + await sql`ALTER INDEX "asset_id_stackId_idx" RENAME TO "IDX_asset_id_stackId";`.execute(db); + await sql`ALTER INDEX "asset_originalPath_libraryId_idx" RENAME TO "IDX_originalPath_libraryId";`.execute(db); + await sql`ALTER INDEX "asset_localDateTime_month_idx" RENAME TO "idx_local_date_time_month";`.execute(db); + await sql`ALTER INDEX "asset_localDateTime_idx" RENAME TO "idx_local_date_time";`.execute(db); + await sql`ALTER INDEX "asset_ownerId_libraryId_checksum_idx" RENAME TO "UQ_assets_owner_library_checksum";`.execute(db); + await sql`ALTER INDEX "asset_ownerId_idx" RENAME TO "IDX_2c5ac0d6fb58b238fd2068de67";`.execute(db); + await sql`ALTER INDEX "asset_fileCreatedAt_idx" RENAME TO "idx_asset_file_created_at";`.execute(db); + await sql`ALTER INDEX "asset_checksum_idx" RENAME TO "IDX_8d3efe36c0755849395e6ea866";`.execute(db); + await sql`ALTER INDEX "asset_livePhotoVideoId_idx" RENAME TO "IDX_16294b83fa8c0149719a1f631e";`.execute(db); + await sql`ALTER INDEX "asset_originalFileName_idx" RENAME TO "IDX_4d66e76dada1ca180f67a205dc";`.execute(db); + await sql`ALTER INDEX "asset_libraryId_idx" RENAME TO "IDX_9977c3c1de01c3d848039a6b90";`.execute(db); + await sql`ALTER INDEX "asset_stackId_idx" RENAME TO "IDX_f15d48fa3ea5e4bda05ca8ab20";`.execute(db); + await sql`ALTER INDEX "asset_duplicateId_idx" RENAME TO "IDX_assets_duplicateId";`.execute(db); + await sql`ALTER INDEX "asset_updateId_idx" RENAME TO "IDX_assets_update_id";`.execute(db); + await sql`ALTER INDEX "asset_face_assetId_personId_idx" RENAME TO "IDX_asset_faces_assetId_personId";`.execute(db); + await sql`ALTER INDEX "asset_file_assetId_idx" RENAME TO "IDX_asset_files_assetId";`.execute(db); + await sql`ALTER INDEX "asset_file_updateId_idx" RENAME TO "IDX_asset_files_update_id";`.execute(db); + await sql`ALTER INDEX "asset_exif_city_idx" RENAME TO "exif_city";`.execute(db); + await sql`ALTER INDEX "asset_exif_livePhotoCID_idx" RENAME TO "IDX_live_photo_cid";`.execute(db); + await sql`ALTER INDEX "asset_exif_autoStackId_idx" RENAME TO "IDX_auto_stack_id";`.execute(db); + await sql`ALTER INDEX "asset_exif_updateId_idx" RENAME TO "IDX_asset_exif_update_id";`.execute(db); + await sql`ALTER INDEX "library_ownerId_idx" RENAME TO "IDX_0f6fc2fb195f24d19b0fb0d57c";`.execute(db); + await sql`ALTER INDEX "library_updateId_idx" RENAME TO "IDX_libraries_update_id";`.execute(db); + await sql`ALTER INDEX "memory_asset_audit_memoryId_idx" RENAME TO "IDX_memory_assets_audit_memory_id";`.execute(db); + await sql`ALTER INDEX "memory_asset_audit_assetId_idx" RENAME TO "IDX_memory_assets_audit_asset_id";`.execute(db); + await sql`ALTER INDEX "memory_asset_audit_deletedAt_idx" RENAME TO "IDX_memory_assets_audit_deleted_at";`.execute(db); + await sql`ALTER INDEX "memory_asset_memoriesId_idx" RENAME TO "IDX_984e5c9ab1f04d34538cd32334";`.execute(db); + await sql`ALTER INDEX "memory_asset_assetsId_idx" RENAME TO "IDX_6942ecf52d75d4273de19d2c16";`.execute(db); + await sql`ALTER INDEX "memory_asset_updateId_idx" RENAME TO "IDX_memory_assets_update_id";`.execute(db); + await sql`ALTER INDEX "memory_audit_memoryId_idx" RENAME TO "IDX_memories_audit_memory_id";`.execute(db); + await sql`ALTER INDEX "memory_audit_userId_idx" RENAME TO "IDX_memories_audit_user_id";`.execute(db); + await sql`ALTER INDEX "memory_audit_deletedAt_idx" RENAME TO "IDX_memories_audit_deleted_at";`.execute(db); + await sql`ALTER INDEX "memory_ownerId_idx" RENAME TO "IDX_575842846f0c28fa5da46c99b1";`.execute(db); + await sql`ALTER INDEX "memory_updateId_idx" RENAME TO "IDX_memories_update_id";`.execute(db); + await sql`ALTER INDEX "notification_updateId_idx" RENAME TO "IDX_notifications_update_id";`.execute(db); + await sql`ALTER INDEX "notification_userId_idx" RENAME TO "IDX_692a909ee0fa9383e7859f9b40";`.execute(db); + await sql`ALTER INDEX "partner_sharedWithId_idx" RENAME TO "IDX_d7e875c6c60e661723dbf372fd";`.execute(db); + await sql`ALTER INDEX "partner_createId_idx" RENAME TO "IDX_partners_create_id";`.execute(db); + await sql`ALTER INDEX "partner_updateId_idx" RENAME TO "IDX_partners_update_id";`.execute(db); + await sql`ALTER INDEX "session_userId_idx" RENAME TO "IDX_57de40bc620f456c7311aa3a1e";`.execute(db); + await sql`ALTER INDEX "session_parentId_idx" RENAME TO "IDX_afbbabbd7daf5b91de4dca84de";`.execute(db); + await sql`ALTER INDEX "session_updateId_idx" RENAME TO "IDX_sessions_update_id";`.execute(db); + await sql`ALTER INDEX "shared_link_asset_assetsId_idx" RENAME TO "IDX_5b7decce6c8d3db9593d6111a6";`.execute(db); + await sql`ALTER INDEX "shared_link_asset_sharedLinksId_idx" RENAME TO "IDX_c9fab4aa97ffd1b034f3d6581a";`.execute(db); + await sql`ALTER INDEX "shared_link_userId_idx" RENAME TO "IDX_66fe3837414c5a9f1c33ca4934";`.execute(db); + await sql`ALTER INDEX "shared_link_key_idx" RENAME TO "IDX_sharedlink_key";`.execute(db); + await sql`ALTER INDEX "shared_link_albumId_idx" RENAME TO "IDX_sharedlink_albumId";`.execute(db); + await sql`ALTER INDEX "stack_audit_deletedAt_idx" RENAME TO "IDX_stacks_audit_deleted_at";`.execute(db); + await sql`ALTER INDEX "stack_primaryAssetId_idx" RENAME TO "IDX_91704e101438fd0653f582426d";`.execute(db); + await sql`ALTER INDEX "stack_ownerId_idx" RENAME TO "IDX_c05079e542fd74de3b5ecb5c1c";`.execute(db); + await sql`ALTER INDEX "session_sync_checkpoint_sessionId_idx" RENAME TO "IDX_d8ddd9d687816cc490432b3d4b";`.execute(db); + await sql`ALTER INDEX "session_sync_checkpoint_updateId_idx" RENAME TO "IDX_session_sync_checkpoints_update_id";`.execute(db); + await sql`ALTER INDEX "tag_closure_id_ancestor_idx" RENAME TO "IDX_15fbcbc67663c6bfc07b354c22";`.execute(db); + await sql`ALTER INDEX "tag_closure_id_descendant_idx" RENAME TO "IDX_b1a2a7ed45c29179b5ad51548a";`.execute(db); + await sql`ALTER INDEX "tag_parentId_idx" RENAME TO "IDX_9f9590cc11561f1f48ff034ef9";`.execute(db); + await sql`ALTER INDEX "tag_updateId_idx" RENAME TO "IDX_tags_update_id";`.execute(db); + await sql`ALTER INDEX "user_updateId_idx" RENAME TO "IDX_users_update_id";`.execute(db); + await sql`ALTER INDEX "user_updatedAt_id_idx" RENAME TO "IDX_users_updated_at_asc_id_asc";`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "activity_updated_at" + BEFORE UPDATE ON "activity" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "person_updated_at" + BEFORE UPDATE ON "person" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "album_assets_updated_at" + BEFORE UPDATE ON "album_asset" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "album_assets_delete_audit" + AFTER DELETE ON "album_asset" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN ((pg_trigger_depth() <= 1)) + EXECUTE FUNCTION album_assets_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "album_users_delete_audit" + AFTER DELETE ON "album_user" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN ((pg_trigger_depth() <= 1)) + EXECUTE FUNCTION album_users_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "album_users_updated_at" + BEFORE UPDATE ON "album_user" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "albums_updated_at" + BEFORE UPDATE ON "album" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "albums_delete_audit" + AFTER DELETE ON "album" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN ((pg_trigger_depth() = 0)) + EXECUTE FUNCTION albums_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "api_keys_updated_at" + BEFORE UPDATE ON "api_key" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "assets_delete_audit" + AFTER DELETE ON "asset" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN ((pg_trigger_depth() = 0)) + EXECUTE FUNCTION assets_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "assets_updated_at" + BEFORE UPDATE ON "asset" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_files_updated_at" + BEFORE UPDATE ON "asset_file" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_exif_updated_at" + BEFORE UPDATE ON "asset_exif" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "libraries_updated_at" + BEFORE UPDATE ON "library" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "memory_assets_updated_at" + BEFORE UPDATE ON "memory_asset" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "memory_assets_delete_audit" + AFTER DELETE ON "memory_asset" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN ((pg_trigger_depth() <= 1)) + EXECUTE FUNCTION memory_assets_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "memories_updated_at" + BEFORE UPDATE ON "memory" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "memories_delete_audit" + AFTER DELETE ON "memory" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN ((pg_trigger_depth() = 0)) + EXECUTE FUNCTION memories_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "notifications_updated_at" + BEFORE UPDATE ON "notification" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "partners_delete_audit" + AFTER DELETE ON "partner" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN ((pg_trigger_depth() = 0)) + EXECUTE FUNCTION partners_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "partners_updated_at" + BEFORE UPDATE ON "partner" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "sessions_updated_at" + BEFORE UPDATE ON "session" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "stacks_delete_audit" + AFTER DELETE ON "stack" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN ((pg_trigger_depth() = 0)) + EXECUTE FUNCTION stacks_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "stacks_updated_at" + BEFORE UPDATE ON "stack" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "session_sync_checkpoints_updated_at" + BEFORE UPDATE ON "session_sync_checkpoint" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "tags_updated_at" + BEFORE UPDATE ON "tag" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "users_delete_audit" + AFTER DELETE ON "user" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN ((pg_trigger_depth() = 0)) + EXECUTE FUNCTION users_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "users_updated_at" + BEFORE UPDATE ON "user" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`DROP FUNCTION user_delete_audit;`.execute(db); + await sql`DROP FUNCTION partner_delete_audit;`.execute(db); + await sql`DROP FUNCTION asset_delete_audit;`.execute(db); + await sql`DROP FUNCTION album_delete_audit;`.execute(db); + await sql`DROP FUNCTION album_asset_delete_audit;`.execute(db); + await sql`DROP FUNCTION album_user_delete_audit;`.execute(db); + await sql`DROP FUNCTION memory_delete_audit;`.execute(db); + await sql`DROP FUNCTION memory_asset_delete_audit;`.execute(db); + await sql`DROP FUNCTION stack_delete_audit;`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_users_delete_audit', '{"sql":"CREATE OR REPLACE FUNCTION users_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO users_audit (\\"userId\\")\\n SELECT \\"id\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;","name":"users_delete_audit","type":"function"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_partners_delete_audit', '{"sql":"CREATE OR REPLACE FUNCTION partners_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO partners_audit (\\"sharedById\\", \\"sharedWithId\\")\\n SELECT \\"sharedById\\", \\"sharedWithId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;","name":"partners_delete_audit","type":"function"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_assets_delete_audit', '{"sql":"CREATE OR REPLACE FUNCTION assets_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO assets_audit (\\"assetId\\", \\"ownerId\\")\\n SELECT \\"id\\", \\"ownerId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;","name":"assets_delete_audit","type":"function"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_albums_delete_audit', '{"sql":"CREATE OR REPLACE FUNCTION albums_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO albums_audit (\\"albumId\\", \\"userId\\")\\n SELECT \\"id\\", \\"ownerId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;","name":"albums_delete_audit","type":"function"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_album_assets_delete_audit', '{"sql":"CREATE OR REPLACE FUNCTION album_assets_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO album_assets_audit (\\"albumId\\", \\"assetId\\")\\n SELECT \\"albumsId\\", \\"assetsId\\" FROM OLD\\n WHERE \\"albumsId\\" IN (SELECT \\"id\\" FROM albums WHERE \\"id\\" IN (SELECT \\"albumsId\\" FROM OLD));\\n RETURN NULL;\\n END\\n $$;","name":"album_assets_delete_audit","type":"function"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_album_users_delete_audit', '{"sql":"CREATE OR REPLACE FUNCTION album_users_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO albums_audit (\\"albumId\\", \\"userId\\")\\n SELECT \\"albumsId\\", \\"usersId\\"\\n FROM OLD;\\n\\n IF pg_trigger_depth() = 1 THEN\\n INSERT INTO album_users_audit (\\"albumId\\", \\"userId\\")\\n SELECT \\"albumsId\\", \\"usersId\\"\\n FROM OLD;\\n END IF;\\n\\n RETURN NULL;\\n END\\n $$;","name":"album_users_delete_audit","type":"function"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_memories_delete_audit', '{"sql":"CREATE OR REPLACE FUNCTION memories_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO memories_audit (\\"memoryId\\", \\"userId\\")\\n SELECT \\"id\\", \\"ownerId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;","name":"memories_delete_audit","type":"function"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_memory_assets_delete_audit', '{"sql":"CREATE OR REPLACE FUNCTION memory_assets_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO memory_assets_audit (\\"memoryId\\", \\"assetId\\")\\n SELECT \\"memoriesId\\", \\"assetsId\\" FROM OLD\\n WHERE \\"memoriesId\\" IN (SELECT \\"id\\" FROM memories WHERE \\"id\\" IN (SELECT \\"memoriesId\\" FROM OLD));\\n RETURN NULL;\\n END\\n $$;","name":"memory_assets_delete_audit","type":"function"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_stacks_delete_audit', '{"sql":"CREATE OR REPLACE FUNCTION stacks_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO stacks_audit (\\"stackId\\", \\"userId\\")\\n SELECT \\"id\\", \\"ownerId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;","name":"stacks_delete_audit","type":"function"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_users_delete_audit', '{"sql":"CREATE OR REPLACE TRIGGER \\"users_delete_audit\\"\\n AFTER DELETE ON \\"users\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION users_delete_audit();","name":"users_delete_audit","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_users_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"users_updated_at\\"\\n BEFORE UPDATE ON \\"users\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"users_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_libraries_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"libraries_updated_at\\"\\n BEFORE UPDATE ON \\"libraries\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"libraries_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_stacks_delete_audit', '{"sql":"CREATE OR REPLACE TRIGGER \\"stacks_delete_audit\\"\\n AFTER DELETE ON \\"asset_stack\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION stacks_delete_audit();","name":"stacks_delete_audit","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_stacks_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"stacks_updated_at\\"\\n BEFORE UPDATE ON \\"asset_stack\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"stacks_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_assets_delete_audit', '{"sql":"CREATE OR REPLACE TRIGGER \\"assets_delete_audit\\"\\n AFTER DELETE ON \\"assets\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION assets_delete_audit();","name":"assets_delete_audit","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_assets_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"assets_updated_at\\"\\n BEFORE UPDATE ON \\"assets\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"assets_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_albums_delete_audit', '{"sql":"CREATE OR REPLACE TRIGGER \\"albums_delete_audit\\"\\n AFTER DELETE ON \\"albums\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION albums_delete_audit();","name":"albums_delete_audit","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_albums_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"albums_updated_at\\"\\n BEFORE UPDATE ON \\"albums\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"albums_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_activity_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"activity_updated_at\\"\\n BEFORE UPDATE ON \\"activity\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"activity_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_assets_delete_audit', '{"sql":"CREATE OR REPLACE TRIGGER \\"album_assets_delete_audit\\"\\n AFTER DELETE ON \\"albums_assets_assets\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() <= 1)\\n EXECUTE FUNCTION album_assets_delete_audit();","name":"album_assets_delete_audit","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_assets_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"album_assets_updated_at\\"\\n BEFORE UPDATE ON \\"albums_assets_assets\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"album_assets_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_users_delete_audit', '{"sql":"CREATE OR REPLACE TRIGGER \\"album_users_delete_audit\\"\\n AFTER DELETE ON \\"albums_shared_users_users\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() <= 1)\\n EXECUTE FUNCTION album_users_delete_audit();","name":"album_users_delete_audit","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_users_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"album_users_updated_at\\"\\n BEFORE UPDATE ON \\"albums_shared_users_users\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"album_users_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_api_keys_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"api_keys_updated_at\\"\\n BEFORE UPDATE ON \\"api_keys\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"api_keys_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_person_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"person_updated_at\\"\\n BEFORE UPDATE ON \\"person\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"person_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_files_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"asset_files_updated_at\\"\\n BEFORE UPDATE ON \\"asset_files\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"asset_files_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_exif_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"asset_exif_updated_at\\"\\n BEFORE UPDATE ON \\"exif\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"asset_exif_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_memories_delete_audit', '{"sql":"CREATE OR REPLACE TRIGGER \\"memories_delete_audit\\"\\n AFTER DELETE ON \\"memories\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION memories_delete_audit();","name":"memories_delete_audit","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_memories_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"memories_updated_at\\"\\n BEFORE UPDATE ON \\"memories\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"memories_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_memory_assets_delete_audit', '{"sql":"CREATE OR REPLACE TRIGGER \\"memory_assets_delete_audit\\"\\n AFTER DELETE ON \\"memories_assets_assets\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() <= 1)\\n EXECUTE FUNCTION memory_assets_delete_audit();","name":"memory_assets_delete_audit","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_memory_assets_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"memory_assets_updated_at\\"\\n BEFORE UPDATE ON \\"memories_assets_assets\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"memory_assets_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_notifications_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"notifications_updated_at\\"\\n BEFORE UPDATE ON \\"notifications\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"notifications_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_partners_delete_audit', '{"sql":"CREATE OR REPLACE TRIGGER \\"partners_delete_audit\\"\\n AFTER DELETE ON \\"partners\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION partners_delete_audit();","name":"partners_delete_audit","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_partners_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"partners_updated_at\\"\\n BEFORE UPDATE ON \\"partners\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"partners_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_sessions_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"sessions_updated_at\\"\\n BEFORE UPDATE ON \\"sessions\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"sessions_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_session_sync_checkpoints_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"session_sync_checkpoints_updated_at\\"\\n BEFORE UPDATE ON \\"session_sync_checkpoints\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"session_sync_checkpoints_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_tags_updated_at', '{"sql":"CREATE OR REPLACE TRIGGER \\"tags_updated_at\\"\\n BEFORE UPDATE ON \\"tags\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();","name":"tags_updated_at","type":"trigger"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_idx_originalfilename_trigram', '{"sql":"CREATE INDEX \\"idx_originalfilename_trigram\\" ON \\"assets\\" USING gin (f_unaccent(\\"originalFileName\\") gin_trgm_ops);","name":"idx_originalfilename_trigram","type":"index"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_idx_local_date_time_month', '{"sql":"CREATE INDEX \\"idx_local_date_time_month\\" ON \\"assets\\" ((date_trunc(''MONTH''::text, (\\"localDateTime\\" AT TIME ZONE ''UTC''::text)) AT TIME ZONE ''UTC''::text));","name":"idx_local_date_time_month","type":"index"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_idx_local_date_time', '{"sql":"CREATE INDEX \\"idx_local_date_time\\" ON \\"assets\\" (((\\"localDateTime\\" at time zone ''UTC'')::date));","name":"idx_local_date_time","type":"index"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_UQ_assets_owner_library_checksum', '{"sql":"CREATE UNIQUE INDEX \\"UQ_assets_owner_library_checksum\\" ON \\"assets\\" (\\"ownerId\\", \\"libraryId\\", \\"checksum\\") WHERE (\\"libraryId\\" IS NOT NULL);","name":"UQ_assets_owner_library_checksum","type":"index"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_IDX_activity_like', '{"sql":"CREATE UNIQUE INDEX \\"IDX_activity_like\\" ON \\"activity\\" (\\"assetId\\", \\"userId\\", \\"albumId\\") WHERE (\\"isLiked\\" = true);","name":"IDX_activity_like","type":"index"}'::jsonb);`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE FUNCTION album_user_after_insert()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE albums SET \\"updatedAt\\" = clock_timestamp(), \\"updateId\\" = immich_uuid_v7(clock_timestamp())\\n WHERE \\"id\\" IN (SELECT DISTINCT \\"albumsId\\" FROM inserted_rows);\\n RETURN NULL;\\n END\\n $$;","name":"album_user_after_insert","type":"function"}'::jsonb WHERE "name" = 'function_album_user_after_insert';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE TRIGGER \\"album_user_after_insert\\"\\n AFTER INSERT ON \\"albums_shared_users_users\\"\\n REFERENCING NEW TABLE AS \\"inserted_rows\\"\\n FOR EACH STATEMENT\\n EXECUTE FUNCTION album_user_after_insert();","name":"album_user_after_insert","type":"trigger"}'::jsonb WHERE "name" = 'trigger_album_user_after_insert';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE UNIQUE INDEX \\"UQ_assets_owner_checksum\\" ON \\"assets\\" (\\"ownerId\\", \\"checksum\\") WHERE (\\"libraryId\\" IS NULL);","name":"UQ_assets_owner_checksum","type":"index"}'::jsonb WHERE "name" = 'index_UQ_assets_owner_checksum';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_user_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_partner_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_asset_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_album_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_album_asset_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_album_user_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_memory_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_memory_asset_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_stack_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_user_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_user_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_library_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_stack_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_stack_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_asset_originalFilename_trigram_idx';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_asset_localDateTime_month_idx';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_asset_localDateTime_idx';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_asset_ownerId_libraryId_checksum_idx';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_asset_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_asset_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_activity_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_activity_like_idx';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_user_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_user_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_api_key_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_exif_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_person_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_file_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_memory_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_memory_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_memory_asset_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_memory_asset_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_notification_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_partner_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_partner_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_session_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_session_sync_checkpoint_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_tag_updatedAt';`.execute(db); + + +} diff --git a/server/src/schema/tables/activity.table.ts b/server/src/schema/tables/activity.table.ts index 47fca24bb1..128cf2eabd 100644 --- a/server/src/schema/tables/activity.table.ts +++ b/server/src/schema/tables/activity.table.ts @@ -1,4 +1,5 @@ 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'; @@ -7,6 +8,7 @@ import { Column, CreateDateColumn, ForeignKeyColumn, + ForeignKeyConstraint, Generated, Index, PrimaryGeneratedColumn, @@ -16,16 +18,23 @@ import { } from 'src/sql-tools'; @Table('activity') -@UpdatedAtTrigger('activity_updated_at') +@UpdatedAtTrigger('activity_updatedAt') @Index({ - name: 'IDX_activity_like', + name: 'activity_like_idx', columns: ['assetId', 'userId', 'albumId'], unique: true, where: '("isLiked" = true)', }) @Check({ - name: 'CHK_2ab1e70f113f450eb40c1e3ec8', - expression: `("comment" IS NULL AND "isLiked" = true) OR ("comment" IS NOT NULL AND "isLiked" = false)`, + name: 'activity_like_check', + expression: `(comment IS NULL AND "isLiked" = true) OR (comment IS NOT NULL AND "isLiked" = false)`, +}) +@ForeignKeyConstraint({ + columns: ['albumId', 'assetId'], + referenceTable: () => AlbumAssetTable, + referenceColumns: ['albumsId', 'assetsId'], + onUpdate: 'NO ACTION', + onDelete: 'CASCADE', }) export class ActivityTable { @PrimaryGeneratedColumn() @@ -52,6 +61,6 @@ export class ActivityTable { @Column({ type: 'boolean', default: false }) isLiked!: Generated; - @UpdateIdColumn({ indexName: 'IDX_activity_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; } diff --git a/server/src/schema/tables/album-asset-audit.table.ts b/server/src/schema/tables/album-asset-audit.table.ts index 7c8da313c0..ab8fd9ae89 100644 --- a/server/src/schema/tables/album-asset-audit.table.ts +++ b/server/src/schema/tables/album-asset-audit.table.ts @@ -2,22 +2,20 @@ 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_assets_audit') +@Table('album_asset_audit') export class AlbumAssetAuditTable { @PrimaryGeneratedUuidV7Column() id!: Generated; @ForeignKeyColumn(() => AlbumTable, { - type: 'uuid', - indexName: 'IDX_album_assets_audit_album_id', onDelete: 'CASCADE', onUpdate: 'CASCADE', }) albumId!: string; - @Column({ type: 'uuid', indexName: 'IDX_album_assets_audit_asset_id' }) + @Column({ type: 'uuid', index: true }) assetId!: string; - @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_album_assets_audit_deleted_at' }) + @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) deletedAt!: Generated; } diff --git a/server/src/schema/tables/album-asset.table.ts b/server/src/schema/tables/album-asset.table.ts index 9b97db7ea0..c34546c3f3 100644 --- a/server/src/schema/tables/album-asset.table.ts +++ b/server/src/schema/tables/album-asset.table.ts @@ -1,5 +1,5 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { album_assets_delete_audit } from 'src/schema/functions'; +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 { @@ -12,11 +12,11 @@ import { UpdateDateColumn, } from 'src/sql-tools'; -@Table({ name: 'albums_assets_assets', primaryConstraintName: 'PK_c67bc36fa845fb7b18e0e398180' }) -@UpdatedAtTrigger('album_assets_updated_at') +@Table({ name: 'album_asset' }) +@UpdatedAtTrigger('album_asset_updatedAt') @AfterDeleteTrigger({ scope: 'statement', - function: album_assets_delete_audit, + function: album_asset_delete_audit, referencingOldTableAs: 'old', when: 'pg_trigger_depth() <= 1', }) @@ -33,6 +33,6 @@ export class AlbumAssetTable { @UpdateDateColumn() updatedAt!: Generated; - @UpdateIdColumn({ indexName: 'IDX_album_assets_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; } diff --git a/server/src/schema/tables/album-audit.table.ts b/server/src/schema/tables/album-audit.table.ts index 3dd2682232..432c51c36a 100644 --- a/server/src/schema/tables/album-audit.table.ts +++ b/server/src/schema/tables/album-audit.table.ts @@ -1,17 +1,17 @@ import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; -@Table('albums_audit') +@Table('album_audit') export class AlbumAuditTable { @PrimaryGeneratedUuidV7Column() id!: Generated; - @Column({ type: 'uuid', indexName: 'IDX_albums_audit_album_id' }) + @Column({ type: 'uuid', index: true }) albumId!: string; - @Column({ type: 'uuid', indexName: 'IDX_albums_audit_user_id' }) + @Column({ type: 'uuid', index: true }) userId!: string; - @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_albums_audit_deleted_at' }) + @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) deletedAt!: Generated; } diff --git a/server/src/schema/tables/album-user-audit.table.ts b/server/src/schema/tables/album-user-audit.table.ts index 01ad991a52..2259511bdd 100644 --- a/server/src/schema/tables/album-user-audit.table.ts +++ b/server/src/schema/tables/album-user-audit.table.ts @@ -1,17 +1,17 @@ import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; -@Table('album_users_audit') +@Table('album_user_audit') export class AlbumUserAuditTable { @PrimaryGeneratedUuidV7Column() id!: Generated; - @Column({ type: 'uuid', indexName: 'IDX_album_users_audit_album_id' }) + @Column({ type: 'uuid', index: true }) albumId!: string; - @Column({ type: 'uuid', indexName: 'IDX_album_users_audit_user_id' }) + @Column({ type: 'uuid', index: true }) userId!: string; - @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_album_users_audit_deleted_at' }) + @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) deletedAt!: Generated; } diff --git a/server/src/schema/tables/album-user.table.ts b/server/src/schema/tables/album-user.table.ts index e8ed720eef..6f20e25d90 100644 --- a/server/src/schema/tables/album-user.table.ts +++ b/server/src/schema/tables/album-user.table.ts @@ -1,6 +1,6 @@ import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { AlbumUserRole } from 'src/enum'; -import { album_user_after_insert, album_users_delete_audit } from 'src/schema/functions'; +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 { @@ -10,17 +10,14 @@ import { CreateDateColumn, ForeignKeyColumn, Generated, - Index, Table, Timestamp, UpdateDateColumn, } from 'src/sql-tools'; -@Table({ name: 'albums_shared_users_users', primaryConstraintName: 'PK_7df55657e0b2e8b626330a0ebc8' }) +@Table({ name: 'album_user' }) // Pre-existing indices from original album <--> user ManyToMany mapping -@Index({ name: 'IDX_427c350ad49bd3935a50baab73', columns: ['albumsId'] }) -@Index({ name: 'IDX_f48513bf9bccefd6ff3ad30bd0', columns: ['usersId'] }) -@UpdatedAtTrigger('album_users_updated_at') +@UpdatedAtTrigger('album_user_updatedAt') @AfterInsertTrigger({ name: 'album_user_after_insert', scope: 'statement', @@ -29,7 +26,7 @@ import { }) @AfterDeleteTrigger({ scope: 'statement', - function: album_users_delete_audit, + function: album_user_delete_audit, referencingOldTableAs: 'old', when: 'pg_trigger_depth() <= 1', }) @@ -53,13 +50,13 @@ export class AlbumUserTable { @Column({ type: 'character varying', default: AlbumUserRole.EDITOR }) role!: Generated; - @CreateIdColumn({ indexName: 'IDX_album_users_create_id' }) + @CreateIdColumn({ index: true }) createId!: Generated; @CreateDateColumn() createdAt!: Generated; - @UpdateIdColumn({ indexName: 'IDX_album_users_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; @UpdateDateColumn() diff --git a/server/src/schema/tables/album.table.ts b/server/src/schema/tables/album.table.ts index e4cad8f469..bca15d520b 100644 --- a/server/src/schema/tables/album.table.ts +++ b/server/src/schema/tables/album.table.ts @@ -1,6 +1,6 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { AssetOrder } from 'src/enum'; -import { albums_delete_audit } from 'src/schema/functions'; +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 { @@ -16,11 +16,11 @@ import { UpdateDateColumn, } from 'src/sql-tools'; -@Table({ name: 'albums', primaryConstraintName: 'PK_7f71c7b5bc7c87b8f94c9a93a00' }) -@UpdatedAtTrigger('albums_updated_at') +@Table({ name: 'album' }) +@UpdatedAtTrigger('album_updatedAt') @AfterDeleteTrigger({ scope: 'statement', - function: albums_delete_audit, + function: album_delete_audit, referencingOldTableAs: 'old', when: 'pg_trigger_depth() = 0', }) @@ -60,6 +60,6 @@ export class AlbumTable { @Column({ default: AssetOrder.DESC }) order!: Generated; - @UpdateIdColumn({ indexName: 'IDX_albums_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; } diff --git a/server/src/schema/tables/api-key.table.ts b/server/src/schema/tables/api-key.table.ts index 4bcd4beae8..efbf18afaa 100644 --- a/server/src/schema/tables/api-key.table.ts +++ b/server/src/schema/tables/api-key.table.ts @@ -12,8 +12,8 @@ import { UpdateDateColumn, } from 'src/sql-tools'; -@Table('api_keys') -@UpdatedAtTrigger('api_keys_updated_at') +@Table('api_key') +@UpdatedAtTrigger('api_key_updatedAt') export class ApiKeyTable { @PrimaryGeneratedColumn() id!: Generated; @@ -36,6 +36,6 @@ export class ApiKeyTable { @Column({ array: true, type: 'character varying' }) permissions!: Permission[]; - @UpdateIdColumn({ indexName: 'IDX_api_keys_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; } diff --git a/server/src/schema/tables/asset-audit.table.ts b/server/src/schema/tables/asset-audit.table.ts index 8ca6aa956d..86c3f6f28b 100644 --- a/server/src/schema/tables/asset-audit.table.ts +++ b/server/src/schema/tables/asset-audit.table.ts @@ -1,17 +1,17 @@ import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; -@Table('assets_audit') +@Table('asset_audit') export class AssetAuditTable { @PrimaryGeneratedUuidV7Column() id!: Generated; - @Column({ type: 'uuid', indexName: 'IDX_assets_audit_asset_id' }) + @Column({ type: 'uuid', index: true }) assetId!: string; - @Column({ type: 'uuid', indexName: 'IDX_assets_audit_owner_id' }) + @Column({ type: 'uuid', index: true }) ownerId!: string; - @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_assets_audit_deleted_at' }) + @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) deletedAt!: Generated; } diff --git a/server/src/schema/tables/exif.table.ts b/server/src/schema/tables/asset-exif.table.ts similarity index 86% rename from server/src/schema/tables/exif.table.ts rename to server/src/schema/tables/asset-exif.table.ts index 668f215776..f28735a2db 100644 --- a/server/src/schema/tables/exif.table.ts +++ b/server/src/schema/tables/asset-exif.table.ts @@ -2,9 +2,9 @@ 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('exif') -@UpdatedAtTrigger('asset_exif_updated_at') -export class ExifTable { +@Table('asset_exif') +@UpdatedAtTrigger('asset_exif_updatedAt') +export class AssetExifTable { @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', primary: true }) assetId!: string; @@ -50,7 +50,7 @@ export class ExifTable { @Column({ type: 'double precision', nullable: true }) longitude!: number | null; - @Column({ type: 'character varying', nullable: true, indexName: 'exif_city' }) + @Column({ type: 'character varying', nullable: true, index: true }) city!: string | null; @Column({ type: 'character varying', nullable: true }) @@ -68,7 +68,7 @@ export class ExifTable { @Column({ type: 'character varying', nullable: true }) exposureTime!: string | null; - @Column({ type: 'character varying', nullable: true, indexName: 'IDX_live_photo_cid' }) + @Column({ type: 'character varying', nullable: true, index: true }) livePhotoCID!: string | null; @Column({ type: 'character varying', nullable: true }) @@ -86,7 +86,7 @@ export class ExifTable { @Column({ type: 'integer', nullable: true }) bitsPerSample!: number | null; - @Column({ type: 'character varying', nullable: true, indexName: 'IDX_auto_stack_id' }) + @Column({ type: 'character varying', nullable: true, index: true }) autoStackId!: string | null; @Column({ type: 'integer', nullable: true }) @@ -95,6 +95,6 @@ export class ExifTable { @UpdateDateColumn({ default: () => 'clock_timestamp()' }) updatedAt!: Generated; - @UpdateIdColumn({ indexName: 'IDX_asset_exif_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; } diff --git a/server/src/schema/tables/asset-face.table.ts b/server/src/schema/tables/asset-face.table.ts index 329783d6df..483d655768 100644 --- a/server/src/schema/tables/asset-face.table.ts +++ b/server/src/schema/tables/asset-face.table.ts @@ -13,8 +13,9 @@ import { Timestamp, } from 'src/sql-tools'; -@Table({ name: 'asset_faces' }) -@Index({ name: 'IDX_asset_faces_assetId_personId', columns: ['assetId', 'personId'] }) +@Table({ name: 'asset_face' }) +// schemaFromDatabase does not preserve column order +@Index({ name: 'asset_face_assetId_personId_idx', columns: ['assetId', 'personId'] }) @Index({ columns: ['personId', 'assetId'] }) export class AssetFaceTable { @PrimaryGeneratedColumn() diff --git a/server/src/schema/tables/asset-files.table.ts b/server/src/schema/tables/asset-file.table.ts similarity index 66% rename from server/src/schema/tables/asset-files.table.ts rename to server/src/schema/tables/asset-file.table.ts index d0721e5b91..6456d1d535 100644 --- a/server/src/schema/tables/asset-files.table.ts +++ b/server/src/schema/tables/asset-file.table.ts @@ -13,18 +13,14 @@ import { UpdateDateColumn, } from 'src/sql-tools'; -@Table('asset_files') -@Unique({ name: 'UQ_assetId_type', columns: ['assetId', 'type'] }) -@UpdatedAtTrigger('asset_files_updated_at') +@Table('asset_file') +@Unique({ columns: ['assetId', 'type'] }) +@UpdatedAtTrigger('asset_file_updatedAt') export class AssetFileTable { @PrimaryGeneratedColumn() id!: Generated; - @ForeignKeyColumn(() => AssetTable, { - onDelete: 'CASCADE', - onUpdate: 'CASCADE', - indexName: 'IDX_asset_files_assetId', - }) + @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) assetId!: string; @CreateDateColumn() @@ -39,6 +35,6 @@ export class AssetFileTable { @Column() path!: string; - @UpdateIdColumn({ indexName: 'IDX_asset_files_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; } diff --git a/server/src/schema/tables/asset.table.ts b/server/src/schema/tables/asset.table.ts index de7dea2b57..4e1d073848 100644 --- a/server/src/schema/tables/asset.table.ts +++ b/server/src/schema/tables/asset.table.ts @@ -1,7 +1,7 @@ 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 { assets_delete_audit } from 'src/schema/functions'; +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'; @@ -20,11 +20,11 @@ import { } from 'src/sql-tools'; import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database'; -@Table('assets') -@UpdatedAtTrigger('assets_updated_at') +@Table('asset') +@UpdatedAtTrigger('asset_updatedAt') @AfterDeleteTrigger({ scope: 'statement', - function: assets_delete_audit, + function: asset_delete_audit, referencingOldTableAs: 'old', when: 'pg_trigger_depth() = 0', }) @@ -36,23 +36,22 @@ import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database'; where: '("libraryId" IS NULL)', }) @Index({ - name: 'UQ_assets_owner_library_checksum' + '', columns: ['ownerId', 'libraryId', 'checksum'], unique: true, where: '("libraryId" IS NOT NULL)', }) @Index({ - name: 'idx_local_date_time', + name: 'asset_localDateTime_idx', expression: `(("localDateTime" at time zone 'UTC')::date)`, }) @Index({ - name: 'idx_local_date_time_month', + name: 'asset_localDateTime_month_idx', expression: `(date_trunc('MONTH'::text, ("localDateTime" AT TIME ZONE 'UTC'::text)) AT TIME ZONE 'UTC'::text)`, }) -@Index({ name: 'IDX_originalPath_libraryId', columns: ['originalPath', 'libraryId'] }) -@Index({ name: 'IDX_asset_id_stackId', columns: ['id', 'stackId'] }) +@Index({ columns: ['originalPath', 'libraryId'] }) +@Index({ columns: ['id', 'stackId'] }) @Index({ - name: 'idx_originalfilename_trigram', + name: 'asset_originalFilename_trigram_idx', using: 'gin', expression: 'f_unaccent("originalFileName") gin_trgm_ops', }) @@ -76,7 +75,7 @@ export class AssetTable { @Column() originalPath!: string; - @Column({ type: 'timestamp with time zone', indexName: 'idx_asset_file_created_at' }) + @Column({ type: 'timestamp with time zone', index: true }) fileCreatedAt!: Timestamp; @Column({ type: 'timestamp with time zone' }) @@ -130,13 +129,13 @@ export class AssetTable { @ForeignKeyColumn(() => StackTable, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' }) stackId!: string | null; - @Column({ type: 'uuid', nullable: true, indexName: 'IDX_assets_duplicateId' }) + @Column({ type: 'uuid', nullable: true, index: true }) duplicateId!: string | null; @Column({ enum: assets_status_enum, default: AssetStatus.ACTIVE }) status!: Generated; - @UpdateIdColumn({ indexName: 'IDX_assets_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; @Column({ enum: asset_visibility_enum, default: AssetVisibility.TIMELINE }) diff --git a/server/src/schema/tables/audit.table.ts b/server/src/schema/tables/audit.table.ts index 049bba9a1b..15b4990814 100644 --- a/server/src/schema/tables/audit.table.ts +++ b/server/src/schema/tables/audit.table.ts @@ -2,7 +2,7 @@ import { DatabaseAction, EntityType } from 'src/enum'; import { Column, CreateDateColumn, Generated, Index, PrimaryColumn, Table, Timestamp } from 'src/sql-tools'; @Table('audit') -@Index({ name: 'IDX_ownerId_createdAt', columns: ['ownerId', 'createdAt'] }) +@Index({ columns: ['ownerId', 'createdAt'] }) export class AuditTable { @PrimaryColumn({ type: 'serial', synchronize: false }) id!: Generated; diff --git a/server/src/schema/tables/face-search.table.ts b/server/src/schema/tables/face-search.table.ts index 600a08833e..ff63879404 100644 --- a/server/src/schema/tables/face-search.table.ts +++ b/server/src/schema/tables/face-search.table.ts @@ -1,7 +1,7 @@ import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; -@Table({ name: 'face_search', primaryConstraintName: 'face_search_pkey' }) +@Table({ name: 'face_search' }) @Index({ name: 'face_index', using: 'hnsw', @@ -9,11 +9,7 @@ import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; with: 'ef_construction = 300, m = 16', }) export class FaceSearchTable { - @ForeignKeyColumn(() => AssetFaceTable, { - onDelete: 'CASCADE', - primary: true, - constraintName: 'face_search_faceId_fkey', - }) + @ForeignKeyColumn(() => AssetFaceTable, { onDelete: 'CASCADE', primary: true }) faceId!: string; @Column({ type: 'vector', length: 512, synchronize: false }) diff --git a/server/src/schema/tables/geodata-places.table.ts b/server/src/schema/tables/geodata-places.table.ts index abdc213e7c..eec2b240d0 100644 --- a/server/src/schema/tables/geodata-places.table.ts +++ b/server/src/schema/tables/geodata-places.table.ts @@ -1,6 +1,6 @@ import { Column, Index, PrimaryColumn, Table, Timestamp } from 'src/sql-tools'; -@Table({ name: 'geodata_places' }) +@Table({ name: 'geodata_places', primaryConstraintName: 'geodata_places_pkey' }) @Index({ name: 'idx_geodata_places_alternate_names', using: 'gin', diff --git a/server/src/schema/tables/library.table.ts b/server/src/schema/tables/library.table.ts index 0bf7e8ea82..57ad144c8e 100644 --- a/server/src/schema/tables/library.table.ts +++ b/server/src/schema/tables/library.table.ts @@ -12,8 +12,8 @@ import { UpdateDateColumn, } from 'src/sql-tools'; -@Table('libraries') -@UpdatedAtTrigger('libraries_updated_at') +@Table('library') +@UpdatedAtTrigger('library_updatedAt') export class LibraryTable { @PrimaryGeneratedColumn() id!: Generated; @@ -42,6 +42,6 @@ export class LibraryTable { @Column({ type: 'timestamp with time zone', nullable: true }) refreshedAt!: Timestamp | null; - @UpdateIdColumn({ indexName: 'IDX_libraries_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; } diff --git a/server/src/schema/tables/memory-asset-audit.table.ts b/server/src/schema/tables/memory-asset-audit.table.ts index ecb72f6270..77a889b455 100644 --- a/server/src/schema/tables/memory-asset-audit.table.ts +++ b/server/src/schema/tables/memory-asset-audit.table.ts @@ -2,22 +2,17 @@ import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { MemoryTable } from 'src/schema/tables/memory.table'; import { Column, CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools'; -@Table('memory_assets_audit') +@Table('memory_asset_audit') export class MemoryAssetAuditTable { @PrimaryGeneratedUuidV7Column() id!: string; - @ForeignKeyColumn(() => MemoryTable, { - type: 'uuid', - indexName: 'IDX_memory_assets_audit_memory_id', - onDelete: 'CASCADE', - onUpdate: 'CASCADE', - }) + @ForeignKeyColumn(() => MemoryTable, { type: 'uuid', onDelete: 'CASCADE', onUpdate: 'CASCADE' }) memoryId!: string; - @Column({ type: 'uuid', indexName: 'IDX_memory_assets_audit_asset_id' }) + @Column({ type: 'uuid', index: true }) assetId!: string; - @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_memory_assets_audit_deleted_at' }) + @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) deletedAt!: Date; } diff --git a/server/src/schema/tables/memory-asset.table.ts b/server/src/schema/tables/memory-asset.table.ts index 7ccdcdc13d..f535155233 100644 --- a/server/src/schema/tables/memory-asset.table.ts +++ b/server/src/schema/tables/memory-asset.table.ts @@ -1,5 +1,5 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { memory_assets_delete_audit } from 'src/schema/functions'; +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 { @@ -12,11 +12,11 @@ import { UpdateDateColumn, } from 'src/sql-tools'; -@Table('memories_assets_assets') -@UpdatedAtTrigger('memory_assets_updated_at') +@Table('memory_asset') +@UpdatedAtTrigger('memory_asset_updatedAt') @AfterDeleteTrigger({ scope: 'statement', - function: memory_assets_delete_audit, + function: memory_asset_delete_audit, referencingOldTableAs: 'old', when: 'pg_trigger_depth() <= 1', }) @@ -33,6 +33,6 @@ export class MemoryAssetTable { @UpdateDateColumn() updatedAt!: Generated; - @UpdateIdColumn({ indexName: 'IDX_memory_assets_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; } diff --git a/server/src/schema/tables/memory-audit.table.ts b/server/src/schema/tables/memory-audit.table.ts index d84573a023..167caf8e6e 100644 --- a/server/src/schema/tables/memory-audit.table.ts +++ b/server/src/schema/tables/memory-audit.table.ts @@ -1,17 +1,17 @@ import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; -@Table('memories_audit') +@Table('memory_audit') export class MemoryAuditTable { @PrimaryGeneratedUuidV7Column() id!: Generated; - @Column({ type: 'uuid', indexName: 'IDX_memories_audit_memory_id' }) + @Column({ type: 'uuid', index: true }) memoryId!: string; - @Column({ type: 'uuid', indexName: 'IDX_memories_audit_user_id' }) + @Column({ type: 'uuid', index: true }) userId!: string; - @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_memories_audit_deleted_at' }) + @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) deletedAt!: Generated; } diff --git a/server/src/schema/tables/memory.table.ts b/server/src/schema/tables/memory.table.ts index 30aef7e0a4..408f7bca19 100644 --- a/server/src/schema/tables/memory.table.ts +++ b/server/src/schema/tables/memory.table.ts @@ -1,6 +1,6 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { MemoryType } from 'src/enum'; -import { memories_delete_audit } from 'src/schema/functions'; +import { memory_delete_audit } from 'src/schema/functions'; import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, @@ -15,11 +15,11 @@ import { UpdateDateColumn, } from 'src/sql-tools'; -@Table('memories') -@UpdatedAtTrigger('memories_updated_at') +@Table('memory') +@UpdatedAtTrigger('memory_updatedAt') @AfterDeleteTrigger({ scope: 'statement', - function: memories_delete_audit, + function: memory_delete_audit, referencingOldTableAs: 'old', when: 'pg_trigger_depth() = 0', }) @@ -63,6 +63,6 @@ export class MemoryTable { @Column({ type: 'timestamp with time zone', nullable: true }) hideAt!: Timestamp | null; - @UpdateIdColumn({ indexName: 'IDX_memories_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; } diff --git a/server/src/schema/tables/notification.table.ts b/server/src/schema/tables/notification.table.ts index aa6cc60be9..01a93a73e5 100644 --- a/server/src/schema/tables/notification.table.ts +++ b/server/src/schema/tables/notification.table.ts @@ -13,8 +13,8 @@ import { UpdateDateColumn, } from 'src/sql-tools'; -@Table('notifications') -@UpdatedAtTrigger('notifications_updated_at') +@Table('notification') +@UpdatedAtTrigger('notification_updatedAt') export class NotificationTable { @PrimaryGeneratedColumn() id!: Generated; @@ -28,7 +28,7 @@ export class NotificationTable { @DeleteDateColumn() deletedAt!: Timestamp | null; - @UpdateIdColumn({ indexName: 'IDX_notifications_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true }) diff --git a/server/src/schema/tables/partner-audit.table.ts b/server/src/schema/tables/partner-audit.table.ts index 35595e47b4..fa2f0c27cc 100644 --- a/server/src/schema/tables/partner-audit.table.ts +++ b/server/src/schema/tables/partner-audit.table.ts @@ -1,17 +1,17 @@ import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; -@Table('partners_audit') +@Table('partner_audit') export class PartnerAuditTable { @PrimaryGeneratedUuidV7Column() id!: Generated; - @Column({ type: 'uuid', indexName: 'IDX_partners_audit_shared_by_id' }) + @Column({ type: 'uuid', index: true }) sharedById!: string; - @Column({ type: 'uuid', indexName: 'IDX_partners_audit_shared_with_id' }) + @Column({ type: 'uuid', index: true }) sharedWithId!: string; - @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_partners_audit_deleted_at' }) + @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) deletedAt!: Generated; } diff --git a/server/src/schema/tables/partner.table.ts b/server/src/schema/tables/partner.table.ts index dd5024832f..8fc332cb12 100644 --- a/server/src/schema/tables/partner.table.ts +++ b/server/src/schema/tables/partner.table.ts @@ -1,5 +1,5 @@ import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { partners_delete_audit } from 'src/schema/functions'; +import { partner_delete_audit } from 'src/schema/functions'; import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, @@ -12,11 +12,11 @@ import { UpdateDateColumn, } from 'src/sql-tools'; -@Table('partners') -@UpdatedAtTrigger('partners_updated_at') +@Table('partner') +@UpdatedAtTrigger('partner_updatedAt') @AfterDeleteTrigger({ scope: 'statement', - function: partners_delete_audit, + function: partner_delete_audit, referencingOldTableAs: 'old', when: 'pg_trigger_depth() = 0', }) @@ -35,7 +35,7 @@ export class PartnerTable { @CreateDateColumn() createdAt!: Generated; - @CreateIdColumn({ indexName: 'IDX_partners_create_id' }) + @CreateIdColumn({ index: true }) createId!: Generated; @UpdateDateColumn() @@ -44,6 +44,6 @@ export class PartnerTable { @Column({ type: 'boolean', default: false }) inTimeline!: Generated; - @UpdateIdColumn({ indexName: 'IDX_partners_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; } diff --git a/server/src/schema/tables/person-audit.table.ts b/server/src/schema/tables/person-audit.table.ts new file mode 100644 index 0000000000..8a899a1808 --- /dev/null +++ b/server/src/schema/tables/person-audit.table.ts @@ -0,0 +1,17 @@ +import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; +import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; + +@Table('person_audit') +export class PersonAuditTable { + @PrimaryGeneratedUuidV7Column() + id!: Generated; + + @Column({ type: 'uuid', index: true }) + personId!: string; + + @Column({ type: 'uuid', index: true }) + ownerId!: string; + + @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) + deletedAt!: Generated; +} diff --git a/server/src/schema/tables/person.table.ts b/server/src/schema/tables/person.table.ts index 5835b2528c..3b523a39d2 100644 --- a/server/src/schema/tables/person.table.ts +++ b/server/src/schema/tables/person.table.ts @@ -1,7 +1,9 @@ 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, Column, CreateDateColumn, @@ -14,8 +16,14 @@ import { } from 'src/sql-tools'; @Table('person') -@UpdatedAtTrigger('person_updated_at') -@Check({ name: 'CHK_b0f82b0ed662bfc24fbb58bb45', expression: `"birthDate" <= CURRENT_DATE` }) +@UpdatedAtTrigger('person_updatedAt') +@AfterDeleteTrigger({ + scope: 'statement', + function: person_delete_audit, + referencingOldTableAs: 'old', + when: 'pg_trigger_depth() = 0', +}) +@Check({ name: 'person_birthDate_chk', expression: `"birthDate" <= CURRENT_DATE` }) export class PersonTable { @PrimaryGeneratedColumn('uuid') id!: Generated; @@ -50,6 +58,6 @@ export class PersonTable { @Column({ type: 'character varying', nullable: true, default: null }) color!: string | null; - @UpdateIdColumn({ indexName: 'IDX_person_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; } diff --git a/server/src/schema/tables/session.table.ts b/server/src/schema/tables/session.table.ts index cc05500beb..706abdf887 100644 --- a/server/src/schema/tables/session.table.ts +++ b/server/src/schema/tables/session.table.ts @@ -11,8 +11,8 @@ import { UpdateDateColumn, } from 'src/sql-tools'; -@Table({ name: 'sessions', primaryConstraintName: 'PK_48cb6b5c20faa63157b3c1baf7f' }) -@UpdatedAtTrigger('sessions_updated_at') +@Table({ name: 'session' }) +@UpdatedAtTrigger('session_updatedAt') export class SessionTable { @PrimaryGeneratedColumn() id!: Generated; @@ -42,9 +42,12 @@ export class SessionTable { @Column({ default: '' }) deviceOS!: Generated; - @UpdateIdColumn({ indexName: 'IDX_sessions_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; + @Column({ type: 'boolean', default: false }) + isPendingSyncReset!: Generated; + @Column({ type: 'timestamp with time zone', nullable: true }) pinExpiresAt!: Timestamp | null; } diff --git a/server/src/schema/tables/shared-link-asset.table.ts b/server/src/schema/tables/shared-link-asset.table.ts index 66c9068441..37b652c4ab 100644 --- a/server/src/schema/tables/shared-link-asset.table.ts +++ b/server/src/schema/tables/shared-link-asset.table.ts @@ -2,7 +2,7 @@ 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') +@Table('shared_link_asset') export class SharedLinkAssetTable { @ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true }) assetsId!: string; diff --git a/server/src/schema/tables/shared-link.table.ts b/server/src/schema/tables/shared-link.table.ts index 0670a7c5f5..40fd2bf6a9 100644 --- a/server/src/schema/tables/shared-link.table.ts +++ b/server/src/schema/tables/shared-link.table.ts @@ -9,11 +9,9 @@ import { PrimaryGeneratedColumn, Table, Timestamp, - Unique, } from 'src/sql-tools'; -@Table('shared_links') -@Unique({ name: 'UQ_sharedlink_key', columns: ['key'] }) +@Table('shared_link') export class SharedLinkTable { @PrimaryGeneratedColumn() id!: Generated; @@ -24,7 +22,7 @@ export class SharedLinkTable { @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) userId!: string; - @Column({ type: 'bytea', indexName: 'IDX_sharedlink_key' }) + @Column({ type: 'bytea', index: true, unique: true }) key!: Buffer; // use to access the individual asset @Column() @@ -39,12 +37,7 @@ export class SharedLinkTable { @Column({ type: 'boolean', default: false }) allowUpload!: boolean; - @ForeignKeyColumn(() => AlbumTable, { - nullable: true, - onDelete: 'CASCADE', - onUpdate: 'CASCADE', - indexName: 'IDX_sharedlink_albumId', - }) + @ForeignKeyColumn(() => AlbumTable, { nullable: true, onDelete: 'CASCADE', onUpdate: 'CASCADE' }) albumId!: string | null; @Column({ type: 'boolean', default: true }) diff --git a/server/src/schema/tables/smart-search.table.ts b/server/src/schema/tables/smart-search.table.ts index 09362b9dda..dc140efb2f 100644 --- a/server/src/schema/tables/smart-search.table.ts +++ b/server/src/schema/tables/smart-search.table.ts @@ -1,7 +1,7 @@ import { AssetTable } from 'src/schema/tables/asset.table'; import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; -@Table({ name: 'smart_search', primaryConstraintName: 'smart_search_pkey' }) +@Table({ name: 'smart_search' }) @Index({ name: 'clip_index', using: 'hnsw', @@ -10,11 +10,7 @@ import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; synchronize: false, }) export class SmartSearchTable { - @ForeignKeyColumn(() => AssetTable, { - onDelete: 'CASCADE', - primary: true, - constraintName: 'smart_search_assetId_fkey', - }) + @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', primary: true }) assetId!: string; @Column({ type: 'vector', length: 512, storage: 'external', synchronize: false }) diff --git a/server/src/schema/tables/stack-audit.table.ts b/server/src/schema/tables/stack-audit.table.ts index 01b628d081..d46ff95e57 100644 --- a/server/src/schema/tables/stack-audit.table.ts +++ b/server/src/schema/tables/stack-audit.table.ts @@ -1,7 +1,7 @@ import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; -@Table('stacks_audit') +@Table('stack_audit') export class StackAuditTable { @PrimaryGeneratedUuidV7Column() id!: Generated; @@ -12,6 +12,6 @@ export class StackAuditTable { @Column({ type: 'uuid' }) userId!: string; - @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_stacks_audit_deleted_at' }) + @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) deletedAt!: Generated; } diff --git a/server/src/schema/tables/stack.table.ts b/server/src/schema/tables/stack.table.ts index fa206f24f2..9c9eb81373 100644 --- a/server/src/schema/tables/stack.table.ts +++ b/server/src/schema/tables/stack.table.ts @@ -1,5 +1,5 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { stacks_delete_audit } from 'src/schema/functions'; +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 { @@ -13,11 +13,11 @@ import { UpdateDateColumn, } from 'src/sql-tools'; -@Table('asset_stack') -@UpdatedAtTrigger('stacks_updated_at') +@Table('stack') +@UpdatedAtTrigger('stack_updatedAt') @AfterDeleteTrigger({ scope: 'statement', - function: stacks_delete_audit, + function: stack_delete_audit, referencingOldTableAs: 'old', when: 'pg_trigger_depth() = 0', }) diff --git a/server/src/schema/tables/sync-checkpoint.table.ts b/server/src/schema/tables/sync-checkpoint.table.ts index a5ae9181e3..6ad4c54a86 100644 --- a/server/src/schema/tables/sync-checkpoint.table.ts +++ b/server/src/schema/tables/sync-checkpoint.table.ts @@ -12,8 +12,8 @@ import { UpdateDateColumn, } from 'src/sql-tools'; -@Table('session_sync_checkpoints') -@UpdatedAtTrigger('session_sync_checkpoints_updated_at') +@Table('session_sync_checkpoint') +@UpdatedAtTrigger('session_sync_checkpoint_updatedAt') export class SessionSyncCheckpointTable { @ForeignKeyColumn(() => SessionTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', primary: true }) sessionId!: string; @@ -30,6 +30,6 @@ export class SessionSyncCheckpointTable { @Column() ack!: string; - @UpdateIdColumn({ indexName: 'IDX_session_sync_checkpoints_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; } diff --git a/server/src/schema/tables/tag-asset.table.ts b/server/src/schema/tables/tag-asset.table.ts index 8793af0a8a..bc02129217 100644 --- a/server/src/schema/tables/tag-asset.table.ts +++ b/server/src/schema/tables/tag-asset.table.ts @@ -2,7 +2,7 @@ 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({ name: 'IDX_tag_asset_assetsId_tagsId', columns: ['assetsId', 'tagsId'] }) +@Index({ columns: ['assetsId', 'tagsId'] }) @Table('tag_asset') export class TagAssetTable { @ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true, index: true }) diff --git a/server/src/schema/tables/tag-closure.table.ts b/server/src/schema/tables/tag-closure.table.ts index 8829e802e1..aeb8c8cf11 100644 --- a/server/src/schema/tables/tag-closure.table.ts +++ b/server/src/schema/tables/tag-closure.table.ts @@ -1,7 +1,7 @@ import { TagTable } from 'src/schema/tables/tag.table'; import { ForeignKeyColumn, Table } from 'src/sql-tools'; -@Table('tags_closure') +@Table('tag_closure') export class TagClosureTable { @ForeignKeyColumn(() => TagTable, { primary: true, onDelete: 'CASCADE', onUpdate: 'NO ACTION', index: true }) id_ancestor!: string; diff --git a/server/src/schema/tables/tag.table.ts b/server/src/schema/tables/tag.table.ts index 55abe0a852..dc1fa2947b 100644 --- a/server/src/schema/tables/tag.table.ts +++ b/server/src/schema/tables/tag.table.ts @@ -12,8 +12,8 @@ import { UpdateDateColumn, } from 'src/sql-tools'; -@Table('tags') -@UpdatedAtTrigger('tags_updated_at') +@Table('tag') +@UpdatedAtTrigger('tag_updatedAt') @Unique({ columns: ['userId', 'value'] }) export class TagTable { @PrimaryGeneratedColumn() @@ -42,6 +42,6 @@ export class TagTable { @ForeignKeyColumn(() => TagTable, { nullable: true, onDelete: 'CASCADE' }) parentId!: string | null; - @UpdateIdColumn({ indexName: 'IDX_tags_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; } diff --git a/server/src/schema/tables/user-audit.table.ts b/server/src/schema/tables/user-audit.table.ts index 0c20323a54..084b42fb65 100644 --- a/server/src/schema/tables/user-audit.table.ts +++ b/server/src/schema/tables/user-audit.table.ts @@ -1,7 +1,7 @@ import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; -@Table('users_audit') +@Table('user_audit') export class UserAuditTable { @PrimaryGeneratedUuidV7Column() id!: Generated; @@ -9,6 +9,6 @@ export class UserAuditTable { @Column({ type: 'uuid' }) userId!: string; - @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_users_audit_deleted_at' }) + @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) deletedAt!: Generated; } diff --git a/server/src/schema/tables/user-metadata-audit.table.ts b/server/src/schema/tables/user-metadata-audit.table.ts new file mode 100644 index 0000000000..de7d21c874 --- /dev/null +++ b/server/src/schema/tables/user-metadata-audit.table.ts @@ -0,0 +1,17 @@ +import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; +import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; + +@Table('user_metadata_audit') +export class UserMetadataAuditTable { + @PrimaryGeneratedUuidV7Column() + id!: Generated; + + @Column({ type: 'uuid', indexName: 'IDX_user_metadata_audit_user_id' }) + userId!: string; + + @Column({ indexName: 'IDX_user_metadata_audit_key' }) + key!: string; + + @CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_user_metadata_audit_deleted_at' }) + deletedAt!: Generated; +} diff --git a/server/src/schema/tables/user-metadata.table.ts b/server/src/schema/tables/user-metadata.table.ts index 04b457867f..a453ec6677 100644 --- a/server/src/schema/tables/user-metadata.table.ts +++ b/server/src/schema/tables/user-metadata.table.ts @@ -1,9 +1,27 @@ +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 { Column, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; +import { + AfterDeleteTrigger, + Column, + ForeignKeyColumn, + Generated, + PrimaryColumn, + Table, + Timestamp, + UpdateDateColumn, +} from 'src/sql-tools'; import { UserMetadata, UserMetadataItem } from 'src/types'; +@UpdatedAtTrigger('user_metadata_updated_at') @Table('user_metadata') +@AfterDeleteTrigger({ + scope: 'statement', + function: user_metadata_audit, + referencingOldTableAs: 'old', + when: 'pg_trigger_depth() = 0', +}) export class UserMetadataTable implements UserMetadataItem { @ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', @@ -19,4 +37,10 @@ export class UserMetadataTable i @Column({ type: 'jsonb' }) value!: UserMetadata[T]; + + @UpdateIdColumn({ indexName: 'IDX_user_metadata_update_id' }) + updateId!: Generated; + + @UpdateDateColumn({ indexName: 'IDX_user_metadata_updated_at' }) + updatedAt!: Generated; } diff --git a/server/src/schema/tables/user.table.ts b/server/src/schema/tables/user.table.ts index 76b60521ac..97ac0ff295 100644 --- a/server/src/schema/tables/user.table.ts +++ b/server/src/schema/tables/user.table.ts @@ -1,7 +1,7 @@ import { ColumnType } from 'kysely'; import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { UserAvatarColor, UserStatus } from 'src/enum'; -import { users_delete_audit } from 'src/schema/functions'; +import { user_delete_audit } from 'src/schema/functions'; import { AfterDeleteTrigger, Column, @@ -15,15 +15,15 @@ import { UpdateDateColumn, } from 'src/sql-tools'; -@Table('users') -@UpdatedAtTrigger('users_updated_at') +@Table('user') +@UpdatedAtTrigger('user_updatedAt') @AfterDeleteTrigger({ scope: 'statement', - function: users_delete_audit, + function: user_delete_audit, referencingOldTableAs: 'old', when: 'pg_trigger_depth() = 0', }) -@Index({ name: 'IDX_users_updated_at_asc_id_asc', columns: ['updatedAt', 'id'] }) +@Index({ columns: ['updatedAt', 'id'] }) export class UserTable { @PrimaryGeneratedColumn() id!: Generated; @@ -79,6 +79,6 @@ export class UserTable { @Column({ type: 'timestamp with time zone', default: () => 'now()' }) profileChangedAt!: Generated; - @UpdateIdColumn({ indexName: 'IDX_users_update_id' }) + @UpdateIdColumn({ index: true }) updateId!: Generated; } diff --git a/server/src/services/api.service.ts b/server/src/services/api.service.ts index 064b2e268d..27a776e867 100644 --- a/server/src/services/api.service.ts +++ b/server/src/services/api.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { Cron, CronExpression, Interval } from '@nestjs/schedule'; +import { Interval } from '@nestjs/schedule'; import { NextFunction, Request, Response } from 'express'; import { readFileSync } from 'node:fs'; import sanitizeHtml from 'sanitize-html'; @@ -54,11 +54,6 @@ export class ApiService { await this.versionService.handleQueueVersionCheck(); } - @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) - async onNightlyJob() { - await this.jobService.handleNightlyJobs(); - } - ssr(excludePaths: string[]) { const { resourcePaths } = this.configRepository.getEnv(); diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 85c9f07815..6f180b3017 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -241,6 +241,7 @@ describe(AuthService.name, () => { const sessionWithToken = { id: session.id, updatedAt: session.updatedAt, + isPendingSyncReset: false, user: factory.authUser(), pinExpiresAt: null, }; @@ -255,7 +256,11 @@ describe(AuthService.name, () => { }), ).resolves.toEqual({ user: sessionWithToken.user, - session: { id: session.id, hasElevatedPermission: false }, + session: { + id: session.id, + hasElevatedPermission: false, + isPendingSyncReset: session.isPendingSyncReset, + }, }); }); }); @@ -366,6 +371,7 @@ describe(AuthService.name, () => { id: session.id, updatedAt: session.updatedAt, user: factory.authUser(), + isPendingSyncReset: false, pinExpiresAt: null, }; @@ -379,7 +385,11 @@ describe(AuthService.name, () => { }), ).resolves.toEqual({ user: sessionWithToken.user, - session: { id: session.id, hasElevatedPermission: false }, + session: { + id: session.id, + hasElevatedPermission: false, + isPendingSyncReset: session.isPendingSyncReset, + }, }); }); @@ -389,6 +399,7 @@ describe(AuthService.name, () => { id: session.id, updatedAt: session.updatedAt, user: factory.authUser(), + isPendingSyncReset: false, pinExpiresAt: null, }; @@ -409,6 +420,7 @@ describe(AuthService.name, () => { id: session.id, updatedAt: session.updatedAt, user: factory.authUser(), + isPendingSyncReset: false, pinExpiresAt: null, }; diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index ec3415ec8c..2d6c4b1b15 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -466,6 +466,7 @@ export class AuthService extends BaseService { user: session.user, session: { id: session.id, + isPendingSyncReset: session.isPendingSyncReset, hasElevatedPermission, }, }; diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index c9020ed96a..a18eccdd8b 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -41,12 +41,12 @@ describe(JobService.name, () => { { name: JobName.USER_DELETE_CHECK }, { name: JobName.PERSON_CLEANUP }, { name: JobName.MEMORIES_CLEANUP }, - { name: JobName.MEMORIES_CREATE }, - { name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }, - { name: JobName.CLEAN_OLD_AUDIT_LOGS }, - { name: JobName.USER_SYNC_USAGE }, - { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false, nightly: true } }, { name: JobName.CLEAN_OLD_SESSION_TOKENS }, + { name: JobName.CLEAN_OLD_AUDIT_LOGS }, + { name: JobName.MEMORIES_CREATE }, + { name: JobName.USER_SYNC_USAGE }, + { name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }, + { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false, nightly: true } }, ]); }); }); diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index fd573d9b97..37a66706c0 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -1,6 +1,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { ClassConstructor } from 'class-transformer'; import { snakeCase } from 'lodash'; +import { SystemConfig } from 'src/config'; import { OnEvent } from 'src/decorators'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto'; @@ -8,6 +9,8 @@ import { AssetType, AssetVisibility, BootstrapEventPriority, + CronJob, + DatabaseLock, ImmichWorker, JobCommand, JobName, @@ -19,6 +22,8 @@ import { import { ArgOf, ArgsOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; import { ConcurrentQueueName, JobItem } from 'src/types'; +import { hexOrBufferToBase64 } from 'src/utils/bytes'; +import { handlePromiseError } from 'src/utils/misc'; const asJobItem = (dto: JobCreateDto): JobItem => { switch (dto.name) { @@ -52,12 +57,59 @@ const asJobItem = (dto: JobCreateDto): JobItem => { } }; +const asNightlyTasksCron = (config: SystemConfig) => { + const [hours, minutes] = config.nightlyTasks.startTime.split(':').map(Number); + return `${minutes} ${hours} * * *`; +}; + @Injectable() export class JobService extends BaseService { private services: ClassConstructor[] = []; + private nightlyJobsLock = false; - @OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] }) - onConfigInit({ newConfig: config }: ArgOf<'config.init'>) { + @OnEvent({ name: 'config.init' }) + async onConfigInit({ newConfig: config }: ArgOf<'config.init'>) { + if (this.worker === ImmichWorker.MICROSERVICES) { + this.updateQueueConcurrency(config); + return; + } + + this.nightlyJobsLock = await this.databaseRepository.tryLock(DatabaseLock.NightlyJobs); + if (this.nightlyJobsLock) { + const cronExpression = asNightlyTasksCron(config); + this.logger.debug(`Scheduling nightly jobs for ${cronExpression}`); + this.cronRepository.create({ + name: CronJob.NightlyJobs, + expression: cronExpression, + start: true, + onTick: () => handlePromiseError(this.handleNightlyJobs(), this.logger), + }); + } + } + + @OnEvent({ name: 'config.update', server: true }) + onConfigUpdate({ newConfig: config }: ArgOf<'config.update'>) { + if (this.worker === ImmichWorker.MICROSERVICES) { + this.updateQueueConcurrency(config); + return; + } + + if (this.nightlyJobsLock) { + const cronExpression = asNightlyTasksCron(config); + this.logger.debug(`Scheduling nightly jobs for ${cronExpression}`); + this.cronRepository.update({ name: CronJob.NightlyJobs, expression: cronExpression, start: true }); + } + } + + @OnEvent({ name: 'app.bootstrap', priority: BootstrapEventPriority.JobService }) + onBootstrap() { + this.jobRepository.setup(this.services); + if (this.worker === ImmichWorker.MICROSERVICES) { + this.jobRepository.startWorkers(); + } + } + + private updateQueueConcurrency(config: SystemConfig) { this.logger.debug(`Updating queue concurrency settings`); for (const queueName of Object.values(QueueName)) { let concurrency = 1; @@ -69,19 +121,6 @@ export class JobService extends BaseService { } } - @OnEvent({ name: 'config.update', server: true, workers: [ImmichWorker.MICROSERVICES] }) - onConfigUpdate({ newConfig: config }: ArgOf<'config.update'>) { - this.onConfigInit({ newConfig: config }); - } - - @OnEvent({ name: 'app.bootstrap', priority: BootstrapEventPriority.JobService }) - onBootstrap() { - this.jobRepository.setup(this.services); - if (this.worker === ImmichWorker.MICROSERVICES) { - this.jobRepository.startWorkers(); - } - } - setServices(services: ClassConstructor[]) { this.services = services; } @@ -232,18 +271,37 @@ export class JobService extends BaseService { } async handleNightlyJobs() { - await this.jobRepository.queueAll([ - { name: JobName.ASSET_DELETION_CHECK }, - { name: JobName.USER_DELETE_CHECK }, - { name: JobName.PERSON_CLEANUP }, - { name: JobName.MEMORIES_CLEANUP }, - { name: JobName.MEMORIES_CREATE }, - { name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }, - { name: JobName.CLEAN_OLD_AUDIT_LOGS }, - { name: JobName.USER_SYNC_USAGE }, - { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false, nightly: true } }, - { name: JobName.CLEAN_OLD_SESSION_TOKENS }, - ]); + const config = await this.getConfig({ withCache: false }); + const jobs: JobItem[] = []; + + if (config.nightlyTasks.databaseCleanup) { + jobs.push( + { name: JobName.ASSET_DELETION_CHECK }, + { name: JobName.USER_DELETE_CHECK }, + { name: JobName.PERSON_CLEANUP }, + { name: JobName.MEMORIES_CLEANUP }, + { name: JobName.CLEAN_OLD_SESSION_TOKENS }, + { name: JobName.CLEAN_OLD_AUDIT_LOGS }, + ); + } + + if (config.nightlyTasks.generateMemories) { + jobs.push({ name: JobName.MEMORIES_CREATE }); + } + + if (config.nightlyTasks.syncQuotaUsage) { + jobs.push({ name: JobName.USER_SYNC_USAGE }); + } + + if (config.nightlyTasks.missingThumbnails) { + jobs.push({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }); + } + + if (config.nightlyTasks.clusterNewFaces) { + jobs.push({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false, nightly: true } }); + } + + await this.jobRepository.queueAll(jobs); } /** @@ -304,6 +362,56 @@ export class JobService extends BaseService { await this.jobRepository.queueAll(jobs); if (asset.visibility === AssetVisibility.TIMELINE || asset.visibility === AssetVisibility.ARCHIVE) { this.eventRepository.clientSend('on_upload_success', asset.ownerId, mapAsset(asset)); + if (asset.exifInfo) { + const exif = asset.exifInfo; + this.eventRepository.clientSend('AssetUploadReadyV1', asset.ownerId, { + // TODO remove `on_upload_success` and then modify the query to select only the required fields) + asset: { + id: asset.id, + ownerId: asset.ownerId, + originalFileName: asset.originalFileName, + thumbhash: asset.thumbhash ? hexOrBufferToBase64(asset.thumbhash) : null, + checksum: hexOrBufferToBase64(asset.checksum), + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + localDateTime: asset.localDateTime, + duration: asset.duration, + type: asset.type, + deletedAt: asset.deletedAt, + isFavorite: asset.isFavorite, + visibility: asset.visibility, + livePhotoVideoId: asset.livePhotoVideoId, + stackId: asset.stackId, + }, + exif: { + assetId: exif.assetId, + description: exif.description, + exifImageWidth: exif.exifImageWidth, + exifImageHeight: exif.exifImageHeight, + fileSizeInByte: exif.fileSizeInByte, + orientation: exif.orientation, + dateTimeOriginal: exif.dateTimeOriginal, + modifyDate: exif.modifyDate, + timeZone: exif.timeZone, + latitude: exif.latitude, + longitude: exif.longitude, + projectionType: exif.projectionType, + city: exif.city, + state: exif.state, + country: exif.country, + make: exif.make, + model: exif.model, + lensModel: exif.lensModel, + fNumber: exif.fNumber, + focalLength: exif.focalLength, + iso: exif.iso, + exposureTime: exif.exposureTime, + profileDescription: exif.profileDescription, + rating: exif.rating, + fps: exif.fps, + }, + }); + } } break; diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index e92cdcf200..ab69e22b99 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -3,7 +3,7 @@ import { Stats } from 'node:fs'; import { defaults, SystemConfig } from 'src/config'; import { JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants'; import { mapLibrary } from 'src/dtos/library.dto'; -import { AssetType, ImmichWorker, JobName, JobStatus } from 'src/enum'; +import { AssetType, CronJob, ImmichWorker, JobName, JobStatus } from 'src/enum'; import { LibraryService } from 'src/services/library.service'; import { ILibraryBulkIdsJob, ILibraryFileJob } from 'src/types'; import { assetStub } from 'test/fixtures/asset.stub'; @@ -56,7 +56,11 @@ describe(LibraryService.name, () => { } as SystemConfig, }); - expect(mocks.cron.update).toHaveBeenCalledWith({ name: 'libraryScan', expression: '0 1 * * *', start: true }); + expect(mocks.cron.update).toHaveBeenCalledWith({ + name: CronJob.LibraryScan, + expression: '0 1 * * *', + start: true, + }); }); it('should initialize watcher for all external libraries', async () => { @@ -128,7 +132,7 @@ describe(LibraryService.name, () => { }); expect(mocks.cron.update).toHaveBeenCalledWith({ - name: 'libraryScan', + name: CronJob.LibraryScan, expression: systemConfigStub.libraryScan.library.scan.cronExpression, start: systemConfigStub.libraryScan.library.scan.enabled, }); @@ -149,7 +153,7 @@ describe(LibraryService.name, () => { }); expect(mocks.cron.update).toHaveBeenCalledWith({ - name: 'libraryScan', + name: CronJob.LibraryScan, expression: systemConfigStub.libraryScan.library.scan.cronExpression, start: systemConfigStub.libraryScan.library.scan.enabled, }); diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 8c7f79ee5c..f6bc5b2ebb 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -17,7 +17,7 @@ import { ValidateLibraryImportPathResponseDto, ValidateLibraryResponseDto, } from 'src/dtos/library.dto'; -import { AssetStatus, AssetType, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; +import { AssetStatus, AssetType, CronJob, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { AssetSyncResult } from 'src/repositories/library.repository'; import { AssetTable } from 'src/schema/tables/asset.table'; @@ -45,7 +45,7 @@ export class LibraryService extends BaseService { if (this.lock) { this.cronRepository.create({ - name: 'libraryScan', + name: CronJob.LibraryScan, expression: scan.cronExpression, onTick: () => handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL }), this.logger), @@ -65,7 +65,7 @@ export class LibraryService extends BaseService { } this.cronRepository.update({ - name: 'libraryScan', + name: CronJob.LibraryScan, expression: library.scan.cronExpression, start: library.scan.enabled, }); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 3212d150f6..9804be9ea4 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -24,8 +24,8 @@ import { import { ArgOf } from 'src/repositories/event.repository'; import { ReverseGeocodeResult } from 'src/repositories/map.repository'; import { ImmichTags } from 'src/repositories/metadata.repository'; +import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; -import { ExifTable } from 'src/schema/tables/exif.table'; import { PersonTable } from 'src/schema/tables/person.table'; import { BaseService } from 'src/services/base.service'; import { JobItem, JobOf } from 'src/types'; @@ -164,7 +164,7 @@ export class MetadataService extends BaseService { private async linkLivePhotos( asset: { id: string; type: AssetType; ownerId: string; libraryId: string | null }, - exifInfo: Insertable, + exifInfo: Insertable, ): Promise { if (!exifInfo.livePhotoCID) { return; @@ -242,7 +242,7 @@ export class MetadataService extends BaseService { } } - const exifData: Insertable = { + const exifData: Insertable = { assetId: asset.id, // dates diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts index 059ff00e16..198e380c53 100644 --- a/server/src/services/session.service.ts +++ b/server/src/services/session.service.ts @@ -2,7 +2,13 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { OnJob } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; -import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto, mapSession } from 'src/dtos/session.dto'; +import { + SessionCreateDto, + SessionCreateResponseDto, + SessionResponseDto, + SessionUpdateDto, + mapSession, +} from 'src/dtos/session.dto'; import { JobName, JobStatus, Permission, QueueName } from 'src/enum'; import { BaseService } from 'src/services/base.service'; @@ -44,6 +50,20 @@ export class SessionService extends BaseService { return sessions.map((session) => mapSession(session, auth.session?.id)); } + async update(auth: AuthDto, id: string, dto: SessionUpdateDto): Promise { + await this.requireAccess({ auth, permission: Permission.SESSION_UPDATE, ids: [id] }); + + if (Object.values(dto).filter((prop) => prop !== undefined).length === 0) { + throw new BadRequestException('No fields to update'); + } + + const session = await this.sessionRepository.update(id, { + isPendingSyncReset: dto.isPendingSyncReset, + }); + + return mapSession(session); + } + async delete(auth: AuthDto, id: string): Promise { await this.requireAccess({ auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] }); await this.sessionRepository.delete(id); diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index e3322de2e1..9779498d70 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -22,7 +22,7 @@ import { SyncAck } from 'src/types'; import { getMyPartnerIds } from 'src/utils/asset.util'; import { hexOrBufferToBase64 } from 'src/utils/bytes'; import { setIsEqual } from 'src/utils/set'; -import { fromAck, serialize, SerializeOptions, toAck } from 'src/utils/sync'; +import { fromAck, mapJsonLine, serialize, SerializeOptions, toAck } from 'src/utils/sync'; type CheckpointMap = Partial>; type AssetLike = Omit & { @@ -69,6 +69,8 @@ export const SYNC_TYPES_ORDER = [ SyncRequestType.PartnerAssetExifsV1, SyncRequestType.MemoriesV1, SyncRequestType.MemoryToAssetsV1, + SyncRequestType.PeopleV1, + SyncRequestType.UserMetadataV1, ]; const throwSessionRequired = () => { @@ -117,30 +119,44 @@ export class SyncService extends BaseService { } async stream(auth: AuthDto, response: Writable, dto: SyncStreamDto) { - const sessionId = auth.session?.id; - if (!sessionId) { + const session = auth.session; + if (!session) { return throwSessionRequired(); } - const checkpoints = await this.syncCheckpointRepository.getAll(sessionId); + if (dto.reset) { + await this.sessionRepository.resetSyncProgress(session.id); + session.isPendingSyncReset = false; + } + + if (session.isPendingSyncReset) { + response.write(mapJsonLine({ type: SyncEntityType.SyncResetV1, data: {} })); + response.end(); + return; + } + + const checkpoints = await this.syncCheckpointRepository.getAll(session.id); const checkpointMap: CheckpointMap = Object.fromEntries(checkpoints.map(({ type, ack }) => [type, fromAck(ack)])); + const handlers: Record Promise> = { [SyncRequestType.UsersV1]: () => this.syncUsersV1(response, checkpointMap), [SyncRequestType.PartnersV1]: () => this.syncPartnersV1(response, checkpointMap, auth), [SyncRequestType.AssetsV1]: () => this.syncAssetsV1(response, checkpointMap, auth), [SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(response, checkpointMap, auth), - [SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(response, checkpointMap, auth, sessionId), + [SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(response, checkpointMap, auth, session.id), [SyncRequestType.PartnerAssetExifsV1]: () => - this.syncPartnerAssetExifsV1(response, checkpointMap, auth, sessionId), + this.syncPartnerAssetExifsV1(response, checkpointMap, auth, session.id), [SyncRequestType.AlbumsV1]: () => this.syncAlbumsV1(response, checkpointMap, auth), - [SyncRequestType.AlbumUsersV1]: () => this.syncAlbumUsersV1(response, checkpointMap, auth, sessionId), - [SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(response, checkpointMap, auth, sessionId), - [SyncRequestType.AlbumToAssetsV1]: () => this.syncAlbumToAssetsV1(response, checkpointMap, auth, sessionId), - [SyncRequestType.AlbumAssetExifsV1]: () => this.syncAlbumAssetExifsV1(response, checkpointMap, auth, sessionId), + [SyncRequestType.AlbumUsersV1]: () => this.syncAlbumUsersV1(response, checkpointMap, auth, session.id), + [SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(response, checkpointMap, auth, session.id), + [SyncRequestType.AlbumToAssetsV1]: () => this.syncAlbumToAssetsV1(response, checkpointMap, auth, session.id), + [SyncRequestType.AlbumAssetExifsV1]: () => this.syncAlbumAssetExifsV1(response, checkpointMap, auth, session.id), [SyncRequestType.MemoriesV1]: () => this.syncMemoriesV1(response, checkpointMap, auth), [SyncRequestType.MemoryToAssetsV1]: () => this.syncMemoryAssetsV1(response, checkpointMap, auth), [SyncRequestType.StacksV1]: () => this.syncStackV1(response, checkpointMap, auth), - [SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(response, checkpointMap, auth, sessionId), + [SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(response, checkpointMap, auth, session.id), + [SyncRequestType.PeopleV1]: () => this.syncPeopleV1(response, checkpointMap, auth), + [SyncRequestType.UserMetadataV1]: () => this.syncUserMetadataV1(response, checkpointMap, auth), }; for (const type of SYNC_TYPES_ORDER.filter((type) => dto.types.includes(type))) { @@ -488,7 +504,7 @@ export class SyncService extends BaseService { private async syncMemoriesV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) { const deleteType = SyncEntityType.MemoryDeleteV1; - const deletes = this.syncRepository.memory.getDeletes(auth.user.id, checkpointMap[SyncEntityType.MemoryDeleteV1]); + const deletes = this.syncRepository.memory.getDeletes(auth.user.id, checkpointMap[deleteType]); for await (const { id, ...data } of deletes) { send(response, { type: deleteType, ids: [id], data }); } @@ -576,6 +592,36 @@ export class SyncService extends BaseService { } } + private async syncPeopleV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) { + const deleteType = SyncEntityType.PersonDeleteV1; + const deletes = this.syncRepository.people.getDeletes(auth.user.id, checkpointMap[deleteType]); + for await (const { id, ...data } of deletes) { + send(response, { type: deleteType, ids: [id], data }); + } + + const upsertType = SyncEntityType.PersonV1; + const upserts = this.syncRepository.people.getUpserts(auth.user.id, checkpointMap[upsertType]); + for await (const { updateId, ...data } of upserts) { + send(response, { type: upsertType, ids: [updateId], data }); + } + } + + private async syncUserMetadataV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) { + const deleteType = SyncEntityType.UserMetadataDeleteV1; + const deletes = this.syncRepository.userMetadata.getDeletes(auth.user.id, checkpointMap[deleteType]); + + for await (const { id, ...data } of deletes) { + send(response, { type: deleteType, ids: [id], data }); + } + + const upsertType = SyncEntityType.UserMetadataV1; + const upserts = this.syncRepository.userMetadata.getUpserts(auth.user.id, checkpointMap[upsertType]); + + for await (const { updateId, ...data } of upserts) { + send(response, { type: upsertType, ids: [updateId], data }); + } + } + private async upsertBackfillCheckpoint(item: { type: SyncEntityType; sessionId: string; createId: string }) { const { type, sessionId, createId } = item; await this.syncCheckpointRepository.upsertAll([ diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index c7b98cc990..43be323459 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -103,6 +103,14 @@ const updatedConfig = Object.freeze({ lightStyle: 'https://tiles.immich.cloud/v1/style/light.json', darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json', }, + nightlyTasks: { + startTime: '00:00', + databaseCleanup: true, + clusterNewFaces: true, + missingThumbnails: true, + generateMemories: true, + syncQuotaUsage: true, + }, reverseGeocoding: { enabled: true, }, diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index 2dd8dcf0ee..4c4aa6bab7 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -27,7 +27,7 @@ export class SystemConfigService extends BaseService { return mapConfig(defaults); } - @OnEvent({ name: 'config.init' }) + @OnEvent({ name: 'config.init', priority: -100 }) onConfigInit({ newConfig: { logging } }: ArgOf<'config.init'>) { const { logLevel: envLevel } = this.configRepository.getEnv(); const configLevel = logging.enabled ? logging.level : false; diff --git a/server/src/sql-tools/comparers/column.comparer.spec.ts b/server/src/sql-tools/comparers/column.comparer.spec.ts index 25ef8543a8..fde237ad7b 100644 --- a/server/src/sql-tools/comparers/column.comparer.spec.ts +++ b/server/src/sql-tools/comparers/column.comparer.spec.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from 'vitest'; const testColumn: DatabaseColumn = { name: 'test', tableName: 'table1', + primary: false, nullable: false, isArray: false, type: 'character varying', diff --git a/server/src/sql-tools/comparers/column.comparer.ts b/server/src/sql-tools/comparers/column.comparer.ts index 5cc3f7a930..035fd6fc98 100644 --- a/server/src/sql-tools/comparers/column.comparer.ts +++ b/server/src/sql-tools/comparers/column.comparer.ts @@ -1,7 +1,32 @@ -import { getColumnType, isDefaultEqual } from 'src/sql-tools/helpers'; +import { asRenameKey, getColumnType, isDefaultEqual } from 'src/sql-tools/helpers'; import { Comparer, DatabaseColumn, Reason, SchemaDiff } from 'src/sql-tools/types'; -export const compareColumns: Comparer = { +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', @@ -67,7 +92,7 @@ export const compareColumns: Comparer = { return items; }, -}; +} satisfies Comparer; const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => { return [ diff --git a/server/src/sql-tools/comparers/constraint.comparer.ts b/server/src/sql-tools/comparers/constraint.comparer.ts index 0ff6fbe131..dda184039f 100644 --- a/server/src/sql-tools/comparers/constraint.comparer.ts +++ b/server/src/sql-tools/comparers/constraint.comparer.ts @@ -1,4 +1,4 @@ -import { haveEqualColumns } from 'src/sql-tools/helpers'; +import { asRenameKey, haveEqualColumns } from 'src/sql-tools/helpers'; import { CompareFunction, Comparer, @@ -13,6 +13,38 @@ import { } 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', diff --git a/server/src/sql-tools/comparers/index.comparer.ts b/server/src/sql-tools/comparers/index.comparer.ts index 99571cf61a..a3db9a61e0 100644 --- a/server/src/sql-tools/comparers/index.comparer.ts +++ b/server/src/sql-tools/comparers/index.comparer.ts @@ -1,7 +1,23 @@ -import { haveEqualColumns } from 'src/sql-tools/helpers'; +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', diff --git a/server/src/sql-tools/contexts/base-context.ts b/server/src/sql-tools/contexts/base-context.ts index 620286cae3..0fa7230a00 100644 --- a/server/src/sql-tools/contexts/base-context.ts +++ b/server/src/sql-tools/contexts/base-context.ts @@ -1,3 +1,6 @@ +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, @@ -11,6 +14,26 @@ import { 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; @@ -24,10 +47,17 @@ export class BaseContext { 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) { diff --git a/server/src/sql-tools/contexts/processor-context.ts b/server/src/sql-tools/contexts/processor-context.ts index 562880a925..3ab196b0af 100644 --- a/server/src/sql-tools/contexts/processor-context.ts +++ b/server/src/sql-tools/contexts/processor-context.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-function-type */ import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { ColumnOptions, TableOptions } from 'src/sql-tools/decorators'; -import { asKey } from 'src/sql-tools/helpers'; +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 }; @@ -59,19 +59,6 @@ export class ProcessorContext extends BaseContext { tableMetadata.methodToColumn.set(propertyName, column); } - asIndexName(table: string, columns?: string[], where?: string) { - const items: string[] = []; - for (const columnName of columns ?? []) { - items.push(columnName); - } - - if (where) { - items.push(where); - } - - return asKey('IDX_', table, items); - } - warnMissingTable(context: string, object: object, propertyName?: symbol | string) { const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : ''); this.warn(context, `Unable to find table (${label})`); diff --git a/server/src/sql-tools/decorators/index.ts b/server/src/sql-tools/decorators/index.ts deleted file mode 100644 index 86affe5002..0000000000 --- a/server/src/sql-tools/decorators/index.ts +++ /dev/null @@ -1,22 +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'; diff --git a/server/src/sql-tools/helpers.ts b/server/src/sql-tools/helpers.ts index afa735e282..8131f0350b 100644 --- a/server/src/sql-tools/helpers.ts +++ b/server/src/sql-tools/helpers.ts @@ -2,13 +2,6 @@ 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 asMetadataKey = (name: string) => `sql-tools:${name}`; - -export const asSnakeCase = (name: string): string => name.replaceAll(/([a-z])([A-Z])/g, '$1_$2').toLowerCase(); -// match TypeORM -export const asKey = (prefix: string, tableName: string, values: string[]) => - (prefix + sha1(`${tableName}_${values.toSorted().join('_')}`)).slice(0, 30); - export const asOptions = (options: string | T): T => { if (typeof options === 'string') { return { name: options } as T; @@ -79,6 +72,10 @@ export const compare = ( 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]; @@ -92,22 +89,63 @@ export const compare = ( } if (source && !target) { - items.push(...comparer.onMissing(source)); - } else if (!source && target) { - items.push(...comparer.onExtra(target)); - } else { - if ( - haveEqualOverrides( - source as unknown as { override?: DatabaseOverride }, - target as unknown as { override?: DatabaseOverride }, - ) - ) { + 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; } - items.push(...comparer.onCompare(source, target)); + + 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; }; @@ -186,8 +224,6 @@ export const asColumnComment = (tableName: string, columnName: string, comment: export const asColumnList = (columns: string[]) => columns.map((column) => `"${column}"`).join(', '); -export const asForeignKeyConstraintName = (table: string, columns: string[]) => asKey('FK_', table, [...columns]); - export const asJsonString = (value: unknown): string => { return `'${escape(JSON.stringify(value))}'::jsonb`; }; @@ -202,3 +238,6 @@ const escape = (value: string) => { .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/naming/default.naming.ts b/server/src/sql-tools/naming/default.naming.ts new file mode 100644 index 0000000000..807580169d --- /dev/null +++ b/server/src/sql-tools/naming/default.naming.ts @@ -0,0 +1,50 @@ +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 new file mode 100644 index 0000000000..575d0f1239 --- /dev/null +++ b/server/src/sql-tools/naming/hash.naming.ts @@ -0,0 +1,51 @@ +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 new file mode 100644 index 0000000000..f331a22c46 --- /dev/null +++ b/server/src/sql-tools/naming/naming.interface.ts @@ -0,0 +1,59 @@ +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 index 21ee47ccc6..5eba1015bf 100644 --- a/server/src/sql-tools/processors/check-constraint.processor.ts +++ b/server/src/sql-tools/processors/check-constraint.processor.ts @@ -1,4 +1,3 @@ -import { asKey } from 'src/sql-tools/helpers'; import { ConstraintType, Processor } from 'src/sql-tools/types'; export const processCheckConstraints: Processor = (ctx, items) => { @@ -15,12 +14,10 @@ export const processCheckConstraints: Processor = (ctx, items) => { table.constraints.push({ type: ConstraintType.CHECK, - name: options.name || asCheckConstraintName(tableName, options.expression), + name: options.name || ctx.getNameFor({ type: 'check', tableName, expression: options.expression }), tableName, expression: options.expression, synchronize: options.synchronize ?? true, }); } }; - -const asCheckConstraintName = (table: string, expression: string) => asKey('CHK_', table, [expression]); diff --git a/server/src/sql-tools/processors/column.processor.ts b/server/src/sql-tools/processors/column.processor.ts index 84c158a234..9b499b380b 100644 --- a/server/src/sql-tools/processors/column.processor.ts +++ b/server/src/sql-tools/processors/column.processor.ts @@ -13,7 +13,7 @@ export const processColumns: Processor = (ctx, items) => { continue; } - const columnName = options.name ?? String(propertyName); + 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 diff --git a/server/src/sql-tools/processors/database.processor.ts b/server/src/sql-tools/processors/database.processor.ts index 7158cd9494..9f2e847fd6 100644 --- a/server/src/sql-tools/processors/database.processor.ts +++ b/server/src/sql-tools/processors/database.processor.ts @@ -1,10 +1,9 @@ -import { asSnakeCase } from 'src/sql-tools/helpers'; 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 || asSnakeCase(object.name); + ctx.databaseName = options.name || ctx.getNameFor({ type: 'database', name: object.name }); } }; diff --git a/server/src/sql-tools/processors/foreign-key-column.processor.ts b/server/src/sql-tools/processors/foreign-key-column.processor.ts index e37b6e3f0b..6d147a78eb 100644 --- a/server/src/sql-tools/processors/foreign-key-column.processor.ts +++ b/server/src/sql-tools/processors/foreign-key-column.processor.ts @@ -1,4 +1,3 @@ -import { asForeignKeyConstraintName, asKey } from 'src/sql-tools/helpers'; import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types'; export const processForeignKeyColumns: Processor = (ctx, items) => { @@ -31,15 +30,24 @@ export const processForeignKeyColumns: Processor = (ctx, items) => { column.type = referenceColumns[0].type; } + const referenceTableName = referenceTable.name; const referenceColumnNames = referenceColumns.map((column) => column.name); - const name = options.constraintName || asForeignKeyConstraintName(table.name, columnNames); + 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: referenceTable.name, + referenceTableName, referenceColumnNames, onUpdate: options.onUpdate as ActionType, onDelete: options.onDelete as ActionType, @@ -48,7 +56,7 @@ export const processForeignKeyColumns: Processor = (ctx, items) => { if (options.unique || options.uniqueConstraintName) { table.constraints.push({ - name: options.uniqueConstraintName || asRelationKeyConstraintName(table.name, columnNames), + name: options.uniqueConstraintName || ctx.getNameFor({ type: 'unique', tableName: table.name, columnNames }), tableName: table.name, columnNames, type: ConstraintType.UNIQUE, @@ -57,5 +65,3 @@ export const processForeignKeyColumns: Processor = (ctx, items) => { } } }; - -const asRelationKeyConstraintName = (table: string, columns: string[]) => asKey('REL_', table, columns); diff --git a/server/src/sql-tools/processors/foreign-key-constraint.processor.ts b/server/src/sql-tools/processors/foreign-key-constraint.processor.ts index 7c44ab2694..39d7508d11 100644 --- a/server/src/sql-tools/processors/foreign-key-constraint.processor.ts +++ b/server/src/sql-tools/processors/foreign-key-constraint.processor.ts @@ -1,4 +1,3 @@ -import { asForeignKeyConstraintName } from 'src/sql-tools/helpers'; import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types'; export const processForeignKeyConstraints: Processor = (ctx, items) => { @@ -46,18 +45,27 @@ export const processForeignKeyConstraints: Processor = (ctx, items) => { continue; } - const referenceColumns = + const referenceTableName = referenceTable.name; + const referenceColumnNames = options.referenceColumns || referenceTable.columns.filter(({ primary }) => primary).map(({ name }) => name); - const name = options.name || asForeignKeyConstraintName(table.name, options.columns); + 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: referenceTable.name, - referenceColumnNames: referenceColumns, + referenceTableName, + referenceColumnNames, onUpdate: options.onUpdate as ActionType, onDelete: options.onDelete as ActionType, synchronize: options.synchronize ?? true, @@ -68,8 +76,15 @@ export const processForeignKeyConstraints: Processor = (ctx, items) => { } 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: options.indexName || ctx.asIndexName(table.name, options.columns), + name: indexName, tableName: table.name, columnNames: options.columns, unique: false, diff --git a/server/src/sql-tools/processors/index.processor.ts b/server/src/sql-tools/processors/index.processor.ts index cd2ee1507e..766e83fe8b 100644 --- a/server/src/sql-tools/processors/index.processor.ts +++ b/server/src/sql-tools/processors/index.processor.ts @@ -10,8 +10,17 @@ export const processIndexes: Processor = (ctx, items) => { continue; } + const indexName = + options.name || + ctx.getNameFor({ + type: 'index', + tableName: table.name, + columnNames: options.columns, + where: options.where, + }); + table.indexes.push({ - name: options.name || ctx.asIndexName(table.name, options.columns, options.where), + name: indexName, tableName: table.name, unique: options.unique ?? false, expression: options.expression, @@ -50,7 +59,13 @@ export const processIndexes: Processor = (ctx, items) => { continue; } - const indexName = options.indexName || ctx.asIndexName(table.name, [column.name]); + 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) { diff --git a/server/src/sql-tools/processors/primary-key-contraint.processor.ts b/server/src/sql-tools/processors/primary-key-contraint.processor.ts index 12fc27db7e..0971bfc337 100644 --- a/server/src/sql-tools/processors/primary-key-contraint.processor.ts +++ b/server/src/sql-tools/processors/primary-key-contraint.processor.ts @@ -1,4 +1,3 @@ -import { asKey } from 'src/sql-tools/helpers'; import { ConstraintType, Processor } from 'src/sql-tools/types'; export const processPrimaryKeyConstraints: Processor = (ctx) => { @@ -15,7 +14,13 @@ export const processPrimaryKeyConstraints: Processor = (ctx) => { const tableMetadata = ctx.getTableMetadata(table); table.constraints.push({ type: ConstraintType.PRIMARY_KEY, - name: tableMetadata.options.primaryConstraintName || asPrimaryKeyConstraintName(table.name, columnNames), + name: + tableMetadata.options.primaryConstraintName || + ctx.getNameFor({ + type: 'primaryKey', + tableName: table.name, + columnNames, + }), tableName: table.name, columnNames, synchronize: tableMetadata.options.synchronize ?? true, @@ -23,5 +28,3 @@ export const processPrimaryKeyConstraints: Processor = (ctx) => { } } }; - -const asPrimaryKeyConstraintName = (table: string, columns: string[]) => asKey('PK_', table, columns); diff --git a/server/src/sql-tools/processors/table.processor.ts b/server/src/sql-tools/processors/table.processor.ts index 7fec4eb317..993c9ec45d 100644 --- a/server/src/sql-tools/processors/table.processor.ts +++ b/server/src/sql-tools/processors/table.processor.ts @@ -1,4 +1,3 @@ -import { asSnakeCase } from 'src/sql-tools/helpers'; import { Processor } from 'src/sql-tools/types'; export const processTables: Processor = (ctx, items) => { @@ -14,7 +13,7 @@ export const processTables: Processor = (ctx, items) => { ctx.addTable( { - name: options.name || asSnakeCase(object.name), + name: options.name || ctx.getNameFor({ type: 'table', name: object.name }), columns: [], constraints: [], indexes: [], diff --git a/server/src/sql-tools/processors/trigger.processor.ts b/server/src/sql-tools/processors/trigger.processor.ts index 10b07a466a..b50b42cc49 100644 --- a/server/src/sql-tools/processors/trigger.processor.ts +++ b/server/src/sql-tools/processors/trigger.processor.ts @@ -1,5 +1,3 @@ -import { TriggerOptions } from 'src/sql-tools/decorators/trigger.decorator'; -import { asKey } from 'src/sql-tools/helpers'; import { Processor } from 'src/sql-tools/types'; export const processTriggers: Processor = (ctx, items) => { @@ -12,8 +10,19 @@ export const processTriggers: Processor = (ctx, items) => { 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: options.name || asTriggerName(table.name, options), + name: triggerName, tableName: table.name, timing: options.timing, actions: options.actions, @@ -26,6 +35,3 @@ export const processTriggers: Processor = (ctx, items) => { }); } }; - -const asTriggerName = (table: string, trigger: TriggerOptions) => - asKey('TR_', table, [...trigger.actions, trigger.scope, trigger.timing, trigger.functionName]); diff --git a/server/src/sql-tools/processors/unique-constraint.processor.ts b/server/src/sql-tools/processors/unique-constraint.processor.ts index 4d962a0a04..0cbfc26a70 100644 --- a/server/src/sql-tools/processors/unique-constraint.processor.ts +++ b/server/src/sql-tools/processors/unique-constraint.processor.ts @@ -1,4 +1,3 @@ -import { asKey } from 'src/sql-tools/helpers'; import { ConstraintType, Processor } from 'src/sql-tools/types'; export const processUniqueConstraints: Processor = (ctx, items) => { @@ -16,7 +15,7 @@ export const processUniqueConstraints: Processor = (ctx, items) => { table.constraints.push({ type: ConstraintType.UNIQUE, - name: options.name || asUniqueConstraintName(tableName, columnNames), + name: options.name || ctx.getNameFor({ type: 'unique', tableName, columnNames }), tableName, columnNames, synchronize: options.synchronize ?? true, @@ -41,9 +40,17 @@ export const processUniqueConstraints: Processor = (ctx, items) => { } 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: options.uniqueConstraintName || asUniqueConstraintName(table.name, [column.name]), + name: uniqueConstraintName, tableName: table.name, columnNames: [column.name], synchronize: options.synchronize ?? true, @@ -51,5 +58,3 @@ export const processUniqueConstraints: Processor = (ctx, items) => { } } }; - -const asUniqueConstraintName = (table: string, columns: string[]) => asKey('UQ_', table, columns); diff --git a/server/src/sql-tools/public_api.ts b/server/src/sql-tools/public_api.ts index aaef55dd8d..9e7983383e 100644 --- a/server/src/sql-tools/public_api.ts +++ b/server/src/sql-tools/public_api.ts @@ -1,4 +1,28 @@ -export * from 'src/sql-tools/decorators'; +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'; diff --git a/server/src/sql-tools/readers/column.reader.ts b/server/src/sql-tools/readers/column.reader.ts index dfd2878185..249bd77f2c 100644 --- a/server/src/sql-tools/readers/column.reader.ts +++ b/server/src/sql-tools/readers/column.reader.ts @@ -76,6 +76,8 @@ export const readColumns: Reader = async (ctx, db) => { 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', diff --git a/server/src/sql-tools/schema-diff.spec.ts b/server/src/sql-tools/schema-diff.spec.ts index 0667d22d6b..fe249b4e29 100644 --- a/server/src/sql-tools/schema-diff.spec.ts +++ b/server/src/sql-tools/schema-diff.spec.ts @@ -28,6 +28,7 @@ const fromColumn = (column: Partial>): Databas columns: [ { name: 'column1', + primary: false, synchronize: true, isArray: false, type: 'character varying', @@ -63,6 +64,7 @@ const fromConstraint = (constraint?: DatabaseConstraint): DatabaseSchema => { columns: [ { name: 'column1', + primary: false, synchronize: true, isArray: false, type: 'character varying', @@ -97,6 +99,7 @@ const fromIndex = (index?: DatabaseIndex): DatabaseSchema => { columns: [ { name: 'column1', + primary: false, synchronize: true, isArray: false, type: 'character varying', @@ -140,6 +143,7 @@ const newSchema = (schema: { columns.push({ tableName, name: columnName, + primary: false, type: column.type || 'character varying', isArray: column.isArray ?? false, nullable: column.nullable ?? false, @@ -182,6 +186,7 @@ describe(schemaDiff.name, () => { const column: DatabaseColumn = { type: 'character varying', tableName: 'table1', + primary: false, name: 'column1', isArray: false, nullable: false, @@ -264,6 +269,7 @@ describe(schemaDiff.name, () => { column: { tableName: 'table1', isArray: false, + primary: false, name: 'column2', nullable: false, type: 'character varying', diff --git a/server/src/sql-tools/schema-diff.ts b/server/src/sql-tools/schema-diff.ts index 24091a0256..bca58c3228 100644 --- a/server/src/sql-tools/schema-diff.ts +++ b/server/src/sql-tools/schema-diff.ts @@ -30,25 +30,35 @@ export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, optio type SchemaName = SchemaDiff['type']; const itemMap: Record = { - EnumCreate: [], - EnumDrop: [], - ExtensionCreate: [], + ColumnRename: [], + ConstraintRename: [], + IndexRename: [], + ExtensionDrop: [], - FunctionCreate: [], - FunctionDrop: [], - TableCreate: [], - TableDrop: [], - ColumnAdd: [], - ColumnAlter: [], - ColumnDrop: [], - ConstraintAdd: [], - ConstraintDrop: [], - IndexCreate: [], - IndexDrop: [], - TriggerCreate: [], - TriggerDrop: [], + ExtensionCreate: [], + ParameterSet: [], ParameterReset: [], + + FunctionDrop: [], + FunctionCreate: [], + + EnumDrop: [], + EnumCreate: [], + + TriggerDrop: [], + ConstraintDrop: [], + TableDrop: [], + ColumnDrop: [], + ColumnAdd: [], + ColumnAlter: [], + TableCreate: [], + ConstraintAdd: [], + TriggerCreate: [], + + IndexCreate: [], + IndexDrop: [], + OverrideCreate: [], OverrideUpdate: [], OverrideDrop: [], @@ -72,11 +82,14 @@ export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, optio ...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, diff --git a/server/src/sql-tools/schema-from-code.ts b/server/src/sql-tools/schema-from-code.ts index 6b4ab93b8d..2e19f414e4 100644 --- a/server/src/sql-tools/schema-from-code.ts +++ b/server/src/sql-tools/schema-from-code.ts @@ -20,9 +20,9 @@ export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => { name: ctx.overrideTableName, columns: [ { - primary: true, name: 'name', tableName: ctx.overrideTableName, + primary: true, type: 'character varying', nullable: false, isArray: false, @@ -31,6 +31,7 @@ export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => { { name: 'value', tableName: ctx.overrideTableName, + primary: false, type: 'jsonb', nullable: false, isArray: false, diff --git a/server/src/sql-tools/transformers/column.transformer.spec.ts b/server/src/sql-tools/transformers/column.transformer.spec.ts index 1386b08c93..1e29d4bff6 100644 --- a/server/src/sql-tools/transformers/column.transformer.spec.ts +++ b/server/src/sql-tools/transformers/column.transformer.spec.ts @@ -13,6 +13,7 @@ describe(transformColumns.name, () => { column: { name: 'column1', tableName: 'table1', + primary: false, type: 'character varying', nullable: false, isArray: false, @@ -30,6 +31,7 @@ describe(transformColumns.name, () => { column: { name: 'column1', tableName: 'table1', + primary: false, type: 'character varying', nullable: true, isArray: false, @@ -47,6 +49,7 @@ describe(transformColumns.name, () => { column: { name: 'column1', tableName: 'table1', + primary: false, type: 'character varying', enumName: 'table1_column1_enum', nullable: true, @@ -65,6 +68,7 @@ describe(transformColumns.name, () => { column: { name: 'column1', tableName: 'table1', + primary: false, type: 'boolean', nullable: true, isArray: true, diff --git a/server/src/sql-tools/transformers/column.transformer.ts b/server/src/sql-tools/transformers/column.transformer.ts index 43188c24c2..ffa565e533 100644 --- a/server/src/sql-tools/transformers/column.transformer.ts +++ b/server/src/sql-tools/transformers/column.transformer.ts @@ -12,8 +12,12 @@ export const transformColumns: SqlTransformer = (ctx, item) => { 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 asColumnDrop(item.tableName, item.columnName); + return `ALTER TABLE "${item.tableName}" DROP COLUMN "${item.columnName}";`; } default: { @@ -28,10 +32,6 @@ const asColumnAdd = (column: DatabaseColumn): string => { ); }; -const asColumnDrop = (tableName: string, columnName: string): string => { - return `ALTER TABLE "${tableName}" DROP COLUMN "${columnName}";`; -}; - export const asColumnAlter = (tableName: string, columnName: string, changes: ColumnChanges): string[] => { const base = `ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}"`; const items: string[] = []; diff --git a/server/src/sql-tools/transformers/constraint.transformer.ts b/server/src/sql-tools/transformers/constraint.transformer.ts index bb8b30e7ce..94421e56fa 100644 --- a/server/src/sql-tools/transformers/constraint.transformer.ts +++ b/server/src/sql-tools/transformers/constraint.transformer.ts @@ -5,11 +5,15 @@ import { ActionType, ConstraintType, DatabaseConstraint } from 'src/sql-tools/ty export const transformConstraints: SqlTransformer = (ctx, item) => { switch (item.type) { case 'ConstraintAdd': { - return asConstraintAdd(item.constraint); + 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 asConstraintDrop(item.tableName, item.constraintName); + return `ALTER TABLE "${item.tableName}" DROP CONSTRAINT "${item.constraintName}";`; } default: { return false; @@ -52,11 +56,3 @@ export const asConstraintBody = (constraint: DatabaseConstraint): string => { } } }; - -export const asConstraintAdd = (constraint: DatabaseConstraint): string | string[] => { - return `ALTER TABLE "${constraint.tableName}" ADD ${asConstraintBody(constraint)};`; -}; - -export const asConstraintDrop = (tableName: string, constraintName: string): string => { - return `ALTER TABLE "${tableName}" DROP CONSTRAINT "${constraintName}";`; -}; diff --git a/server/src/sql-tools/transformers/index.transformer.ts b/server/src/sql-tools/transformers/index.transformer.ts index bfdbf8e157..acd65140ee 100644 --- a/server/src/sql-tools/transformers/index.transformer.ts +++ b/server/src/sql-tools/transformers/index.transformer.ts @@ -8,8 +8,12 @@ export const transformIndexes: SqlTransformer = (ctx, item) => { return asIndexCreate(item.index); } + case 'IndexRename': { + return `ALTER INDEX "${item.oldName}" RENAME TO "${item.newName}";`; + } + case 'IndexDrop': { - return asIndexDrop(item.indexName); + return `DROP INDEX "${item.indexName}";`; } default: { @@ -50,7 +54,3 @@ export const asIndexCreate = (index: DatabaseIndex): string => { return sql + ';'; }; - -export const asIndexDrop = (indexName: string): string => { - return `DROP INDEX "${indexName}";`; -}; diff --git a/server/src/sql-tools/transformers/table.transformer.spec.ts b/server/src/sql-tools/transformers/table.transformer.spec.ts index 662e1ba7fd..0d89fcd278 100644 --- a/server/src/sql-tools/transformers/table.transformer.spec.ts +++ b/server/src/sql-tools/transformers/table.transformer.spec.ts @@ -19,6 +19,7 @@ const table1: DatabaseTable = { }, { name: 'column2', + primary: false, tableName: 'table1', type: 'character varying', nullable: true, @@ -106,6 +107,7 @@ describe(transformTables.name, () => { columns: [ { tableName: 'table1', + primary: false, name: 'column1', type: 'character varying', isArray: false, @@ -137,6 +139,7 @@ describe(transformTables.name, () => { { tableName: 'table1', name: 'column1', + primary: false, type: 'character varying', isArray: false, nullable: true, @@ -167,6 +170,7 @@ describe(transformTables.name, () => { columns: [ { tableName: 'table1', + primary: false, name: 'column1', type: 'character varying', length: 2, @@ -198,6 +202,7 @@ describe(transformTables.name, () => { columns: [ { tableName: 'table1', + primary: false, name: 'column1', type: 'character varying', isArray: true, diff --git a/server/src/sql-tools/types.ts b/server/src/sql-tools/types.ts index 617adc0a58..9529067040 100644 --- a/server/src/sql-tools/types.ts +++ b/server/src/sql-tools/types.ts @@ -1,12 +1,14 @@ 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 & { @@ -386,7 +388,7 @@ export type DatabaseConstraint = | DatabaseCheckConstraint; export type DatabaseColumn = { - primary?: boolean; + primary: boolean; name: string; tableName: string; comment?: string; @@ -487,11 +489,14 @@ export type SchemaDiff = { reason: 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 } @@ -509,11 +514,15 @@ 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; diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 3d6f7b12a5..f23a3deb35 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -153,128 +153,128 @@ export function toJson(qb: SelectQueryBuilder) { - return qb.where('assets.visibility', 'in', [sql.lit(AssetVisibility.ARCHIVE), sql.lit(AssetVisibility.TIMELINE)]); +export function withDefaultVisibility(qb: SelectQueryBuilder) { + return qb.where('asset.visibility', 'in', [sql.lit(AssetVisibility.ARCHIVE), sql.lit(AssetVisibility.TIMELINE)]); } // TODO come up with a better query that only selects the fields we need -export function withExif(qb: SelectQueryBuilder) { +export function withExif(qb: SelectQueryBuilder) { return qb - .leftJoin('exif', 'assets.id', 'exif.assetId') - .select((eb) => eb.fn.toJson(eb.table('exif')).$castTo().as('exifInfo')); + .leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .select((eb) => eb.fn.toJson(eb.table('asset_exif')).$castTo().as('exifInfo')); } -export function withExifInner(qb: SelectQueryBuilder) { +export function withExifInner(qb: SelectQueryBuilder) { return qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .select((eb) => eb.fn.toJson(eb.table('exif')).$castTo().as('exifInfo')); + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .select((eb) => eb.fn.toJson(eb.table('asset_exif')).$castTo().as('exifInfo')); } -export function withSmartSearch(qb: SelectQueryBuilder) { +export function withSmartSearch(qb: SelectQueryBuilder) { return qb - .leftJoin('smart_search', 'assets.id', 'smart_search.assetId') + .leftJoin('smart_search', 'asset.id', 'smart_search.assetId') .select((eb) => toJson(eb, 'smart_search').as('smartSearch')); } -export function withFaces(eb: ExpressionBuilder, withDeletedFace?: boolean) { +export function withFaces(eb: ExpressionBuilder, withDeletedFace?: boolean) { return jsonArrayFrom( eb - .selectFrom('asset_faces') - .selectAll('asset_faces') - .whereRef('asset_faces.assetId', '=', 'assets.id') - .$if(!withDeletedFace, (qb) => qb.where('asset_faces.deletedAt', 'is', null)), + .selectFrom('asset_face') + .selectAll('asset_face') + .whereRef('asset_face.assetId', '=', 'asset.id') + .$if(!withDeletedFace, (qb) => qb.where('asset_face.deletedAt', 'is', null)), ).as('faces'); } -export function withFiles(eb: ExpressionBuilder, type?: AssetFileType) { +export function withFiles(eb: ExpressionBuilder, type?: AssetFileType) { return jsonArrayFrom( eb - .selectFrom('asset_files') + .selectFrom('asset_file') .select(columns.assetFiles) - .whereRef('asset_files.assetId', '=', 'assets.id') - .$if(!!type, (qb) => qb.where('asset_files.type', '=', type!)), + .whereRef('asset_file.assetId', '=', 'asset.id') + .$if(!!type, (qb) => qb.where('asset_file.type', '=', type!)), ).as('files'); } -export function withFacesAndPeople(eb: ExpressionBuilder, withDeletedFace?: boolean) { +export function withFacesAndPeople(eb: ExpressionBuilder, withDeletedFace?: boolean) { return jsonArrayFrom( eb - .selectFrom('asset_faces') + .selectFrom('asset_face') .leftJoinLateral( (eb) => - eb.selectFrom('person').selectAll('person').whereRef('asset_faces.personId', '=', 'person.id').as('person'), + eb.selectFrom('person').selectAll('person').whereRef('asset_face.personId', '=', 'person.id').as('person'), (join) => join.onTrue(), ) - .selectAll('asset_faces') + .selectAll('asset_face') .select((eb) => eb.table('person').$castTo().as('person')) - .whereRef('asset_faces.assetId', '=', 'assets.id') - .$if(!withDeletedFace, (qb) => qb.where('asset_faces.deletedAt', 'is', null)), + .whereRef('asset_face.assetId', '=', 'asset.id') + .$if(!withDeletedFace, (qb) => qb.where('asset_face.deletedAt', 'is', null)), ).as('faces'); } -export function hasPeople(qb: SelectQueryBuilder, personIds: string[]) { +export function hasPeople(qb: SelectQueryBuilder, personIds: string[]) { return qb.innerJoin( (eb) => eb - .selectFrom('asset_faces') + .selectFrom('asset_face') .select('assetId') .where('personId', '=', anyUuid(personIds!)) .where('deletedAt', 'is', null) .groupBy('assetId') .having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length) .as('has_people'), - (join) => join.onRef('has_people.assetId', '=', 'assets.id'), + (join) => join.onRef('has_people.assetId', '=', 'asset.id'), ); } -export function inAlbums(qb: SelectQueryBuilder, albumIds: string[]) { +export function inAlbums(qb: SelectQueryBuilder, albumIds: string[]) { return qb.innerJoin( (eb) => eb - .selectFrom('albums_assets_assets') + .selectFrom('album_asset') .select('assetsId') .where('albumsId', '=', anyUuid(albumIds!)) .groupBy('assetsId') .having((eb) => eb.fn.count('albumsId').distinct(), '=', albumIds.length) .as('has_album'), - (join) => join.onRef('has_album.assetsId', '=', 'assets.id'), + (join) => join.onRef('has_album.assetsId', '=', 'asset.id'), ); } -export function hasTags(qb: SelectQueryBuilder, tagIds: string[]) { +export function hasTags(qb: SelectQueryBuilder, tagIds: string[]) { return qb.innerJoin( (eb) => eb .selectFrom('tag_asset') .select('assetsId') - .innerJoin('tags_closure', 'tag_asset.tagsId', 'tags_closure.id_descendant') - .where('tags_closure.id_ancestor', '=', anyUuid(tagIds)) + .innerJoin('tag_closure', 'tag_asset.tagsId', 'tag_closure.id_descendant') + .where('tag_closure.id_ancestor', '=', anyUuid(tagIds)) .groupBy('assetsId') - .having((eb) => eb.fn.count('tags_closure.id_ancestor').distinct(), '>=', tagIds.length) + .having((eb) => eb.fn.count('tag_closure.id_ancestor').distinct(), '>=', tagIds.length) .as('has_tags'), - (join) => join.onRef('has_tags.assetsId', '=', 'assets.id'), + (join) => join.onRef('has_tags.assetsId', '=', 'asset.id'), ); } -export function withOwner(eb: ExpressionBuilder) { - return jsonObjectFrom(eb.selectFrom('users').select(columns.user).whereRef('users.id', '=', 'assets.ownerId')).as( +export function withOwner(eb: ExpressionBuilder) { + return jsonObjectFrom(eb.selectFrom('user').select(columns.user).whereRef('user.id', '=', 'asset.ownerId')).as( 'owner', ); } -export function withLibrary(eb: ExpressionBuilder) { +export function withLibrary(eb: ExpressionBuilder) { return jsonObjectFrom( - eb.selectFrom('libraries').selectAll('libraries').whereRef('libraries.id', '=', 'assets.libraryId'), + eb.selectFrom('library').selectAll('library').whereRef('library.id', '=', 'asset.libraryId'), ).as('library'); } -export function withTags(eb: ExpressionBuilder) { +export function withTags(eb: ExpressionBuilder) { return jsonArrayFrom( eb - .selectFrom('tags') + .selectFrom('tag') .select(columns.tag) - .innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId') - .whereRef('assets.id', '=', 'tag_asset.assetsId'), + .innerJoin('tag_asset', 'tag.id', 'tag_asset.tagsId') + .whereRef('asset.id', '=', 'tag_asset.assetsId'), ).as('tags'); } @@ -282,14 +282,14 @@ export function truncatedDate() { return sql`date_trunc(${sql.lit('MONTH')}, "localDateTime" AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'`; } -export function withTagId(qb: SelectQueryBuilder, tagId: string) { +export function withTagId(qb: SelectQueryBuilder, tagId: string) { return qb.where((eb) => eb.exists( eb - .selectFrom('tags_closure') - .innerJoin('tag_asset', 'tag_asset.tagsId', 'tags_closure.id_descendant') - .whereRef('tag_asset.assetsId', '=', 'assets.id') - .where('tags_closure.id_ancestor', '=', tagId), + .selectFrom('tag_closure') + .innerJoin('tag_asset', 'tag_asset.tagsId', 'tag_closure.id_descendant') + .whereRef('tag_asset.assetsId', '=', 'asset.id') + .where('tag_closure.id_ancestor', '=', tagId), ), ); } @@ -303,96 +303,94 @@ export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuild return kysely .withPlugin(joinDeduplicationPlugin) - .selectFrom('assets') - .where('assets.visibility', '=', visibility) + .selectFrom('asset') + .where('asset.visibility', '=', visibility) .$if(!!options.albumIds && options.albumIds.length > 0, (qb) => inAlbums(qb, options.albumIds!)) .$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!)) .$if(options.tagIds === null, (qb) => - qb.where((eb) => eb.not(eb.exists((eb) => eb.selectFrom('tag_asset').whereRef('assetsId', '=', 'assets.id')))), + qb.where((eb) => eb.not(eb.exists((eb) => eb.selectFrom('tag_asset').whereRef('assetsId', '=', 'asset.id')))), ) .$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!)) - .$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!)) - .$if(!!options.createdAfter, (qb) => qb.where('assets.createdAt', '>=', options.createdAfter!)) - .$if(!!options.updatedBefore, (qb) => qb.where('assets.updatedAt', '<=', options.updatedBefore!)) - .$if(!!options.updatedAfter, (qb) => qb.where('assets.updatedAt', '>=', options.updatedAfter!)) - .$if(!!options.trashedBefore, (qb) => qb.where('assets.deletedAt', '<=', options.trashedBefore!)) - .$if(!!options.trashedAfter, (qb) => qb.where('assets.deletedAt', '>=', options.trashedAfter!)) - .$if(!!options.takenBefore, (qb) => qb.where('assets.fileCreatedAt', '<=', options.takenBefore!)) - .$if(!!options.takenAfter, (qb) => qb.where('assets.fileCreatedAt', '>=', options.takenAfter!)) + .$if(!!options.createdBefore, (qb) => qb.where('asset.createdAt', '<=', options.createdBefore!)) + .$if(!!options.createdAfter, (qb) => qb.where('asset.createdAt', '>=', options.createdAfter!)) + .$if(!!options.updatedBefore, (qb) => qb.where('asset.updatedAt', '<=', options.updatedBefore!)) + .$if(!!options.updatedAfter, (qb) => qb.where('asset.updatedAt', '>=', options.updatedAfter!)) + .$if(!!options.trashedBefore, (qb) => qb.where('asset.deletedAt', '<=', options.trashedBefore!)) + .$if(!!options.trashedAfter, (qb) => qb.where('asset.deletedAt', '>=', options.trashedAfter!)) + .$if(!!options.takenBefore, (qb) => qb.where('asset.fileCreatedAt', '<=', options.takenBefore!)) + .$if(!!options.takenAfter, (qb) => qb.where('asset.fileCreatedAt', '>=', options.takenAfter!)) .$if(options.city !== undefined, (qb) => qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .where('exif.city', options.city === null ? 'is' : '=', options.city!), + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .where('asset_exif.city', options.city === null ? 'is' : '=', options.city!), ) .$if(options.state !== undefined, (qb) => qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .where('exif.state', options.state === null ? 'is' : '=', options.state!), + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .where('asset_exif.state', options.state === null ? 'is' : '=', options.state!), ) .$if(options.country !== undefined, (qb) => qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .where('exif.country', options.country === null ? 'is' : '=', options.country!), + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .where('asset_exif.country', options.country === null ? 'is' : '=', options.country!), ) .$if(options.make !== undefined, (qb) => qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .where('exif.make', options.make === null ? 'is' : '=', options.make!), + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .where('asset_exif.make', options.make === null ? 'is' : '=', options.make!), ) .$if(options.model !== undefined, (qb) => qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .where('exif.model', options.model === null ? 'is' : '=', options.model!), + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .where('asset_exif.model', options.model === null ? 'is' : '=', options.model!), ) .$if(options.lensModel !== undefined, (qb) => qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .where('exif.lensModel', options.lensModel === null ? 'is' : '=', options.lensModel!), + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .where('asset_exif.lensModel', options.lensModel === null ? 'is' : '=', options.lensModel!), ) .$if(options.rating !== undefined, (qb) => qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .where('exif.rating', options.rating === null ? 'is' : '=', options.rating!), + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .where('asset_exif.rating', options.rating === null ? 'is' : '=', options.rating!), ) - .$if(!!options.checksum, (qb) => qb.where('assets.checksum', '=', options.checksum!)) - .$if(!!options.deviceAssetId, (qb) => qb.where('assets.deviceAssetId', '=', options.deviceAssetId!)) - .$if(!!options.deviceId, (qb) => qb.where('assets.deviceId', '=', options.deviceId!)) - .$if(!!options.id, (qb) => qb.where('assets.id', '=', asUuid(options.id!))) - .$if(!!options.libraryId, (qb) => qb.where('assets.libraryId', '=', asUuid(options.libraryId!))) - .$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!))) - .$if(!!options.encodedVideoPath, (qb) => qb.where('assets.encodedVideoPath', '=', options.encodedVideoPath!)) + .$if(!!options.checksum, (qb) => qb.where('asset.checksum', '=', options.checksum!)) + .$if(!!options.deviceAssetId, (qb) => qb.where('asset.deviceAssetId', '=', options.deviceAssetId!)) + .$if(!!options.deviceId, (qb) => qb.where('asset.deviceId', '=', options.deviceId!)) + .$if(!!options.id, (qb) => qb.where('asset.id', '=', asUuid(options.id!))) + .$if(!!options.libraryId, (qb) => qb.where('asset.libraryId', '=', asUuid(options.libraryId!))) + .$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!))) + .$if(!!options.encodedVideoPath, (qb) => qb.where('asset.encodedVideoPath', '=', options.encodedVideoPath!)) .$if(!!options.originalPath, (qb) => - qb.where(sql`f_unaccent(assets."originalPath")`, 'ilike', sql`'%' || f_unaccent(${options.originalPath}) || '%'`), + qb.where(sql`f_unaccent(asset."originalPath")`, 'ilike', sql`'%' || f_unaccent(${options.originalPath}) || '%'`), ) .$if(!!options.originalFileName, (qb) => qb.where( - sql`f_unaccent(assets."originalFileName")`, + sql`f_unaccent(asset."originalFileName")`, 'ilike', sql`'%' || f_unaccent(${options.originalFileName}) || '%'`, ), ) .$if(!!options.description, (qb) => qb - .innerJoin('exif', 'assets.id', 'exif.assetId') - .where(sql`f_unaccent(exif.description)`, 'ilike', sql`'%' || f_unaccent(${options.description}) || '%'`), + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .where(sql`f_unaccent(asset_exif.description)`, 'ilike', sql`'%' || f_unaccent(${options.description}) || '%'`), ) - .$if(!!options.type, (qb) => qb.where('assets.type', '=', options.type!)) - .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) - .$if(options.isOffline !== undefined, (qb) => qb.where('assets.isOffline', '=', options.isOffline!)) + .$if(!!options.type, (qb) => qb.where('asset.type', '=', options.type!)) + .$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!)) + .$if(options.isOffline !== undefined, (qb) => qb.where('asset.isOffline', '=', options.isOffline!)) .$if(options.isEncoded !== undefined, (qb) => - qb.where('assets.encodedVideoPath', options.isEncoded ? 'is not' : 'is', null), + qb.where('asset.encodedVideoPath', options.isEncoded ? 'is not' : 'is', null), ) .$if(options.isMotion !== undefined, (qb) => - qb.where('assets.livePhotoVideoId', options.isMotion ? 'is not' : 'is', null), + qb.where('asset.livePhotoVideoId', options.isMotion ? 'is not' : 'is', null), ) .$if(!!options.isNotInAlbum && (!options.albumIds || options.albumIds.length === 0), (qb) => - qb.where((eb) => - eb.not(eb.exists((eb) => eb.selectFrom('albums_assets_assets').whereRef('assetsId', '=', 'assets.id'))), - ), + qb.where((eb) => eb.not(eb.exists((eb) => eb.selectFrom('album_asset').whereRef('assetsId', '=', 'asset.id')))), ) .$if(!!options.withExif, withExifInner) .$if(!!(options.withFaces || options.withPeople || options.personIds), (qb) => qb.select(withFacesAndPeople)) - .$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null)); + .$if(!options.withDeleted, (qb) => qb.where('asset.deletedAt', 'is', null)); } export type ReindexVectorIndexOptions = { indexName: string; lists?: number }; diff --git a/server/src/workers/api.ts b/server/src/workers/api.ts index ce1520c475..f56adf3b68 100644 --- a/server/src/workers/api.ts +++ b/server/src/workers/api.ts @@ -7,7 +7,6 @@ import { existsSync } from 'node:fs'; import sirv from 'sirv'; import { ApiModule } from 'src/app.module'; import { excludePaths, serverVersion } from 'src/constants'; -import { ImmichEnvironment } from 'src/enum'; import { WebSocketAdapter } from 'src/middleware/websocket.adapter'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -27,7 +26,6 @@ async function bootstrap() { const configRepository = app.get(ConfigRepository); const { environment, host, port, resourcePaths } = configRepository.getEnv(); - const isDev = environment === ImmichEnvironment.DEVELOPMENT; logger.setContext('Bootstrap'); app.useLogger(logger); @@ -35,11 +33,11 @@ async function bootstrap() { app.set('etag', 'strong'); app.use(cookieParser()); app.use(json({ limit: '10mb' })); - if (isDev) { + if (configRepository.isDev()) { app.enableCors(); } app.useWebSocketAdapter(new WebSocketAdapter(app)); - useSwagger(app, { write: isDev }); + useSwagger(app, { write: configRepository.isDev() }); app.setGlobalPrefix('api', { exclude: excludePaths }); if (existsSync(resourcePaths.web.root)) { diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index 44182602cd..9c1032663f 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -34,9 +34,9 @@ import { UserRepository } from 'src/repositories/user.repository'; import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { DB } from 'src/schema'; import { AlbumTable } from 'src/schema/tables/album.table'; +import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { ExifTable } from 'src/schema/tables/exif.table'; import { FaceSearchTable } from 'src/schema/tables/face-search.table'; import { MemoryTable } from 'src/schema/tables/memory.table'; import { PersonTable } from 'src/schema/tables/person.table'; @@ -165,7 +165,7 @@ export class MediumTestContext { return { memoryAsset: dto, result }; } - async newExif(dto: Insertable) { + async newExif(dto: Insertable) { const result = await this.get(AssetRepository).upsertExif(dto); return { result }; } @@ -234,11 +234,11 @@ export class SyncTestContext extends MediumTestContext { }); } - async syncStream(auth: AuthDto, types: SyncRequestType[]) { + async syncStream(auth: AuthDto, types: SyncRequestType[], reset?: boolean) { const stream = mediumFactory.syncStream(); // Wait for 2ms to ensure all updates are available and account for setTimeout inaccuracy await wait(2); - await this.sut.stream(auth, stream, { types }); + await this.sut.stream(auth, stream, { types, reset }); return stream.getResponse(); } @@ -481,6 +481,7 @@ const sessionInsert = ({ const defaults: Insertable = { id, userId, + isPendingSyncReset: false, token: sha256(id), }; diff --git a/server/test/medium/specs/services/audit.database.spec.ts b/server/test/medium/specs/services/audit.database.spec.ts index 8ca68cdb4a..7506fcf2c3 100644 --- a/server/test/medium/specs/services/audit.database.spec.ts +++ b/server/test/medium/specs/services/audit.database.spec.ts @@ -1,7 +1,7 @@ import { LoggingRepository } from 'src/repositories/logging.repository'; import { PartnerRepository } from 'src/repositories/partner.repository'; import { UserRepository } from 'src/repositories/user.repository'; -import { partners_delete_audit, stacks_delete_audit } from 'src/schema/functions'; +import { partner_delete_audit, stack_delete_audit } from 'src/schema/functions'; import { BaseService } from 'src/services/base.service'; import { MediumTestContext } from 'test/medium.factory'; import { getKyselyDB } from 'test/utils'; @@ -17,7 +17,7 @@ describe('audit', () => { }); }); - describe(partners_delete_audit.name, () => { + describe(partner_delete_audit.name, () => { it('should not cascade user deletes to partners_audit', async () => { const partnerRepo = ctx.get(PartnerRepository); const userRepo = ctx.get(UserRepository); @@ -26,12 +26,12 @@ describe('audit', () => { await partnerRepo.create({ sharedById: user1.id, sharedWithId: user2.id }); await userRepo.delete(user1, true); await expect( - ctx.database.selectFrom('partners_audit').select(['id']).where('sharedById', '=', user1.id).execute(), + ctx.database.selectFrom('partner_audit').select(['id']).where('sharedById', '=', user1.id).execute(), ).resolves.toHaveLength(0); }); }); - describe(stacks_delete_audit.name, () => { + describe(stack_delete_audit.name, () => { it('should not cascade user deletes to stacks_audit', async () => { const userRepo = ctx.get(UserRepository); const { user } = await ctx.newUser(); @@ -40,7 +40,7 @@ describe('audit', () => { await ctx.newStack({ ownerId: user.id }, [asset1.id, asset2.id]); await userRepo.delete(user, true); await expect( - ctx.database.selectFrom('stacks_audit').select(['id']).where('userId', '=', user.id).execute(), + ctx.database.selectFrom('stack_audit').select(['id']).where('userId', '=', user.id).execute(), ).resolves.toHaveLength(0); }); }); @@ -52,7 +52,7 @@ describe('audit', () => { const { asset } = await ctx.newAsset({ ownerId: user.id }); await userRepo.delete(user, true); await expect( - ctx.database.selectFrom('assets_audit').select(['id']).where('assetId', '=', asset.id).execute(), + ctx.database.selectFrom('asset_audit').select(['id']).where('assetId', '=', asset.id).execute(), ).resolves.toHaveLength(0); }); }); @@ -64,7 +64,7 @@ describe('audit', () => { await ctx.newExif({ assetId: asset.id, make: 'Canon' }); const before = await ctx.database - .selectFrom('exif') + .selectFrom('asset_exif') .select(['updatedAt', 'updateId']) .where('assetId', '=', asset.id) .executeTakeFirstOrThrow(); @@ -72,7 +72,7 @@ describe('audit', () => { await ctx.newExif({ assetId: asset.id, make: 'Canon 2' }); const after = await ctx.database - .selectFrom('exif') + .selectFrom('asset_exif') .select(['updatedAt', 'updateId']) .where('assetId', '=', asset.id) .executeTakeFirstOrThrow(); diff --git a/server/test/medium/specs/sync/sync-album-asset.spec.ts b/server/test/medium/specs/sync/sync-album-asset.spec.ts index 6bfb6b5d27..41700d29d4 100644 --- a/server/test/medium/specs/sync/sync-album-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-album-asset.spec.ts @@ -36,6 +36,8 @@ describe(SyncRequestType.AlbumAssetsV1, () => { localDateTime: date, deletedAt: null, duration: '0:10:00.00000', + livePhotoVideoId: null, + stackId: null, }); const { album } = await ctx.newAlbum({ ownerId: user2.id }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); @@ -60,6 +62,8 @@ describe(SyncRequestType.AlbumAssetsV1, () => { type: asset.type, visibility: asset.visibility, duration: asset.duration, + livePhotoVideoId: asset.livePhotoVideoId, + stackId: asset.stackId, }, type: SyncEntityType.AlbumAssetV1, }, diff --git a/server/test/medium/specs/sync/sync-asset.spec.ts b/server/test/medium/specs/sync/sync-asset.spec.ts index 8603296028..52d6bcb524 100644 --- a/server/test/medium/specs/sync/sync-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-asset.spec.ts @@ -57,6 +57,8 @@ describe(SyncEntityType.AssetV1, () => { type: asset.type, visibility: asset.visibility, duration: asset.duration, + stackId: null, + livePhotoVideoId: null, }, type: 'AssetV1', }, diff --git a/server/test/medium/specs/sync/sync-partner-asset.spec.ts b/server/test/medium/specs/sync/sync-partner-asset.spec.ts index d9655add57..2daa750bf3 100644 --- a/server/test/medium/specs/sync/sync-partner-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-partner-asset.spec.ts @@ -63,6 +63,8 @@ describe(SyncRequestType.PartnerAssetsV1, () => { type: asset.type, visibility: asset.visibility, duration: asset.duration, + stackId: null, + livePhotoVideoId: null, }, type: SyncEntityType.PartnerAssetV1, }, diff --git a/server/test/medium/specs/sync/sync-person.spec.ts b/server/test/medium/specs/sync/sync-person.spec.ts new file mode 100644 index 0000000000..807e41894c --- /dev/null +++ b/server/test/medium/specs/sync/sync-person.spec.ts @@ -0,0 +1,87 @@ +import { Kysely } from 'kysely'; +import { SyncEntityType, SyncRequestType } from 'src/enum'; +import { PersonRepository } from 'src/repositories/person.repository'; +import { DB } from 'src/schema'; +import { SyncTestContext } from 'test/medium.factory'; +import { factory } from 'test/small.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = async (db?: Kysely) => { + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(SyncEntityType.PersonV1, () => { + it('should detect and sync the first person', async () => { + const { auth, ctx } = await setup(); + const { person } = await ctx.newPerson({ ownerId: auth.user.id }); + + const response = await ctx.syncStream(auth, [SyncRequestType.PeopleV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + id: person.id, + name: person.name, + thumbnailPath: person.thumbnailPath, + isHidden: person.isHidden, + birthDate: person.birthDate, + faceAssetId: person.faceAssetId, + isFavorite: person.isFavorite, + ownerId: auth.user.id, + color: person.color, + }), + type: 'PersonV1', + }, + ]); + + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.PeopleV1])).resolves.toEqual([]); + }); + + it('should detect and sync a deleted person', async () => { + const { auth, ctx } = await setup(); + const personRepo = ctx.get(PersonRepository); + const { person } = await ctx.newPerson({ ownerId: auth.user.id }); + await personRepo.delete([person.id]); + + const response = await ctx.syncStream(auth, [SyncRequestType.PeopleV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + personId: person.id, + }, + type: 'PersonDeleteV1', + }, + ]); + + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.PeopleV1])).resolves.toEqual([]); + }); + + it('should not sync a person or person 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 { person } = await ctx.newPerson({ ownerId: user2.id }); + const auth2 = factory.auth({ session, user: user2 }); + + expect(await ctx.syncStream(auth2, [SyncRequestType.PeopleV1])).toHaveLength(1); + expect(await ctx.syncStream(auth, [SyncRequestType.PeopleV1])).toHaveLength(0); + + await personRepo.delete([person.id]); + expect(await ctx.syncStream(auth2, [SyncRequestType.PeopleV1])).toHaveLength(1); + expect(await ctx.syncStream(auth, [SyncRequestType.PeopleV1])).toHaveLength(0); + }); +}); diff --git a/server/test/medium/specs/sync/sync-reset.spec.ts b/server/test/medium/specs/sync/sync-reset.spec.ts new file mode 100644 index 0000000000..4cfdc8249e --- /dev/null +++ b/server/test/medium/specs/sync/sync-reset.spec.ts @@ -0,0 +1,63 @@ +import { Kysely } from 'kysely'; +import { SyncEntityType, SyncRequestType } from 'src/enum'; +import { DB } from 'src/schema'; +import { SyncTestContext } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = async (db?: Kysely) => { + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(SyncEntityType.SyncResetV1, () => { + it('should work', async () => { + const { auth, ctx } = await setup(); + + const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); + expect(response).toEqual([]); + }); + + it('should detect a pending sync reset', async () => { + const { auth, ctx } = await setup(); + + auth.session!.isPendingSyncReset = true; + + const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); + expect(response).toEqual([{ type: SyncEntityType.SyncResetV1, data: {} }]); + }); + + it('should not send other dtos when a reset is pending', async () => { + const { auth, user, ctx } = await setup(); + + await ctx.newAsset({ ownerId: user.id }); + + await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); + + auth.session!.isPendingSyncReset = true; + + await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([ + { type: SyncEntityType.SyncResetV1, data: {} }, + ]); + }); + + it('should allow resetting a pending reset when requesting changes ', async () => { + const { auth, user, ctx } = await setup(); + + await ctx.newAsset({ ownerId: user.id }); + + auth.session!.isPendingSyncReset = true; + + await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1], true)).resolves.toEqual([ + expect.objectContaining({ + type: SyncEntityType.AssetV1, + }), + ]); + }); +}); diff --git a/server/test/medium/specs/sync/sync-user-metadata.spec.ts b/server/test/medium/specs/sync/sync-user-metadata.spec.ts new file mode 100644 index 0000000000..bb4a500a60 --- /dev/null +++ b/server/test/medium/specs/sync/sync-user-metadata.spec.ts @@ -0,0 +1,123 @@ +import { Kysely } from 'kysely'; +import { SyncEntityType, SyncRequestType, UserMetadataKey } from 'src/enum'; +import { UserRepository } from 'src/repositories/user.repository'; +import { DB } from 'src/schema'; +import { SyncTestContext } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = async (db?: Kysely) => { + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(SyncEntityType.UserMetadataV1, () => { + it('should detect and sync new user metadata', async () => { + const { auth, user, ctx } = await setup(); + + const userRepo = ctx.get(UserRepository); + await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.ONBOARDING, value: { isOnboarded: true } }); + + const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + key: UserMetadataKey.ONBOARDING, + userId: user.id, + value: { isOnboarded: true }, + }, + type: 'UserMetadataV1', + }, + ]); + + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.UserMetadataV1])).resolves.toEqual([]); + }); + + it('should update user metadata', async () => { + const { auth, user, ctx } = await setup(); + + const userRepo = ctx.get(UserRepository); + await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.ONBOARDING, value: { isOnboarded: true } }); + + const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + key: UserMetadataKey.ONBOARDING, + userId: user.id, + value: { isOnboarded: true }, + }, + type: 'UserMetadataV1', + }, + ]); + + await ctx.syncAckAll(auth, response); + + await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.ONBOARDING, value: { isOnboarded: false } }); + + const updatedResponse = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); + expect(updatedResponse).toEqual([ + { + ack: expect.any(String), + data: { + key: UserMetadataKey.ONBOARDING, + userId: user.id, + value: { isOnboarded: false }, + }, + type: 'UserMetadataV1', + }, + ]); + + await ctx.syncAckAll(auth, updatedResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.UserMetadataV1])).resolves.toEqual([]); + }); +}); + +describe(SyncEntityType.UserMetadataDeleteV1, () => { + it('should delete and sync user metadata', async () => { + const { auth, user, ctx } = await setup(); + + const userRepo = ctx.get(UserRepository); + await userRepo.upsertMetadata(user.id, { key: UserMetadataKey.ONBOARDING, value: { isOnboarded: true } }); + + const response = await ctx.syncStream(auth, [SyncRequestType.UserMetadataV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + key: UserMetadataKey.ONBOARDING, + userId: user.id, + value: { isOnboarded: true }, + }, + type: 'UserMetadataV1', + }, + ]); + + await ctx.syncAckAll(auth, response); + + await userRepo.deleteMetadata(auth.user.id, UserMetadataKey.ONBOARDING); + + await expect(ctx.syncStream(auth, [SyncRequestType.UserMetadataV1])).resolves.toEqual([ + { + ack: expect.any(String), + data: { + userId: user.id, + key: UserMetadataKey.ONBOARDING, + }, + type: 'UserMetadataDeleteV1', + }, + ]); + }); +}); diff --git a/server/test/repositories/config.repository.mock.ts b/server/test/repositories/config.repository.mock.ts index a2585e3b9e..460dfeef57 100644 --- a/server/test/repositories/config.repository.mock.ts +++ b/server/test/repositories/config.repository.mock.ts @@ -94,5 +94,6 @@ export const newConfigRepositoryMock = (): Mocked = {}) => ({ expiresAt: null, userId: newUuid(), pinExpiresAt: newDate(), + isPendingSyncReset: false, ...session, }); 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 index c38c29cde0..288f7c6698 100644 --- a/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts +++ b/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts @@ -83,7 +83,7 @@ export const schema: DatabaseSchema = { }, { type: ConstraintType.UNIQUE, - name: 'REL_3fcca5cc563abf256fc346e3ff', + name: 'UQ_3fcca5cc563abf256fc346e3ff4', tableName: 'table2', columnNames: ['parentId'], synchronize: true, diff --git a/server/tsconfig.json b/server/tsconfig.json index 8d8d12c54e..e12b614f0d 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -17,6 +17,9 @@ "skipLibCheck": true, "esModuleInterop": true, "preserveWatchOutput": true, + "paths": { + "src/*": ["./src/*"], + }, "baseUrl": "./", "jsx": "react", "types": ["vitest/globals"], diff --git a/web/package-lock.json b/web/package-lock.json index 6af393b042..6bf70e96da 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -19,6 +19,8 @@ "@photo-sphere-viewer/resolution-plugin": "^5.11.5", "@photo-sphere-viewer/settings-plugin": "^5.11.5", "@photo-sphere-viewer/video-plugin": "^5.11.5", + "@types/geojson": "^7946.0.16", + "@zoom-image/core": "^0.41.0", "@zoom-image/svelte": "^0.3.0", "async-mutex": "^0.5.0", "dom-to-image": "^2.6.0", @@ -50,7 +52,7 @@ "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/enhanced-img": "^0.6.0", "@sveltejs/kit": "^2.15.2", - "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@sveltejs/vite-plugin-svelte": "^6.0.0", "@tailwindcss/vite": "^4.1.7", "@testing-library/jest-dom": "^6.4.2", "@testing-library/svelte": "^5.2.8", @@ -85,7 +87,7 @@ "tslib": "^2.6.2", "typescript": "^5.7.3", "typescript-eslint": "^8.28.0", - "vite": "^6.0.0", + "vite": "^7.0.0", "vitest": "^3.0.0" } }, @@ -2216,43 +2218,43 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.0.tgz", - "integrity": "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.0.0.tgz", + "integrity": "sha512-mma5GJ23pYiWpTNbN//g9XI3Hfob3aAlXPP42qRtvjgTAU6pfJyLyNPTdLjFuj+jfC9JslP4J3AkeiJNhjtLLA==", "dev": true, "license": "MIT", "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0-next.1", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", - "vitefu": "^1.0.6" + "vitefu": "^1.1.1" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" + "node": "^20.19 || ^22.12 || >=24" }, "peerDependencies": { "svelte": "^5.0.0", - "vite": "^6.0.0" + "vite": "^6.3.0 || ^7.0.0" } }, "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", - "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.0.tgz", + "integrity": "sha512-iwQ8Z4ET6ZFSt/gC+tVfcsSBHwsqc6RumSaiLUkAurW3BCpJam65cmHw0oOlDMTO0u+PZi9hilBRYN+LZNHTUQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.7" + "debug": "^4.4.1" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" + "node": "^20.19 || ^22.12 || >=24" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", - "vite": "^6.0.0" + "vite": "^6.3.0 || ^7.0.0" } }, "node_modules/@swc/helpers": { @@ -5302,9 +5304,9 @@ } }, "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -7599,9 +7601,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -7619,7 +7621,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -9474,24 +9476,24 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz", + "integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.4", + "fdir": "^6.4.6", "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -9500,14 +9502,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", - "less": "*", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -10019,17 +10021,18 @@ } }, "node_modules/vitefu": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz", - "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", "dev": true, "license": "MIT", "workspaces": [ "tests/deps/*", - "tests/projects/*" + "tests/projects/*", + "tests/projects/workspace/packages/*" ], "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "peerDependenciesMeta": { "vite": { diff --git a/web/package.json b/web/package.json index 2ad2a8361a..019f651305 100644 --- a/web/package.json +++ b/web/package.json @@ -36,6 +36,8 @@ "@photo-sphere-viewer/resolution-plugin": "^5.11.5", "@photo-sphere-viewer/settings-plugin": "^5.11.5", "@photo-sphere-viewer/video-plugin": "^5.11.5", + "@types/geojson": "^7946.0.16", + "@zoom-image/core": "^0.41.0", "@zoom-image/svelte": "^0.3.0", "async-mutex": "^0.5.0", "dom-to-image": "^2.6.0", @@ -67,7 +69,7 @@ "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/enhanced-img": "^0.6.0", "@sveltejs/kit": "^2.15.2", - "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@sveltejs/vite-plugin-svelte": "^6.0.0", "@tailwindcss/vite": "^4.1.7", "@testing-library/jest-dom": "^6.4.2", "@testing-library/svelte": "^5.2.8", @@ -102,7 +104,7 @@ "tslib": "^2.6.2", "typescript": "^5.7.3", "typescript-eslint": "^8.28.0", - "vite": "^6.0.0", + "vite": "^7.0.0", "vitest": "^3.0.0" }, "volta": { diff --git a/web/src/lib/components/admin-page/settings/nightly-tasks-settings/nightly-tasks-settings.svelte b/web/src/lib/components/admin-page/settings/nightly-tasks-settings/nightly-tasks-settings.svelte new file mode 100644 index 0000000000..af653f2473 --- /dev/null +++ b/web/src/lib/components/admin-page/settings/nightly-tasks-settings/nightly-tasks-settings.svelte @@ -0,0 +1,81 @@ + + +
+
+
+
+ + + + + + +
+ + onReset({ ...options, configKeys: ['nightlyTasks'] })} + onSave={() => onSave({ nightlyTasks: config.nightlyTasks })} + showResetToDefault={!isEqual(savedConfig.nightlyTasks, defaultConfig.nightlyTasks)} + {disabled} + /> + +
+
diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index e053eea179..3b1d68a49e 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -12,6 +12,7 @@ import MapSettings from '$lib/components/admin-page/settings/map-settings/map-settings.svelte'; import MetadataSettings from '$lib/components/admin-page/settings/metadata-settings/metadata-settings.svelte'; import NewVersionCheckSettings from '$lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte'; + import NightlyTasksSettings from '$lib/components/admin-page/settings/nightly-tasks-settings/nightly-tasks-settings.svelte'; import NotificationSettings from '$lib/components/admin-page/settings/notification-settings/notification-settings.svelte'; import ServerSettings from '$lib/components/admin-page/settings/server/server-settings.svelte'; import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte'; @@ -33,6 +34,7 @@ mdiBackupRestore, mdiBellOutline, mdiBookshelf, + mdiClockOutline, mdiContentCopy, mdiDatabaseOutline, mdiDownload, @@ -136,13 +138,6 @@ key: 'job', icon: mdiSync, }, - { - component: MetadataSettings, - title: $t('admin.metadata_settings'), - subtitle: $t('admin.metadata_settings_description'), - key: 'metadata', - icon: mdiDatabaseOutline, - }, { component: LibrarySettings, title: $t('admin.library_settings'), @@ -171,6 +166,20 @@ key: 'location', icon: mdiMapMarkerOutline, }, + { + component: MetadataSettings, + title: $t('admin.metadata_settings'), + subtitle: $t('admin.metadata_settings_description'), + key: 'metadata', + icon: mdiDatabaseOutline, + }, + { + component: NightlyTasksSettings, + title: $t('admin.nightly_tasks_settings'), + subtitle: $t('admin.nightly_tasks_settings_description'), + key: 'nightly-tasks', + icon: mdiClockOutline, + }, { component: NotificationSettings, title: $t('admin.notification_settings'),