Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61c4b27b94 | |||
| a5760129f0 | |||
| d430b869ac | |||
| 4179c8a17d | |||
| 0a9cbf01d2 | |||
| 9567a2a560 | |||
| 58dd6f094c | |||
| 02381343ff | |||
| 09a5963eee | |||
| a573a23c83 | |||
| 7118dca559 | |||
| 13d43e193e | |||
| 7a7843467c | |||
| 9e6fee4064 | |||
| 9680f1290d | |||
| ce2ea98926 | |||
| 5c76cc34e1 | |||
| eb2f4c866e | |||
| 2a370087e8 | |||
| 272c8a5812 | |||
| 08fe549ed8 | |||
| ae15efdf2a | |||
| 8e003f95db | |||
| 3e92e837f1 | |||
| 081307ced2 | |||
| a91bb399f0 | |||
| 42b78c59b5 |
+1
-1
@@ -1 +1 @@
|
|||||||
22.17.1
|
22.18.0
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4
|
uses: github/codeql-action/init@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4
|
uses: github/codeql-action/autobuild@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -76,6 +76,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4
|
uses: github/codeql-action/analyze@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Upload SARIF file
|
- name: Upload SARIF file
|
||||||
uses: github/codeql-action/upload-sarif@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4
|
uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
category: zizmor
|
category: zizmor
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
22.17.1
|
22.18.0
|
||||||
|
|||||||
Generated
+70
-102
@@ -27,7 +27,7 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/micromatch": "^4.0.9",
|
"@types/micromatch": "^4.0.9",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"@vitest/coverage-v8": "^3.0.0",
|
"@vitest/coverage-v8": "^3.0.0",
|
||||||
"byte-size": "^9.0.0",
|
"byte-size": "^9.0.0",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^59.0.0",
|
"eslint-plugin-unicorn": "^60.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -90,9 +90,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.25.9",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -632,9 +632,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/core": {
|
"node_modules/@eslint/core": {
|
||||||
"version": "0.14.0",
|
"version": "0.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
|
||||||
"integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
|
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -682,9 +682,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.31.0",
|
"version": "9.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz",
|
||||||
"integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==",
|
"integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -705,13 +705,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/plugin-kit": {
|
"node_modules/@eslint/plugin-kit": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz",
|
||||||
"integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==",
|
"integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^0.14.0",
|
"@eslint/core": "^0.15.1",
|
||||||
"levn": "^0.4.1"
|
"levn": "^0.4.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1355,9 +1355,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.16.5",
|
"version": "22.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.0.tgz",
|
||||||
"integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==",
|
"integrity": "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1897,9 +1897,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.24.4",
|
"version": "4.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
|
||||||
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
|
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1917,10 +1917,10 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001688",
|
"caniuse-lite": "^1.0.30001726",
|
||||||
"electron-to-chromium": "^1.5.73",
|
"electron-to-chromium": "^1.5.173",
|
||||||
"node-releases": "^2.0.19",
|
"node-releases": "^2.0.19",
|
||||||
"update-browserslist-db": "^1.1.1"
|
"update-browserslist-db": "^1.1.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"browserslist": "cli.js"
|
"browserslist": "cli.js"
|
||||||
@@ -1981,9 +1981,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001713",
|
"version": "1.0.30001731",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
|
||||||
"integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==",
|
"integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2035,6 +2035,13 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/change-case": {
|
||||||
|
"version": "5.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
|
||||||
|
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/check-error": {
|
"node_modules/check-error": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
||||||
@@ -2061,9 +2068,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ci-info": {
|
"node_modules/ci-info": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz",
|
||||||
"integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==",
|
"integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2150,13 +2157,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.41.0",
|
"version": "3.45.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.0.tgz",
|
||||||
"integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==",
|
"integrity": "sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.4"
|
"browserslist": "^4.25.1"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -2221,9 +2228,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.137",
|
"version": "1.5.195",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.195.tgz",
|
||||||
"integrity": "sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA==",
|
"integrity": "sha512-URclP0iIaDUzqcAyV1v2PgduJ9N0IdXmWsnPzPfelvBmjmZzEy6xJcjb1cXj+TbYqXgtLrjHEoaSIdTYhw4ezg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -2306,9 +2313,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.31.0",
|
"version": "9.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz",
|
||||||
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
|
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2318,8 +2325,8 @@
|
|||||||
"@eslint/config-helpers": "^0.3.0",
|
"@eslint/config-helpers": "^0.3.0",
|
||||||
"@eslint/core": "^0.15.0",
|
"@eslint/core": "^0.15.0",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "9.31.0",
|
"@eslint/js": "9.32.0",
|
||||||
"@eslint/plugin-kit": "^0.3.1",
|
"@eslint/plugin-kit": "^0.3.4",
|
||||||
"@humanfs/node": "^0.16.6",
|
"@humanfs/node": "^0.16.6",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
"@humanwhocodes/retry": "^0.4.2",
|
"@humanwhocodes/retry": "^0.4.2",
|
||||||
@@ -2414,65 +2421,39 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-unicorn": {
|
"node_modules/eslint-plugin-unicorn": {
|
||||||
"version": "59.0.1",
|
"version": "60.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-59.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-60.0.0.tgz",
|
||||||
"integrity": "sha512-EtNXYuWPUmkgSU2E7Ttn57LbRREQesIP1BiLn7OZLKodopKfDXfBUkC/0j6mpw2JExwf43Uf3qLSvrSvppgy8Q==",
|
"integrity": "sha512-QUzTefvP8stfSXsqKQ+vBQSEsXIlAiCduS/V1Em+FKgL9c21U/IIm20/e3MFy1jyCf14tHAhqC1sX8OTy6VUCg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.25.9",
|
"@babel/helper-validator-identifier": "^7.27.1",
|
||||||
"@eslint-community/eslint-utils": "^4.5.1",
|
"@eslint-community/eslint-utils": "^4.7.0",
|
||||||
"@eslint/plugin-kit": "^0.2.7",
|
"@eslint/plugin-kit": "^0.3.3",
|
||||||
"ci-info": "^4.2.0",
|
"change-case": "^5.4.4",
|
||||||
|
"ci-info": "^4.3.0",
|
||||||
"clean-regexp": "^1.0.0",
|
"clean-regexp": "^1.0.0",
|
||||||
"core-js-compat": "^3.41.0",
|
"core-js-compat": "^3.44.0",
|
||||||
"esquery": "^1.6.0",
|
"esquery": "^1.6.0",
|
||||||
"find-up-simple": "^1.0.1",
|
"find-up-simple": "^1.0.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.3.0",
|
||||||
"indent-string": "^5.0.0",
|
"indent-string": "^5.0.0",
|
||||||
"is-builtin-module": "^5.0.0",
|
"is-builtin-module": "^5.0.0",
|
||||||
"jsesc": "^3.1.0",
|
"jsesc": "^3.1.0",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"regexp-tree": "^0.1.27",
|
"regexp-tree": "^0.1.27",
|
||||||
"regjsparser": "^0.12.0",
|
"regjsparser": "^0.12.0",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.2",
|
||||||
"strip-indent": "^4.0.0"
|
"strip-indent": "^4.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.20.0 || ^20.10.0 || >=21.0.0"
|
"node": "^20.10.0 || >=21.0.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": ">=9.22.0"
|
"eslint": ">=9.29.0"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/core": {
|
|
||||||
"version": "0.13.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
|
|
||||||
"integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/json-schema": "^7.0.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/plugin-kit": {
|
|
||||||
"version": "0.2.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz",
|
|
||||||
"integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@eslint/core": "^0.13.0",
|
|
||||||
"levn": "^0.4.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-scope": {
|
"node_modules/eslint-scope": {
|
||||||
@@ -2505,19 +2486,6 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"url": "https://opencollective.com/eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint/node_modules/@eslint/core": {
|
|
||||||
"version": "0.15.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
|
|
||||||
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/json-schema": "^7.0.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/espree": {
|
"node_modules/espree": {
|
||||||
"version": "10.4.0",
|
"version": "10.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||||
@@ -3727,9 +3695,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.1",
|
"version": "7.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -4211,15 +4179,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.0.5",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz",
|
||||||
"integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==",
|
"integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.6",
|
"fdir": "^6.4.6",
|
||||||
"picomatch": "^4.0.2",
|
"picomatch": "^4.0.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"rollup": "^4.40.0",
|
"rollup": "^4.40.0",
|
||||||
"tinyglobby": "^0.2.14"
|
"tinyglobby": "^0.2.14"
|
||||||
|
|||||||
+3
-3
@@ -21,7 +21,7 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/micromatch": "^4.0.9",
|
"@types/micromatch": "^4.0.9",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"@vitest/coverage-v8": "^3.0.0",
|
"@vitest/coverage-v8": "^3.0.0",
|
||||||
"byte-size": "^9.0.0",
|
"byte-size": "^9.0.0",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^59.0.0",
|
"eslint-plugin-unicorn": "^60.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
@@ -69,6 +69,6 @@
|
|||||||
"micromatch": "^4.0.8"
|
"micromatch": "^4.0.8"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.17.1"
|
"node": "22.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ services:
|
|||||||
command: ['./run.sh', '-disable-reporting']
|
command: ['./run.sh', '-disable-reporting']
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
image: grafana/grafana:12.0.2-ubuntu@sha256:0512d81cdeaaff0e370a9aa66027b465d1f1f04379c3a9c801a905fabbdbc7a5
|
image: grafana/grafana:12.1.0-ubuntu@sha256:397aa30dd1af16cb6c5c9879498e467973a7f87eacf949f6d5a29407a3843809
|
||||||
volumes:
|
volumes:
|
||||||
- grafana-data:/var/lib/grafana
|
- grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
22.17.1
|
22.18.0
|
||||||
|
|||||||
+1
-1
@@ -59,6 +59,6 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.17.1"
|
"node": "22.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
22.17.1
|
22.18.0
|
||||||
|
|||||||
Generated
+74
-106
@@ -16,7 +16,7 @@
|
|||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@socket.io/component-emitter": "^3.1.2",
|
"@socket.io/component-emitter": "^3.1.2",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"@types/oidc-provider": "^9.0.0",
|
"@types/oidc-provider": "^9.0.0",
|
||||||
"@types/pg": "^8.15.1",
|
"@types/pg": "^8.15.1",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^59.0.0",
|
"eslint-plugin-unicorn": "^60.0.0",
|
||||||
"exiftool-vendored": "^28.3.1",
|
"exiftool-vendored": "^28.3.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jose": "^5.6.3",
|
"jose": "^5.6.3",
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/micromatch": "^4.0.9",
|
"@types/micromatch": "^4.0.9",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"@vitest/coverage-v8": "^3.0.0",
|
"@vitest/coverage-v8": "^3.0.0",
|
||||||
"byte-size": "^9.0.0",
|
"byte-size": "^9.0.0",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^59.0.0",
|
"eslint-plugin-unicorn": "^60.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -131,9 +131,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.25.9",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -684,9 +684,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/core": {
|
"node_modules/@eslint/core": {
|
||||||
"version": "0.14.0",
|
"version": "0.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
|
||||||
"integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
|
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -734,9 +734,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.31.0",
|
"version": "9.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz",
|
||||||
"integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==",
|
"integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -757,13 +757,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/plugin-kit": {
|
"node_modules/@eslint/plugin-kit": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz",
|
||||||
"integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==",
|
"integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^0.14.0",
|
"@eslint/core": "^0.15.1",
|
||||||
"levn": "^0.4.1"
|
"levn": "^0.4.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1999,9 +1999,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/luxon": {
|
"node_modules/@types/luxon": {
|
||||||
"version": "3.6.2",
|
"version": "3.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
|
||||||
"integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==",
|
"integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -2020,9 +2020,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.16.5",
|
"version": "22.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.0.tgz",
|
||||||
"integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==",
|
"integrity": "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2030,9 +2030,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/oidc-provider": {
|
"node_modules/@types/oidc-provider": {
|
||||||
"version": "9.1.1",
|
"version": "9.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-9.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-9.1.2.tgz",
|
||||||
"integrity": "sha512-sG4UcE4AbUwAsEpyrcyoqZ383wJiQObZU+gTa1Iv288+l09HwSr88hBZE2IBLlXS+RKmLId0i4B430PBFO/XRA==",
|
"integrity": "sha512-JAreXkbWsZR72Gt3eigG652wq1qBcjhuy421PXU2a8PS0mM00XlG+UdXbM/QPihM3ko0YF8cwvt0H2kacXGcsg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2042,9 +2042,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/pg": {
|
"node_modules/@types/pg": {
|
||||||
"version": "8.15.4",
|
"version": "8.15.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz",
|
||||||
"integrity": "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==",
|
"integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2741,9 +2741,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.24.4",
|
"version": "4.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
|
||||||
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
|
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2761,10 +2761,10 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001688",
|
"caniuse-lite": "^1.0.30001726",
|
||||||
"electron-to-chromium": "^1.5.73",
|
"electron-to-chromium": "^1.5.173",
|
||||||
"node-releases": "^2.0.19",
|
"node-releases": "^2.0.19",
|
||||||
"update-browserslist-db": "^1.1.1"
|
"update-browserslist-db": "^1.1.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"browserslist": "cli.js"
|
"browserslist": "cli.js"
|
||||||
@@ -2862,9 +2862,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001713",
|
"version": "1.0.30001731",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
|
||||||
"integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==",
|
"integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2916,6 +2916,13 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/change-case": {
|
||||||
|
"version": "5.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
|
||||||
|
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/check-error": {
|
"node_modules/check-error": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
||||||
@@ -2937,9 +2944,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ci-info": {
|
"node_modules/ci-info": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz",
|
||||||
"integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==",
|
"integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3112,13 +3119,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.41.0",
|
"version": "3.45.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.0.tgz",
|
||||||
"integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==",
|
"integrity": "sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.4"
|
"browserslist": "^4.25.1"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3271,9 +3278,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.137",
|
"version": "1.5.195",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.195.tgz",
|
||||||
"integrity": "sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA==",
|
"integrity": "sha512-URclP0iIaDUzqcAyV1v2PgduJ9N0IdXmWsnPzPfelvBmjmZzEy6xJcjb1cXj+TbYqXgtLrjHEoaSIdTYhw4ezg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -3464,9 +3471,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.31.0",
|
"version": "9.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz",
|
||||||
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
|
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3476,8 +3483,8 @@
|
|||||||
"@eslint/config-helpers": "^0.3.0",
|
"@eslint/config-helpers": "^0.3.0",
|
||||||
"@eslint/core": "^0.15.0",
|
"@eslint/core": "^0.15.0",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "9.31.0",
|
"@eslint/js": "9.32.0",
|
||||||
"@eslint/plugin-kit": "^0.3.1",
|
"@eslint/plugin-kit": "^0.3.4",
|
||||||
"@humanfs/node": "^0.16.6",
|
"@humanfs/node": "^0.16.6",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
"@humanwhocodes/retry": "^0.4.2",
|
"@humanwhocodes/retry": "^0.4.2",
|
||||||
@@ -3572,65 +3579,39 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-unicorn": {
|
"node_modules/eslint-plugin-unicorn": {
|
||||||
"version": "59.0.1",
|
"version": "60.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-59.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-60.0.0.tgz",
|
||||||
"integrity": "sha512-EtNXYuWPUmkgSU2E7Ttn57LbRREQesIP1BiLn7OZLKodopKfDXfBUkC/0j6mpw2JExwf43Uf3qLSvrSvppgy8Q==",
|
"integrity": "sha512-QUzTefvP8stfSXsqKQ+vBQSEsXIlAiCduS/V1Em+FKgL9c21U/IIm20/e3MFy1jyCf14tHAhqC1sX8OTy6VUCg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.25.9",
|
"@babel/helper-validator-identifier": "^7.27.1",
|
||||||
"@eslint-community/eslint-utils": "^4.5.1",
|
"@eslint-community/eslint-utils": "^4.7.0",
|
||||||
"@eslint/plugin-kit": "^0.2.7",
|
"@eslint/plugin-kit": "^0.3.3",
|
||||||
"ci-info": "^4.2.0",
|
"change-case": "^5.4.4",
|
||||||
|
"ci-info": "^4.3.0",
|
||||||
"clean-regexp": "^1.0.0",
|
"clean-regexp": "^1.0.0",
|
||||||
"core-js-compat": "^3.41.0",
|
"core-js-compat": "^3.44.0",
|
||||||
"esquery": "^1.6.0",
|
"esquery": "^1.6.0",
|
||||||
"find-up-simple": "^1.0.1",
|
"find-up-simple": "^1.0.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.3.0",
|
||||||
"indent-string": "^5.0.0",
|
"indent-string": "^5.0.0",
|
||||||
"is-builtin-module": "^5.0.0",
|
"is-builtin-module": "^5.0.0",
|
||||||
"jsesc": "^3.1.0",
|
"jsesc": "^3.1.0",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"regexp-tree": "^0.1.27",
|
"regexp-tree": "^0.1.27",
|
||||||
"regjsparser": "^0.12.0",
|
"regjsparser": "^0.12.0",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.2",
|
||||||
"strip-indent": "^4.0.0"
|
"strip-indent": "^4.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.20.0 || ^20.10.0 || >=21.0.0"
|
"node": "^20.10.0 || >=21.0.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": ">=9.22.0"
|
"eslint": ">=9.29.0"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/core": {
|
|
||||||
"version": "0.13.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
|
|
||||||
"integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/json-schema": "^7.0.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/plugin-kit": {
|
|
||||||
"version": "0.2.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz",
|
|
||||||
"integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@eslint/core": "^0.13.0",
|
|
||||||
"levn": "^0.4.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-scope": {
|
"node_modules/eslint-scope": {
|
||||||
@@ -3663,19 +3644,6 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"url": "https://opencollective.com/eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint/node_modules/@eslint/core": {
|
|
||||||
"version": "0.15.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
|
|
||||||
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/json-schema": "^7.0.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/espree": {
|
"node_modules/espree": {
|
||||||
"version": "10.4.0",
|
"version": "10.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||||
|
|||||||
+3
-3
@@ -26,7 +26,7 @@
|
|||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@socket.io/component-emitter": "^3.1.2",
|
"@socket.io/component-emitter": "^3.1.2",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"@types/oidc-provider": "^9.0.0",
|
"@types/oidc-provider": "^9.0.0",
|
||||||
"@types/pg": "^8.15.1",
|
"@types/pg": "^8.15.1",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^59.0.0",
|
"eslint-plugin-unicorn": "^60.0.0",
|
||||||
"exiftool-vendored": "^28.3.1",
|
"exiftool-vendored": "^28.3.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jose": "^5.6.3",
|
"jose": "^5.6.3",
|
||||||
@@ -54,6 +54,6 @@
|
|||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.17.1"
|
"node": "22.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, asBearerAuth, shareUrl, utils } from 'src/utils';
|
import { app, asBearerAuth, baseUrl, shareUrl, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@@ -78,6 +78,7 @@ describe('/shared-links', () => {
|
|||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
albumId: metadataAlbum.id,
|
albumId: metadataAlbum.id,
|
||||||
showMetadata: true,
|
showMetadata: true,
|
||||||
|
slug: 'metadata-album',
|
||||||
}),
|
}),
|
||||||
utils.createSharedLink(user1.accessToken, {
|
utils.createSharedLink(user1.accessToken, {
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
@@ -138,6 +139,17 @@ describe('/shared-links', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('GET /s/:slug', () => {
|
||||||
|
it('should work for slug auth', async () => {
|
||||||
|
const resp = await request(baseUrl).get(`/s/${linkWithMetadata.slug}`);
|
||||||
|
expect(resp.status).toBe(200);
|
||||||
|
expect(resp.header['content-type']).toContain('text/html');
|
||||||
|
expect(resp.text).toContain(
|
||||||
|
`<meta name="description" content="${metadataAlbum.assets.length} shared photos & videos" />`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('GET /shared-links', () => {
|
describe('GET /shared-links', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/shared-links');
|
const { status, body } = await request(app).get('/shared-links');
|
||||||
|
|||||||
@@ -724,6 +724,7 @@
|
|||||||
"create_new_user": "Create new user",
|
"create_new_user": "Create new user",
|
||||||
"create_shared_album_page_share_add_assets": "ADD ASSETS",
|
"create_shared_album_page_share_add_assets": "ADD ASSETS",
|
||||||
"create_shared_album_page_share_select_photos": "Select Photos",
|
"create_shared_album_page_share_select_photos": "Select Photos",
|
||||||
|
"create_shared_link": "Create shared link",
|
||||||
"create_tag": "Create tag",
|
"create_tag": "Create tag",
|
||||||
"create_tag_description": "Create a new tag. For nested tags, please enter the full path of the tag including forward slashes.",
|
"create_tag_description": "Create a new tag. For nested tags, please enter the full path of the tag including forward slashes.",
|
||||||
"create_user": "Create user",
|
"create_user": "Create user",
|
||||||
@@ -1173,6 +1174,7 @@
|
|||||||
"latest_version": "Latest Version",
|
"latest_version": "Latest Version",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
"leave": "Leave",
|
"leave": "Leave",
|
||||||
|
"leave_album": "Leave album",
|
||||||
"lens_model": "Lens model",
|
"lens_model": "Lens model",
|
||||||
"let_others_respond": "Let others respond",
|
"let_others_respond": "Let others respond",
|
||||||
"level": "Level",
|
"level": "Level",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
|
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||||
// See also: https://pub.dev/packages/pigeon
|
// See also: https://pub.dev/packages/pigeon
|
||||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||||
|
|
||||||
|
|||||||
@@ -24,14 +24,23 @@ class ImmichAPI(cfg: ServerConfig) {
|
|||||||
|
|
||||||
val serverURL = prefs.getString("widget_server_url", "") ?: ""
|
val serverURL = prefs.getString("widget_server_url", "") ?: ""
|
||||||
val sessionKey = prefs.getString("widget_auth_token", "") ?: ""
|
val sessionKey = prefs.getString("widget_auth_token", "") ?: ""
|
||||||
|
val customHeadersJSON = prefs.getString("widget_custom_headers", "") ?: ""
|
||||||
|
|
||||||
if (serverURL.isBlank() || sessionKey.isBlank()) {
|
if (serverURL.isBlank() || sessionKey.isBlank()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var customHeaders: Map<String, String> = HashMap<String, String>()
|
||||||
|
|
||||||
|
if (customHeadersJSON.isNotBlank()) {
|
||||||
|
val stringMapType = object : TypeToken<Map<String, String>>() {}.type
|
||||||
|
customHeaders = Gson().fromJson(customHeadersJSON, stringMapType)
|
||||||
|
}
|
||||||
|
|
||||||
return ServerConfig(
|
return ServerConfig(
|
||||||
serverURL,
|
serverURL,
|
||||||
sessionKey
|
sessionKey,
|
||||||
|
customHeaders
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,11 +59,19 @@ class ImmichAPI(cfg: ServerConfig) {
|
|||||||
return URL(urlString.toString())
|
return URL(urlString.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun HttpURLConnection.applyCustomHeaders() {
|
||||||
|
serverConfig.customHeaders.forEach { (key, value) ->
|
||||||
|
setRequestProperty(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun fetchSearchResults(filters: SearchFilters): List<Asset> = withContext(Dispatchers.IO) {
|
suspend fun fetchSearchResults(filters: SearchFilters): List<Asset> = withContext(Dispatchers.IO) {
|
||||||
val url = buildRequestURL("/search/random")
|
val url = buildRequestURL("/search/random")
|
||||||
val connection = (url.openConnection() as HttpURLConnection).apply {
|
val connection = (url.openConnection() as HttpURLConnection).apply {
|
||||||
requestMethod = "POST"
|
requestMethod = "POST"
|
||||||
setRequestProperty("Content-Type", "application/json")
|
setRequestProperty("Content-Type", "application/json")
|
||||||
|
applyCustomHeaders()
|
||||||
|
|
||||||
doOutput = true
|
doOutput = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +92,7 @@ class ImmichAPI(cfg: ServerConfig) {
|
|||||||
val url = buildRequestURL("/memories", listOf("for" to iso8601))
|
val url = buildRequestURL("/memories", listOf("for" to iso8601))
|
||||||
val connection = (url.openConnection() as HttpURLConnection).apply {
|
val connection = (url.openConnection() as HttpURLConnection).apply {
|
||||||
requestMethod = "GET"
|
requestMethod = "GET"
|
||||||
|
applyCustomHeaders()
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = connection.inputStream.bufferedReader().readText()
|
val response = connection.inputStream.bufferedReader().readText()
|
||||||
@@ -94,6 +112,7 @@ class ImmichAPI(cfg: ServerConfig) {
|
|||||||
val url = buildRequestURL("/albums")
|
val url = buildRequestURL("/albums")
|
||||||
val connection = (url.openConnection() as HttpURLConnection).apply {
|
val connection = (url.openConnection() as HttpURLConnection).apply {
|
||||||
requestMethod = "GET"
|
requestMethod = "GET"
|
||||||
|
applyCustomHeaders()
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = connection.inputStream.bufferedReader().readText()
|
val response = connection.inputStream.bufferedReader().readText()
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ data class WidgetEntry (
|
|||||||
val deeplink: String?
|
val deeplink: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ServerConfig(val serverEndpoint: String, val sessionKey: String)
|
data class ServerConfig(
|
||||||
|
val serverEndpoint: String,
|
||||||
|
val sessionKey: String,
|
||||||
|
val customHeaders: Map<String, String>
|
||||||
|
)
|
||||||
|
|
||||||
// MARK: Widget State Keys
|
// MARK: Widget State Keys
|
||||||
val kImageUUID = stringPreferencesKey("uuid")
|
val kImageUUID = stringPreferencesKey("uuid")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
|
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||||
// See also: https://pub.dev/packages/pigeon
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|||||||
@@ -104,10 +104,13 @@ struct Album: Codable, Equatable {
|
|||||||
// MARK: API
|
// MARK: API
|
||||||
|
|
||||||
class ImmichAPI {
|
class ImmichAPI {
|
||||||
|
typealias CustomHeaders = [String:String]
|
||||||
struct ServerConfig {
|
struct ServerConfig {
|
||||||
let serverEndpoint: String
|
let serverEndpoint: String
|
||||||
let sessionKey: String
|
let sessionKey: String
|
||||||
|
let customHeaders: CustomHeaders
|
||||||
}
|
}
|
||||||
|
|
||||||
let serverConfig: ServerConfig
|
let serverConfig: ServerConfig
|
||||||
|
|
||||||
init() async throws {
|
init() async throws {
|
||||||
@@ -122,10 +125,20 @@ class ImmichAPI {
|
|||||||
if serverURL == "" || sessionKey == "" {
|
if serverURL == "" || sessionKey == "" {
|
||||||
throw WidgetError.noLogin
|
throw WidgetError.noLogin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// custom headers come in the form of KV pairs in JSON
|
||||||
|
var customHeadersJSON = (defaults.string(forKey: "widget_custom_headers") ?? "")
|
||||||
|
var customHeaders: CustomHeaders = [:]
|
||||||
|
|
||||||
|
if customHeadersJSON != "",
|
||||||
|
let parsedHeaders = try? JSONDecoder().decode(CustomHeaders.self, from: customHeadersJSON.data(using: .utf8)!) {
|
||||||
|
customHeaders = parsedHeaders
|
||||||
|
}
|
||||||
|
|
||||||
serverConfig = ServerConfig(
|
serverConfig = ServerConfig(
|
||||||
serverEndpoint: serverURL,
|
serverEndpoint: serverURL,
|
||||||
sessionKey: sessionKey
|
sessionKey: sessionKey,
|
||||||
|
customHeaders: customHeaders
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +168,12 @@ class ImmichAPI {
|
|||||||
|
|
||||||
return components?.url
|
return components?.url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyCustomHeaders(for request: inout URLRequest) {
|
||||||
|
for (header, value) in serverConfig.customHeaders {
|
||||||
|
request.addValue(value, forHTTPHeaderField: header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func fetchSearchResults(with filters: SearchFilter = Album.NONE.filter)
|
func fetchSearchResults(with filters: SearchFilter = Album.NONE.filter)
|
||||||
async throws
|
async throws
|
||||||
@@ -174,7 +193,8 @@ class ImmichAPI {
|
|||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
request.httpBody = try JSONEncoder().encode(filters)
|
request.httpBody = try JSONEncoder().encode(filters)
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
applyCustomHeaders(for: &request)
|
||||||
|
|
||||||
let (data, _) = try await URLSession.shared.data(for: request)
|
let (data, _) = try await URLSession.shared.data(for: request)
|
||||||
|
|
||||||
// decode data
|
// decode data
|
||||||
@@ -196,6 +216,7 @@ class ImmichAPI {
|
|||||||
|
|
||||||
var request = URLRequest(url: searchURL)
|
var request = URLRequest(url: searchURL)
|
||||||
request.httpMethod = "GET"
|
request.httpMethod = "GET"
|
||||||
|
applyCustomHeaders(for: &request)
|
||||||
|
|
||||||
let (data, _) = try await URLSession.shared.data(for: request)
|
let (data, _) = try await URLSession.shared.data(for: request)
|
||||||
|
|
||||||
@@ -254,7 +275,8 @@ class ImmichAPI {
|
|||||||
|
|
||||||
var request = URLRequest(url: searchURL)
|
var request = URLRequest(url: searchURL)
|
||||||
request.httpMethod = "GET"
|
request.httpMethod = "GET"
|
||||||
|
applyCustomHeaders(for: &request)
|
||||||
|
|
||||||
let (data, _) = try await URLSession.shared.data(for: request)
|
let (data, _) = try await URLSession.shared.data(for: request)
|
||||||
|
|
||||||
// decode data
|
// decode data
|
||||||
|
|||||||
@@ -30,9 +30,10 @@ const int kTimelineAssetLoadBatchSize = 256;
|
|||||||
const int kTimelineAssetLoadOppositeSize = 64;
|
const int kTimelineAssetLoadOppositeSize = 64;
|
||||||
|
|
||||||
// Widget keys
|
// Widget keys
|
||||||
|
const String appShareGroupId = "group.app.immich.share";
|
||||||
const String kWidgetAuthToken = "widget_auth_token";
|
const String kWidgetAuthToken = "widget_auth_token";
|
||||||
const String kWidgetServerEndpoint = "widget_server_url";
|
const String kWidgetServerEndpoint = "widget_server_url";
|
||||||
const String appShareGroupId = "group.app.immich.share";
|
const String kWidgetCustomHeaders = "widget_custom_headers";
|
||||||
|
|
||||||
// add widget identifiers here for new widgets
|
// add widget identifiers here for new widgets
|
||||||
// these are used to force a widget refresh
|
// these are used to force a widget refresh
|
||||||
|
|||||||
@@ -128,6 +128,18 @@ class RemoteAlbumService {
|
|||||||
return _repository.addUsers(albumId, userIds);
|
return _repository.addUsers(albumId, userIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> removeUser(String albumId, {required String userId}) async {
|
||||||
|
await _albumApiRepository.removeUser(albumId, userId: userId);
|
||||||
|
|
||||||
|
return _repository.removeUser(albumId, userId: userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setActivityStatus(String albumId, bool enabled) async {
|
||||||
|
await _albumApiRepository.setActivityStatus(albumId, enabled);
|
||||||
|
|
||||||
|
return _repository.setActivityStatus(albumId, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> getCount() {
|
Future<int> getCount() {
|
||||||
return _repository.getCount();
|
return _repository.getCount();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,14 +139,18 @@ class SyncStreamService {
|
|||||||
return _syncStreamRepository.updateAlbumUsersV1(data.cast(), debugLabel: 'backfill');
|
return _syncStreamRepository.updateAlbumUsersV1(data.cast(), debugLabel: 'backfill');
|
||||||
case SyncEntityType.albumUserDeleteV1:
|
case SyncEntityType.albumUserDeleteV1:
|
||||||
return _syncStreamRepository.deleteAlbumUsersV1(data.cast());
|
return _syncStreamRepository.deleteAlbumUsersV1(data.cast());
|
||||||
case SyncEntityType.albumAssetV1:
|
case SyncEntityType.albumAssetCreateV1:
|
||||||
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album');
|
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset create');
|
||||||
|
case SyncEntityType.albumAssetUpdateV1:
|
||||||
|
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset update');
|
||||||
case SyncEntityType.albumAssetBackfillV1:
|
case SyncEntityType.albumAssetBackfillV1:
|
||||||
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album backfill');
|
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset backfill');
|
||||||
case SyncEntityType.albumAssetExifV1:
|
case SyncEntityType.albumAssetExifCreateV1:
|
||||||
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'album');
|
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'album asset exif create');
|
||||||
|
case SyncEntityType.albumAssetExifUpdateV1:
|
||||||
|
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'album asset exif update');
|
||||||
case SyncEntityType.albumAssetExifBackfillV1:
|
case SyncEntityType.albumAssetExifBackfillV1:
|
||||||
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'album backfill');
|
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'album asset exif backfill');
|
||||||
case SyncEntityType.albumToAssetV1:
|
case SyncEntityType.albumToAssetV1:
|
||||||
return _syncStreamRepository.updateAlbumToAssetsV1(data.cast());
|
return _syncStreamRepository.updateAlbumToAssetsV1(data.cast());
|
||||||
case SyncEntityType.albumToAssetBackfillV1:
|
case SyncEntityType.albumToAssetBackfillV1:
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ extension ContextHelper on BuildContext {
|
|||||||
// Returns the current height from MediaQuery
|
// Returns the current height from MediaQuery
|
||||||
double get height => MediaQuery.sizeOf(this).height;
|
double get height => MediaQuery.sizeOf(this).height;
|
||||||
|
|
||||||
|
// Returns the current size from MediaQuery
|
||||||
|
Size get sizeData => MediaQuery.sizeOf(this);
|
||||||
|
|
||||||
// Returns true if the app is running on a mobile device (!tablets)
|
// Returns true if the app is running on a mobile device (!tablets)
|
||||||
bool get isMobile => width < 550;
|
bool get isMobile => width < 550;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/painting.dart';
|
||||||
|
|
||||||
|
extension CodecImageInfoExtension on Codec {
|
||||||
|
Future<ImageInfo> getImageInfo({double scale = 1.0}) async {
|
||||||
|
final frame = await getNextFrame();
|
||||||
|
return ImageInfo(image: frame.image, scale: scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -113,6 +113,7 @@ class DriftBackupRepository extends DriftDatabaseRepository {
|
|||||||
final query = _db.localAssetEntity.select()
|
final query = _db.localAssetEntity.select()
|
||||||
..where(
|
..where(
|
||||||
(lae) =>
|
(lae) =>
|
||||||
|
lae.checksum.isNotNull() &
|
||||||
existsQuery(
|
existsQuery(
|
||||||
_db.localAlbumAssetEntity.selectOnly()
|
_db.localAlbumAssetEntity.selectOnly()
|
||||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||||
@@ -125,9 +126,7 @@ class DriftBackupRepository extends DriftDatabaseRepository {
|
|||||||
_db.remoteAssetEntity.selectOnly()
|
_db.remoteAssetEntity.selectOnly()
|
||||||
..addColumns([_db.remoteAssetEntity.checksum])
|
..addColumns([_db.remoteAssetEntity.checksum])
|
||||||
..where(
|
..where(
|
||||||
_db.remoteAssetEntity.checksum.equalsExp(lae.checksum) &
|
_db.remoteAssetEntity.checksum.equalsExp(lae.checksum) & _db.remoteAssetEntity.ownerId.equals(userId),
|
||||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
|
||||||
lae.checksum.isNotNull(),
|
|
||||||
),
|
),
|
||||||
) &
|
) &
|
||||||
lae.id.isNotInQuery(_getExcludedSubquery()),
|
lae.id.isNotInQuery(_getExcludedSubquery()),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class DriftPeopleRepository extends DriftDatabaseRepository {
|
|||||||
leftOuterJoin(_db.assetFaceEntity, _db.assetFaceEntity.personId.equalsExp(_db.personEntity.id)),
|
leftOuterJoin(_db.assetFaceEntity, _db.assetFaceEntity.personId.equalsExp(_db.personEntity.id)),
|
||||||
])
|
])
|
||||||
..where(_db.personEntity.isHidden.equals(false))
|
..where(_db.personEntity.isHidden.equals(false))
|
||||||
..groupBy([_db.personEntity.id])
|
..groupBy([_db.personEntity.id], having: _db.assetFaceEntity.id.count().isBiggerOrEqualValue(3))
|
||||||
..orderBy([
|
..orderBy([
|
||||||
OrderingTerm(expression: _db.personEntity.name.equals('').not(), mode: OrderingMode.desc),
|
OrderingTerm(expression: _db.personEntity.name.equals('').not(), mode: OrderingMode.desc),
|
||||||
OrderingTerm(expression: _db.assetFaceEntity.id.count(), mode: OrderingMode.desc),
|
OrderingTerm(expression: _db.assetFaceEntity.id.count(), mode: OrderingMode.desc),
|
||||||
|
|||||||
@@ -220,12 +220,22 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> removeUser(String albumId, {required String userId}) {
|
||||||
|
return _db.remoteAlbumUserEntity.deleteWhere((row) => row.albumId.equals(albumId) & row.userId.equals(userId));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> deleteAlbum(String albumId) async {
|
Future<void> deleteAlbum(String albumId) async {
|
||||||
return _db.transaction(() async {
|
return _db.transaction(() async {
|
||||||
await _db.remoteAlbumEntity.deleteWhere((table) => table.id.equals(albumId));
|
await _db.remoteAlbumEntity.deleteWhere((table) => table.id.equals(albumId));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setActivityStatus(String albumId, bool isEnabled) async {
|
||||||
|
final query = _db.update(_db.remoteAlbumEntity)..where((row) => row.id.equals(albumId));
|
||||||
|
|
||||||
|
await query.write(RemoteAlbumEntityCompanion(isActivityEnabled: Value(isEnabled)));
|
||||||
|
}
|
||||||
|
|
||||||
Stream<RemoteAlbum?> watchAlbum(String albumId) {
|
Stream<RemoteAlbum?> watchAlbum(String albumId) {
|
||||||
final query =
|
final query =
|
||||||
_db.remoteAlbumEntity.select().join([
|
_db.remoteAlbumEntity.select().join([
|
||||||
|
|||||||
@@ -149,9 +149,11 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
|||||||
SyncEntityType.albumUserV1: SyncAlbumUserV1.fromJson,
|
SyncEntityType.albumUserV1: SyncAlbumUserV1.fromJson,
|
||||||
SyncEntityType.albumUserBackfillV1: SyncAlbumUserV1.fromJson,
|
SyncEntityType.albumUserBackfillV1: SyncAlbumUserV1.fromJson,
|
||||||
SyncEntityType.albumUserDeleteV1: SyncAlbumUserDeleteV1.fromJson,
|
SyncEntityType.albumUserDeleteV1: SyncAlbumUserDeleteV1.fromJson,
|
||||||
SyncEntityType.albumAssetV1: SyncAssetV1.fromJson,
|
SyncEntityType.albumAssetCreateV1: SyncAssetV1.fromJson,
|
||||||
|
SyncEntityType.albumAssetUpdateV1: SyncAssetV1.fromJson,
|
||||||
SyncEntityType.albumAssetBackfillV1: SyncAssetV1.fromJson,
|
SyncEntityType.albumAssetBackfillV1: SyncAssetV1.fromJson,
|
||||||
SyncEntityType.albumAssetExifV1: SyncAssetExifV1.fromJson,
|
SyncEntityType.albumAssetExifCreateV1: SyncAssetExifV1.fromJson,
|
||||||
|
SyncEntityType.albumAssetExifUpdateV1: SyncAssetExifV1.fromJson,
|
||||||
SyncEntityType.albumAssetExifBackfillV1: SyncAssetExifV1.fromJson,
|
SyncEntityType.albumAssetExifBackfillV1: SyncAssetExifV1.fromJson,
|
||||||
SyncEntityType.albumToAssetV1: SyncAlbumToAssetV1.fromJson,
|
SyncEntityType.albumToAssetV1: SyncAlbumToAssetV1.fromJson,
|
||||||
SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson,
|
SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import 'package:immich_mobile/providers/routes.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||||
import 'package:immich_mobile/routing/app_navigation_observer.dart';
|
import 'package:immich_mobile/routing/app_navigation_observer.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/backgroundLegacy.service.dart';
|
||||||
import 'package:immich_mobile/services/deep_link.service.dart';
|
import 'package:immich_mobile/services/deep_link.service.dart';
|
||||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||||
import 'package:immich_mobile/theme/dynamic_theme.dart';
|
import 'package:immich_mobile/theme/dynamic_theme.dart';
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
|
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||||
// See also: https://pub.dev/packages/pigeon
|
// See also: https://pub.dev/packages/pigeon
|
||||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/presentation/widgets/memory/memory_lane.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/memory/memory_lane.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class MainTimelinePage extends ConsumerWidget {
|
class MainTimelinePage extends ConsumerWidget {
|
||||||
@@ -12,10 +13,14 @@ class MainTimelinePage extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
|
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
|
||||||
|
final memoriesEnabled = ref.watch(currentUserProvider.select((user) => user?.memoryEnabled ?? true));
|
||||||
|
|
||||||
|
// TODO: the user preferences need to be updated
|
||||||
|
// from the server to get live hiding/showing of memory lane
|
||||||
|
|
||||||
return memoryLaneProvider.maybeWhen(
|
return memoryLaneProvider.maybeWhen(
|
||||||
data: (memories) {
|
data: (memories) {
|
||||||
return memories.isEmpty
|
return memories.isEmpty || !memoriesEnabled
|
||||||
? const Timeline()
|
? const Timeline()
|
||||||
: Timeline(
|
: Timeline(
|
||||||
topSliverWidget: SliverToBoxAdapter(
|
topSliverWidget: SliverToBoxAdapter(
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.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/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
|
||||||
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftAlbumOptionsPage extends HookConsumerWidget {
|
||||||
|
const DriftAlbumOptionsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final album = ref.watch(currentRemoteAlbumProvider);
|
||||||
|
if (album == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
final sharedUsersAsync = ref.watch(remoteAlbumSharedUsersProvider(album.id));
|
||||||
|
final userId = ref.watch(authProvider).userId;
|
||||||
|
final activityEnabled = useState(album.isActivityEnabled);
|
||||||
|
final isOwner = album.ownerId == userId;
|
||||||
|
|
||||||
|
void showErrorMessage() {
|
||||||
|
context.pop();
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "shared_album_section_people_action_error".t(context: context),
|
||||||
|
toastType: ToastType.error,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void leaveAlbum() async {
|
||||||
|
try {
|
||||||
|
await ref.read(remoteAlbumProvider.notifier).leaveAlbum(album.id, userId: userId);
|
||||||
|
context.navigateTo(const DriftAlbumsRoute());
|
||||||
|
} catch (_) {
|
||||||
|
showErrorMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeUserFromAlbum(UserDto user) async {
|
||||||
|
try {
|
||||||
|
await ref.read(remoteAlbumProvider.notifier).removeUser(album.id, user.id);
|
||||||
|
ref.invalidate(remoteAlbumSharedUsersProvider(album.id));
|
||||||
|
} catch (_) {
|
||||||
|
showErrorMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addUsers() async {
|
||||||
|
final newUsers = await context.pushRoute<List<String>>(DriftUserSelectionRoute(album: album));
|
||||||
|
|
||||||
|
if (newUsers == null || newUsers.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ref.read(remoteAlbumProvider.notifier).addUsers(album.id, newUsers);
|
||||||
|
|
||||||
|
if (newUsers.isNotEmpty) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "users_added_to_album_count".t(context: context, args: {'count': newUsers.length}),
|
||||||
|
toastType: ToastType.success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.invalidate(remoteAlbumSharedUsersProvider(album.id));
|
||||||
|
} catch (e) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "Failed to add users to album: ${e.toString()}",
|
||||||
|
toastType: ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleUserClick(UserDto user) {
|
||||||
|
var actions = [];
|
||||||
|
|
||||||
|
if (user.id == userId) {
|
||||||
|
actions = [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.exit_to_app_rounded),
|
||||||
|
title: const Text("leave_album").t(context: context),
|
||||||
|
onTap: leaveAlbum,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOwner) {
|
||||||
|
actions = [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.person_remove_rounded),
|
||||||
|
title: const Text("remove_user").t(context: context),
|
||||||
|
onTap: () => removeUserFromAlbum(user),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
backgroundColor: context.colorScheme.surfaceContainer,
|
||||||
|
isScrollControlled: false,
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 24.0),
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, children: [...actions]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildOwnerInfo() {
|
||||||
|
if (isOwner) {
|
||||||
|
final owner = ref.watch(currentUserProvider);
|
||||||
|
return ListTile(
|
||||||
|
leading: owner != null ? UserCircleAvatar(user: owner) : const SizedBox(),
|
||||||
|
title: Text(album.ownerName, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||||
|
subtitle: Text(owner?.email ?? "", style: TextStyle(color: context.colorScheme.onSurfaceSecondary)),
|
||||||
|
trailing: Text("owner", style: context.textTheme.labelLarge).t(context: context),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final usersProvider = ref.watch(driftUsersProvider);
|
||||||
|
return usersProvider.maybeWhen(
|
||||||
|
data: (users) {
|
||||||
|
final user = users.firstWhereOrNull((u) => u.id == album.ownerId);
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: UserCircleAvatar(user: user, radius: 22),
|
||||||
|
title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||||
|
subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)),
|
||||||
|
trailing: Text("owner", style: context.textTheme.labelLarge).t(context: context),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
orElse: () => const SizedBox(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSharedUsersList() {
|
||||||
|
return sharedUsersAsync.maybeWhen(
|
||||||
|
data: (sharedUsers) => ListView.builder(
|
||||||
|
primary: false,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: sharedUsers.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final user = sharedUsers[index];
|
||||||
|
return ListTile(
|
||||||
|
leading: UserCircleAvatar(user: user, radius: 22),
|
||||||
|
title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||||
|
subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)),
|
||||||
|
trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) : const SizedBox(),
|
||||||
|
onTap: userId == user.id || isOwner ? () => handleUserClick(user) : null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
orElse: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSectionTitle(String text) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Text(text, style: context.textTheme.bodySmall),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||||
|
onPressed: () => context.maybePop(null),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
title: Text("options".t(context: context)),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (isOwner)
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
value: activityEnabled.value,
|
||||||
|
onChanged: (bool value) async {
|
||||||
|
activityEnabled.value = value;
|
||||||
|
await ref.read(remoteAlbumProvider.notifier).setActivityStatus(album.id, value);
|
||||||
|
},
|
||||||
|
activeColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor,
|
||||||
|
dense: true,
|
||||||
|
title: Text(
|
||||||
|
"comments_and_likes",
|
||||||
|
style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||||
|
).t(context: context),
|
||||||
|
subtitle: Text(
|
||||||
|
"let_others_respond",
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
|
).t(context: context),
|
||||||
|
),
|
||||||
|
buildSectionTitle("shared_album_section_people_title".t(context: context)),
|
||||||
|
if (isOwner) ...[
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.person_add_rounded),
|
||||||
|
title: Text("invite_people".t(context: context)),
|
||||||
|
onTap: () async => addUsers(),
|
||||||
|
),
|
||||||
|
const Divider(indent: 16),
|
||||||
|
],
|
||||||
|
buildOwnerInfo(),
|
||||||
|
buildSharedUsersList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -200,6 +200,14 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
context.pop();
|
context.pop();
|
||||||
await showEditTitleAndDescription(context);
|
await showEditTitleAndDescription(context);
|
||||||
},
|
},
|
||||||
|
onCreateSharedLink: () async {
|
||||||
|
context.pop();
|
||||||
|
context.pushRoute(SharedLinkEditRoute(albumId: _album.id));
|
||||||
|
},
|
||||||
|
onShowOptions: () {
|
||||||
|
context.pop();
|
||||||
|
context.pushRoute(const DriftAlbumOptionsRoute());
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class DownloadActionButton extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.download,
|
iconData: Icons.download,
|
||||||
|
maxWidth: 95,
|
||||||
label: "download".t(context: context),
|
label: "download".t(context: context),
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
|
|||||||
+1
-1
@@ -45,7 +45,7 @@ class MoveToLockFolderActionButton extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
maxWidth: 100.0,
|
maxWidth: 115.0,
|
||||||
iconData: Icons.lock_outline_rounded,
|
iconData: Icons.lock_outline_rounded,
|
||||||
label: "move_to_locked_folder".t(context: context),
|
label: "move_to_locked_folder".t(context: context),
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
|
|||||||
+1
@@ -43,6 +43,7 @@ class RemoveFromAlbumActionButton extends ConsumerWidget {
|
|||||||
iconData: Icons.remove_circle_outline,
|
iconData: Icons.remove_circle_outline,
|
||||||
label: "remove_from_album".t(context: context),
|
label: "remove_from_album".t(context: context),
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
|
maxWidth: 100,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ typedef AlbumSelectorCallback = void Function(RemoteAlbum album);
|
|||||||
|
|
||||||
class AlbumSelector extends ConsumerStatefulWidget {
|
class AlbumSelector extends ConsumerStatefulWidget {
|
||||||
final AlbumSelectorCallback onAlbumSelected;
|
final AlbumSelectorCallback onAlbumSelected;
|
||||||
|
final Function? onKeyboardExpanded;
|
||||||
|
|
||||||
const AlbumSelector({super.key, required this.onAlbumSelected});
|
const AlbumSelector({super.key, required this.onAlbumSelected, this.onKeyboardExpanded});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<AlbumSelector> createState() => _AlbumSelectorState();
|
ConsumerState<AlbumSelector> createState() => _AlbumSelectorState();
|
||||||
@@ -52,6 +53,12 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
|||||||
searchController.addListener(() {
|
searchController.addListener(() {
|
||||||
onSearch(searchController.text, filterMode);
|
onSearch(searchController.text, filterMode);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
searchFocusNode.addListener(() {
|
||||||
|
if (searchFocusNode.hasFocus) {
|
||||||
|
widget.onKeyboardExpanded?.call();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void onSearch(String searchTerm, QuickFilterMode sortMode) {
|
void onSearch(String searchTerm, QuickFilterMode sortMode) {
|
||||||
|
|||||||
@@ -147,11 +147,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
// Precache both thumbnail and full image for smooth transitions
|
// Precache both thumbnail and full image for smooth transitions
|
||||||
unawaited(
|
unawaited(
|
||||||
Future.wait([
|
Future.wait([
|
||||||
precacheImage(
|
precacheImage(getThumbnailImageProvider(asset: asset), context, onError: (_, __) {}),
|
||||||
getThumbnailImageProvider(asset: asset, size: screenSize),
|
|
||||||
context,
|
|
||||||
onError: (_, __) {},
|
|
||||||
),
|
|
||||||
precacheImage(getFullImageProvider(asset, size: screenSize), context, onError: (_, __) {}),
|
precacheImage(getFullImageProvider(asset, size: screenSize), context, onError: (_, __) {}),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
@@ -482,7 +478,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
child: Thumbnail(asset: asset, fit: BoxFit.contain, size: Size(ctx.width, ctx.height)),
|
child: Thumbnail(asset: asset, fit: BoxFit.contain),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,7 +509,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PhotoViewGalleryPageOptions _imageBuilder(BuildContext ctx, BaseAsset asset) {
|
PhotoViewGalleryPageOptions _imageBuilder(BuildContext ctx, BaseAsset asset) {
|
||||||
final size = Size(ctx.width, ctx.height);
|
final size = ctx.sizeData;
|
||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
key: ValueKey(asset.heroTag),
|
key: ValueKey(asset.heroTag),
|
||||||
imageProvider: getFullImageProvider(asset, size: size),
|
imageProvider: getFullImageProvider(asset, size: size),
|
||||||
@@ -529,10 +525,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
onTapDown: _onTapDown,
|
onTapDown: _onTapDown,
|
||||||
onLongPressStart: asset.isMotionPhoto ? _onLongPress : null,
|
onLongPressStart: asset.isMotionPhoto ? _onLongPress : null,
|
||||||
errorBuilder: (_, __, ___) => Container(
|
errorBuilder: (_, __, ___) => Container(
|
||||||
width: ctx.width,
|
width: size.width,
|
||||||
height: ctx.height,
|
height: size.height,
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
child: Thumbnail(asset: asset, fit: BoxFit.contain, size: size),
|
child: Thumbnail(asset: asset, fit: BoxFit.contain),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -562,7 +558,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
asset: asset,
|
asset: asset,
|
||||||
image: Image(
|
image: Image(
|
||||||
key: ValueKey(asset),
|
key: ValueKey(asset),
|
||||||
image: getFullImageProvider(asset, size: Size(ctx.width, ctx.height)),
|
image: getFullImageProvider(asset, size: ctx.sizeData),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
height: ctx.height,
|
height: ctx.height,
|
||||||
width: ctx.width,
|
width: ctx.width,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permane
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_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/download_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/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_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/share_link_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/trash_action_button.widget.dart';
|
||||||
@@ -21,6 +22,7 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/she
|
|||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
@@ -42,8 +44,8 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||||
|
|
||||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||||
|
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
||||||
|
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
const ShareActionButton(source: ActionSource.viewer),
|
const ShareActionButton(source: ActionSource.viewer),
|
||||||
@@ -61,6 +63,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
const DeleteLocalActionButton(source: ActionSource.viewer),
|
const DeleteLocalActionButton(source: ActionSource.viewer),
|
||||||
const UploadActionButton(source: ActionSource.timeline),
|
const UploadActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
|
if (currentAlbum != null) RemoveFromAlbumActionButton(albumId: currentAlbum.id, source: ActionSource.viewer),
|
||||||
];
|
];
|
||||||
|
|
||||||
final lockedViewActions = <Widget>[];
|
final lockedViewActions = <Widget>[];
|
||||||
|
|||||||
@@ -25,12 +25,30 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class GeneralBottomSheet extends ConsumerWidget {
|
class GeneralBottomSheet extends ConsumerStatefulWidget {
|
||||||
final double? minChildSize;
|
final double? minChildSize;
|
||||||
const GeneralBottomSheet({super.key, this.minChildSize});
|
const GeneralBottomSheet({super.key, this.minChildSize});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<GeneralBottomSheet> createState() => _GeneralBottomSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||||
|
late DraggableScrollableController sheetController;
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
sheetController = DraggableScrollableController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
sheetController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
final multiselect = ref.watch(multiSelectProvider);
|
final multiselect = ref.watch(multiSelectProvider);
|
||||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||||
|
|
||||||
@@ -59,9 +77,14 @@ class GeneralBottomSheet extends ConsumerWidget {
|
|||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> onKeyboardExpand() {
|
||||||
|
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
||||||
|
}
|
||||||
|
|
||||||
return BaseBottomSheet(
|
return BaseBottomSheet(
|
||||||
|
controller: sheetController,
|
||||||
initialChildSize: 0.45,
|
initialChildSize: 0.45,
|
||||||
minChildSize: minChildSize,
|
minChildSize: widget.minChildSize,
|
||||||
maxChildSize: 0.85,
|
maxChildSize: 0.85,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
@@ -71,26 +94,21 @@ class GeneralBottomSheet extends ConsumerWidget {
|
|||||||
const ArchiveActionButton(source: ActionSource.timeline),
|
const ArchiveActionButton(source: ActionSource.timeline),
|
||||||
const FavoriteActionButton(source: ActionSource.timeline),
|
const FavoriteActionButton(source: ActionSource.timeline),
|
||||||
const DownloadActionButton(source: ActionSource.timeline),
|
const DownloadActionButton(source: ActionSource.timeline),
|
||||||
isTrashEnable
|
|
||||||
? const TrashActionButton(source: ActionSource.timeline)
|
|
||||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
|
||||||
const DeleteActionButton(source: ActionSource.timeline),
|
|
||||||
if (multiselect.hasLocal || multiselect.hasMerged) ...[
|
|
||||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
|
||||||
],
|
|
||||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||||
const EditLocationActionButton(source: ActionSource.timeline),
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||||
const StackActionButton(source: ActionSource.timeline),
|
const StackActionButton(source: ActionSource.timeline),
|
||||||
|
isTrashEnable
|
||||||
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
|
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||||
|
const DeleteActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
if (multiselect.hasLocal) ...[
|
if (multiselect.hasLocal || multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
if (multiselect.hasLocal) const UploadActionButton(source: ActionSource.timeline),
|
||||||
const UploadActionButton(source: ActionSource.timeline),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
slivers: [
|
slivers: [
|
||||||
const AddToAlbumHeader(),
|
const AddToAlbumHeader(),
|
||||||
AlbumSelector(onAlbumSelected: addAssetsToAlbum),
|
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import 'package:immich_mobile/domain/models/setting.model.dart';
|
|||||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||||
|
|
||||||
ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920)}) {
|
ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920)}) {
|
||||||
// Create new provider and cache it
|
// Create new provider and cache it
|
||||||
final ImageProvider provider;
|
final ImageProvider provider;
|
||||||
if (_shouldUseLocalAsset(asset)) {
|
if (_shouldUseLocalAsset(asset)) {
|
||||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
||||||
provider = LocalFullImageProvider(id: id, name: asset.name, size: size, type: asset.type);
|
provider = LocalFullImageProvider(id: id, size: size, type: asset.type, updatedAt: asset.updatedAt);
|
||||||
} else {
|
} else {
|
||||||
final String assetId;
|
final String assetId;
|
||||||
if (asset is LocalAsset && asset.hasRemote) {
|
if (asset is LocalAsset && asset.hasRemote) {
|
||||||
@@ -26,7 +27,7 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080
|
|||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Size size = const Size.square(256)}) {
|
ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Size size = kThumbnailResolution}) {
|
||||||
assert(asset != null || remoteId != null, 'Either asset or remoteId must be provided');
|
assert(asset != null || remoteId != null, 'Either asset or remoteId must be provided');
|
||||||
|
|
||||||
if (remoteId != null) {
|
if (remoteId != null) {
|
||||||
@@ -35,7 +36,7 @@ ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Siz
|
|||||||
|
|
||||||
if (_shouldUseLocalAsset(asset!)) {
|
if (_shouldUseLocalAsset(asset!)) {
|
||||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
||||||
return LocalThumbProvider(id: id, updatedAt: asset.updatedAt, name: asset.name, size: size);
|
return LocalThumbProvider(id: id, updatedAt: asset.updatedAt, size: size);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String assetId;
|
final String assetId;
|
||||||
@@ -52,3 +53,26 @@ ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Siz
|
|||||||
|
|
||||||
bool _shouldUseLocalAsset(BaseAsset asset) =>
|
bool _shouldUseLocalAsset(BaseAsset asset) =>
|
||||||
asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage));
|
asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage));
|
||||||
|
|
||||||
|
ImageInfo? getCachedImage(ImageProvider key) {
|
||||||
|
ImageInfo? thumbnail;
|
||||||
|
final ImageStreamCompleter? stream = PaintingBinding.instance.imageCache.putIfAbsent(
|
||||||
|
key,
|
||||||
|
() => throw Exception(), // don't bother loading if it isn't cached
|
||||||
|
onError: (_, __) {},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stream != null) {
|
||||||
|
void listener(ImageInfo info, bool synchronousCall) {
|
||||||
|
thumbnail = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
stream.addListener(ImageStreamListener(listener));
|
||||||
|
} finally {
|
||||||
|
stream.removeListener(ImageStreamListener(listener));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return thumbnail;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,15 +2,17 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
|
import 'package:immich_mobile/extensions/codec_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||||
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
|
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
|
||||||
import 'package:immich_mobile/providers/image/exceptions/image_loading_exception.dart';
|
import 'package:immich_mobile/providers/image/exceptions/image_loading_exception.dart';
|
||||||
@@ -22,14 +24,12 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
|||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
final String name;
|
|
||||||
final Size size;
|
final Size size;
|
||||||
|
|
||||||
const LocalThumbProvider({
|
const LocalThumbProvider({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
required this.name,
|
this.size = kThumbnailResolution,
|
||||||
this.size = const Size.square(kTimelineFixedTileExtent),
|
|
||||||
this.cacheManager,
|
this.cacheManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,10 +45,8 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
|||||||
codec: _codec(key, cache, decode),
|
codec: _codec(key, cache, decode),
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
informationCollector: () => <DiagnosticsNode>[
|
informationCollector: () => <DiagnosticsNode>[
|
||||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
|
||||||
DiagnosticsProperty<String>('Id', key.id),
|
DiagnosticsProperty<String>('Id', key.id),
|
||||||
DiagnosticsProperty<DateTime>('Updated at', key.updatedAt),
|
DiagnosticsProperty<DateTime>('Updated at', key.updatedAt),
|
||||||
DiagnosticsProperty<String>('Name', key.name),
|
|
||||||
DiagnosticsProperty<Size>('Size', key.size),
|
DiagnosticsProperty<Size>('Size', key.size),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -68,7 +66,7 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
|||||||
final thumbnailBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
|
final thumbnailBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
|
||||||
if (thumbnailBytes == null) {
|
if (thumbnailBytes == null) {
|
||||||
PaintingBinding.instance.imageCache.evict(key);
|
PaintingBinding.instance.imageCache.evict(key);
|
||||||
throw StateError("Loading thumb for local photo ${key.name} failed");
|
throw StateError("Loading thumb for local photo ${key.id} failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes);
|
final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes);
|
||||||
@@ -94,11 +92,11 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
|||||||
final StorageRepository _storageRepository = const StorageRepository();
|
final StorageRepository _storageRepository = const StorageRepository();
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
|
||||||
final Size size;
|
final Size size;
|
||||||
final AssetType type;
|
final AssetType type;
|
||||||
|
final DateTime updatedAt; // temporary, only exists to fetch cached thumbnail until local disk cache is removed
|
||||||
|
|
||||||
const LocalFullImageProvider({required this.id, required this.name, required this.size, required this.type});
|
const LocalFullImageProvider({required this.id, required this.size, required this.type, required this.updatedAt});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
@@ -107,52 +105,45 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) {
|
ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) {
|
||||||
return MultiImageStreamCompleter(
|
return OneFramePlaceholderImageStreamCompleter(
|
||||||
codec: _codec(key, decode),
|
_codec(key, decode),
|
||||||
scale: 1.0,
|
initialImage: getCachedImage(LocalThumbProvider(id: key.id, updatedAt: key.updatedAt)),
|
||||||
informationCollector: () sync* {
|
informationCollector: () => <DiagnosticsNode>[
|
||||||
yield ErrorDescription(name);
|
DiagnosticsProperty<String>('Id', key.id),
|
||||||
},
|
DiagnosticsProperty<DateTime>('Updated at', key.updatedAt),
|
||||||
|
DiagnosticsProperty<Size>('Size', key.size),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Streams in each stage of the image as we ask for it
|
// Streams in each stage of the image as we ask for it
|
||||||
Stream<Codec> _codec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
Stream<ImageInfo> _codec(LocalFullImageProvider key, ImageDecoderCallback decode) {
|
||||||
try {
|
try {
|
||||||
switch (key.type) {
|
return switch (key.type) {
|
||||||
case AssetType.image:
|
AssetType.image => _decodeProgressive(key, decode),
|
||||||
yield* _decodeProgressive(key, decode);
|
AssetType.video => _getThumbnailCodec(key, decode),
|
||||||
break;
|
_ => throw StateError('Unsupported asset type ${key.type}'),
|
||||||
case AssetType.video:
|
};
|
||||||
final codec = await _getThumbnailCodec(key, decode);
|
|
||||||
if (codec == null) {
|
|
||||||
throw StateError("Failed to load preview for ${key.name}");
|
|
||||||
}
|
|
||||||
yield codec;
|
|
||||||
break;
|
|
||||||
case AssetType.other:
|
|
||||||
case AssetType.audio:
|
|
||||||
throw StateError('Unsupported asset type ${key.type}');
|
|
||||||
}
|
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
Logger('ImmichLocalImageProvider').severe('Error loading local image ${key.name}', error, stack);
|
Logger('ImmichLocalImageProvider').severe('Error loading local image ${key.id}', error, stack);
|
||||||
throw const ImageLoadingException('Could not load image from local storage');
|
throw const ImageLoadingException('Could not load image from local storage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Codec?> _getThumbnailCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async {
|
Stream<ImageInfo> _getThumbnailCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||||
final thumbBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
|
final thumbBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
|
||||||
if (thumbBytes == null) {
|
if (thumbBytes == null) {
|
||||||
return null;
|
throw StateError("Failed to load preview for ${key.id}");
|
||||||
}
|
}
|
||||||
final buffer = await ImmutableBuffer.fromUint8List(thumbBytes);
|
final buffer = await ImmutableBuffer.fromUint8List(thumbBytes);
|
||||||
return decode(buffer);
|
final codec = await decode(buffer);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<Codec> _decodeProgressive(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
Stream<ImageInfo> _decodeProgressive(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||||
final file = await _storageRepository.getFileForAsset(key.id);
|
final file = await _storageRepository.getFileForAsset(key.id);
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
throw StateError("Opening file for asset ${key.name} failed");
|
throw StateError("Opening file for asset ${key.id} failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
final fileSize = await file.length();
|
final fileSize = await file.length();
|
||||||
@@ -171,7 +162,8 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
|||||||
final mediumThumb = await _assetMediaRepository.getThumbnail(key.id, size: size);
|
final mediumThumb = await _assetMediaRepository.getThumbnail(key.id, size: size);
|
||||||
if (mediumThumb != null) {
|
if (mediumThumb != null) {
|
||||||
final mediumBuffer = await ImmutableBuffer.fromUint8List(mediumThumb);
|
final mediumBuffer = await ImmutableBuffer.fromUint8List(mediumThumb);
|
||||||
yield await decode(mediumBuffer);
|
final codec = await decode(mediumBuffer);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
@@ -187,24 +179,26 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
|||||||
final highThumb = await _assetMediaRepository.getThumbnail(key.id, size: size);
|
final highThumb = await _assetMediaRepository.getThumbnail(key.id, size: size);
|
||||||
if (highThumb != null) {
|
if (highThumb != null) {
|
||||||
final highBuffer = await ImmutableBuffer.fromUint8List(highThumb);
|
final highBuffer = await ImmutableBuffer.fromUint8List(highThumb);
|
||||||
yield await decode(highBuffer);
|
final codec = await decode(highBuffer);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final buffer = await ImmutableBuffer.fromFilePath(file.path);
|
final buffer = await ImmutableBuffer.fromFilePath(file.path);
|
||||||
yield await decode(buffer);
|
final codec = await decode(buffer);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
if (other is LocalFullImageProvider) {
|
if (other is LocalFullImageProvider) {
|
||||||
return id == other.id && size == other.size && type == other.type && name == other.name;
|
return id == other.id && size == other.size && type == other.type;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => id.hashCode ^ size.hashCode ^ type.hashCode ^ name.hashCode;
|
int get hashCode => id.hashCode ^ size.hashCode ^ type.hashCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// The below code is adapted from cached_network_image package's
|
||||||
|
// MultiImageStreamCompleter to better suit one-frame image loading.
|
||||||
|
// In particular, it allows providing an initial image to emit synchronously.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/painting.dart';
|
||||||
|
|
||||||
|
/// An ImageStreamCompleter with support for loading multiple images.
|
||||||
|
class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter {
|
||||||
|
ImageInfo? _initialImage;
|
||||||
|
|
||||||
|
/// The constructor to create an OneFramePlaceholderImageStreamCompleter. The [images]
|
||||||
|
/// should be the primary images to display (typically asynchronously as they load).
|
||||||
|
/// The [initialImage] is an optional image that will be emitted synchronously
|
||||||
|
/// until the first stream image is completed, useful as a thumbnail or placeholder.
|
||||||
|
OneFramePlaceholderImageStreamCompleter(
|
||||||
|
Stream<ImageInfo> images, {
|
||||||
|
ImageInfo? initialImage,
|
||||||
|
InformationCollector? informationCollector,
|
||||||
|
}) {
|
||||||
|
_initialImage = initialImage;
|
||||||
|
images.listen(
|
||||||
|
_onImage,
|
||||||
|
onError: (Object error, StackTrace stack) {
|
||||||
|
reportError(
|
||||||
|
context: ErrorDescription('resolving a single-frame image stream'),
|
||||||
|
exception: error,
|
||||||
|
stack: stack,
|
||||||
|
informationCollector: informationCollector,
|
||||||
|
silent: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onImage(ImageInfo image) {
|
||||||
|
setImage(image);
|
||||||
|
_initialImage?.dispose();
|
||||||
|
_initialImage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void addListener(ImageStreamListener listener) {
|
||||||
|
final initialImage = _initialImage;
|
||||||
|
if (initialImage != null) {
|
||||||
|
try {
|
||||||
|
listener.onImage(initialImage.clone(), true);
|
||||||
|
} catch (exception, stack) {
|
||||||
|
reportError(
|
||||||
|
context: ErrorDescription('by a synchronously-called image listener'),
|
||||||
|
exception: exception,
|
||||||
|
stack: stack,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.addListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onDisposed() {
|
||||||
|
_initialImage?.dispose();
|
||||||
|
_initialImage = null;
|
||||||
|
super.onDisposed();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
|
import 'package:immich_mobile/extensions/codec_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart';
|
||||||
import 'package:immich_mobile/providers/image/cache/image_loader.dart';
|
import 'package:immich_mobile/providers/image/cache/image_loader.dart';
|
||||||
import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart';
|
import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
@@ -81,36 +83,28 @@ class RemoteFullImageProvider extends ImageProvider<RemoteFullImageProvider> {
|
|||||||
@override
|
@override
|
||||||
ImageStreamCompleter loadImage(RemoteFullImageProvider key, ImageDecoderCallback decode) {
|
ImageStreamCompleter loadImage(RemoteFullImageProvider key, ImageDecoderCallback decode) {
|
||||||
final cache = cacheManager ?? RemoteImageCacheManager();
|
final cache = cacheManager ?? RemoteImageCacheManager();
|
||||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
return OneFramePlaceholderImageStreamCompleter(
|
||||||
return MultiImageStreamCompleter(
|
_codec(key, cache, decode),
|
||||||
codec: _codec(key, cache, decode, chunkEvents),
|
initialImage: getCachedImage(RemoteThumbProvider(assetId: key.assetId)),
|
||||||
scale: 1.0,
|
|
||||||
chunkEvents: chunkEvents.stream,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<Codec> _codec(
|
Stream<ImageInfo> _codec(RemoteFullImageProvider key, CacheManager cache, ImageDecoderCallback decode) async* {
|
||||||
RemoteFullImageProvider key,
|
final codec = await ImageLoader.loadImageFromCache(
|
||||||
CacheManager cache,
|
|
||||||
ImageDecoderCallback decode,
|
|
||||||
StreamController<ImageChunkEvent> chunkController,
|
|
||||||
) async* {
|
|
||||||
yield await ImageLoader.loadImageFromCache(
|
|
||||||
getPreviewUrlForRemoteId(key.assetId),
|
getPreviewUrlForRemoteId(key.assetId),
|
||||||
cache: cache,
|
cache: cache,
|
||||||
decode: decode,
|
decode: decode,
|
||||||
chunkEvents: chunkController,
|
|
||||||
);
|
);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
|
|
||||||
if (AppSetting.get(Setting.loadOriginal)) {
|
if (AppSetting.get(Setting.loadOriginal)) {
|
||||||
yield await ImageLoader.loadImageFromCache(
|
final codec = await ImageLoader.loadImageFromCache(
|
||||||
getOriginalUrlForRemoteId(key.assetId),
|
getOriginalUrlForRemoteId(key.assetId),
|
||||||
cache: cache,
|
cache: cache,
|
||||||
decode: decode,
|
decode: decode,
|
||||||
chunkEvents: chunkController,
|
|
||||||
);
|
);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
}
|
}
|
||||||
await chunkController.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class Thumbnail extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final thumbHash = asset is RemoteAsset ? (asset as RemoteAsset).thumbHash : null;
|
final thumbHash = asset is RemoteAsset ? (asset as RemoteAsset).thumbHash : null;
|
||||||
final provider = getThumbnailImageProvider(asset: asset, remoteId: remoteId, size: size);
|
final provider = getThumbnailImageProvider(asset: asset, remoteId: remoteId);
|
||||||
|
|
||||||
return OctoImage.fromSet(
|
return OctoImage.fromSet(
|
||||||
image: provider,
|
image: provider,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class DriftRemoteAlbumOption extends ConsumerWidget {
|
|||||||
this.onCreateSharedLink,
|
this.onCreateSharedLink,
|
||||||
this.onToggleAlbumOrder,
|
this.onToggleAlbumOrder,
|
||||||
this.onEditAlbum,
|
this.onEditAlbum,
|
||||||
|
this.onShowOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
final VoidCallback? onAddPhotos;
|
final VoidCallback? onAddPhotos;
|
||||||
@@ -22,6 +23,7 @@ class DriftRemoteAlbumOption extends ConsumerWidget {
|
|||||||
final VoidCallback? onCreateSharedLink;
|
final VoidCallback? onCreateSharedLink;
|
||||||
final VoidCallback? onToggleAlbumOrder;
|
final VoidCallback? onToggleAlbumOrder;
|
||||||
final VoidCallback? onEditAlbum;
|
final VoidCallback? onEditAlbum;
|
||||||
|
final VoidCallback? onShowOptions;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -69,6 +71,12 @@ class DriftRemoteAlbumOption extends ConsumerWidget {
|
|||||||
title: Text('create_shared_link'.t(context: context), style: textStyle),
|
title: Text('create_shared_link'.t(context: context), style: textStyle),
|
||||||
onTap: onCreateSharedLink,
|
onTap: onCreateSharedLink,
|
||||||
),
|
),
|
||||||
|
if (onShowOptions != null)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.settings),
|
||||||
|
title: Text('options'.t(context: context), style: textStyle),
|
||||||
|
onTap: onShowOptions,
|
||||||
|
),
|
||||||
if (onDeleteAlbum != null) ...[
|
if (onDeleteAlbum != null) ...[
|
||||||
const Divider(indent: 16, endIndent: 16),
|
const Divider(indent: 16, endIndent: 16),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
const double kTimelineHeaderExtent = 80.0;
|
const double kTimelineHeaderExtent = 80.0;
|
||||||
const double kTimelineFixedTileExtent = 256;
|
const Size kTimelineFixedTileExtent = Size.square(256);
|
||||||
|
const Size kThumbnailResolution = kTimelineFixedTileExtent;
|
||||||
const double kTimelineSpacing = 2.0;
|
const double kTimelineSpacing = 2.0;
|
||||||
const int kTimelineColumnCount = 3;
|
const int kTimelineColumnCount = 3;
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ abstract class SegmentBuilder {
|
|||||||
static Widget buildPlaceholder(
|
static Widget buildPlaceholder(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
int count, {
|
int count, {
|
||||||
Size size = const Size.square(kTimelineFixedTileExtent),
|
Size size = kTimelineFixedTileExtent,
|
||||||
double spacing = kTimelineSpacing,
|
double spacing = kTimelineSpacing,
|
||||||
}) => RepaintBoundary(
|
}) => RepaintBoundary(
|
||||||
child: FixedTimelineRow(
|
child: FixedTimelineRow(
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import 'package:immich_mobile/providers/tab.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/backgroundLegacy.service.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|||||||
@@ -121,7 +121,9 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
Future<bool> saveAuthInfo({required String accessToken}) async {
|
Future<bool> saveAuthInfo({required String accessToken}) async {
|
||||||
await _apiService.setAccessToken(accessToken);
|
await _apiService.setAccessToken(accessToken);
|
||||||
|
|
||||||
await _widgetService.writeCredentials(Store.get(StoreKey.serverEndpoint), accessToken);
|
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||||
|
final customHeaders = Store.tryGet(StoreKey.customHeaders);
|
||||||
|
await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders);
|
||||||
|
|
||||||
// Get the deviceid from the store if it exists, otherwise generate a new one
|
// Get the deviceid from the store if it exists, otherwise generate a new one
|
||||||
String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
|
String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
|||||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/backgroundLegacy.service.dart';
|
||||||
import 'package:immich_mobile/services/backup.service.dart';
|
import 'package:immich_mobile/services/backup.service.dart';
|
||||||
import 'package:immich_mobile/services/backup_album.service.dart';
|
import 'package:immich_mobile/services/backup_album.service.dart';
|
||||||
import 'package:immich_mobile/services/server_info.service.dart';
|
import 'package:immich_mobile/services/server_info.service.dart';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/backgroundLegacy.service.dart';
|
||||||
|
|
||||||
class IOSBackgroundSettings {
|
class IOSBackgroundSettings {
|
||||||
final bool appRefreshEnabled;
|
final bool appRefreshEnabled;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/backgroundLegacy.service.dart';
|
||||||
import 'package:immich_mobile/services/backup.service.dart';
|
import 'package:immich_mobile/services/backup.service.dart';
|
||||||
import 'package:immich_mobile/services/backup_album.service.dart';
|
import 'package:immich_mobile/services/backup_album.service.dart';
|
||||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||||
|
|||||||
@@ -156,6 +156,23 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
|||||||
Future<void> addUsers(String albumId, List<String> userIds) {
|
Future<void> addUsers(String albumId, List<String> userIds) {
|
||||||
return _remoteAlbumService.addUsers(albumId: albumId, userIds: userIds);
|
return _remoteAlbumService.addUsers(albumId: albumId, userIds: userIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> removeUser(String albumId, String userId) {
|
||||||
|
return _remoteAlbumService.removeUser(albumId, userId: userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> leaveAlbum(String albumId, {required String userId}) async {
|
||||||
|
await _remoteAlbumService.removeUser(albumId, userId: userId);
|
||||||
|
|
||||||
|
final updatedAlbums = state.albums.where((album) => album.id != albumId).toList();
|
||||||
|
final updatedFilteredAlbums = state.filteredAlbums.where((album) => album.id != albumId).toList();
|
||||||
|
|
||||||
|
state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setActivityStatus(String albumId, bool enabled) {
|
||||||
|
return _remoteAlbumService.setActivityStatus(albumId, enabled);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final remoteAlbumDateRangeProvider = FutureProvider.family<(DateTime, DateTime), String>((ref, albumId) async {
|
final remoteAlbumDateRangeProvider = FutureProvider.family<(DateTime, DateTime), String>((ref, albumId) async {
|
||||||
|
|||||||
@@ -87,6 +87,15 @@ class DriftAlbumApiRepository extends ApiRepository {
|
|||||||
final response = await checkNull(_api.addUsersToAlbum(albumId, AddUsersDto(albumUsers: albumUsers)));
|
final response = await checkNull(_api.addUsersToAlbum(albumId, AddUsersDto(albumUsers: albumUsers)));
|
||||||
return response.toRemoteAlbum();
|
return response.toRemoteAlbum();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> removeUser(String albumId, {required String userId}) async {
|
||||||
|
await _api.removeUserFromAlbum(albumId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setActivityStatus(String albumId, bool isEnabled) async {
|
||||||
|
final response = await checkNull(_api.updateAlbumInfo(albumId, UpdateAlbumDto(isActivityEnabled: isEnabled)));
|
||||||
|
return response.isActivityEnabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on AlbumResponseDto {
|
extension on AlbumResponseDto {
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ import 'package:immich_mobile/pages/album/album_shared_user_selection.page.dart'
|
|||||||
import 'package:immich_mobile/pages/album/album_viewer.page.dart';
|
import 'package:immich_mobile/pages/album/album_viewer.page.dart';
|
||||||
import 'package:immich_mobile/pages/albums/albums.page.dart';
|
import 'package:immich_mobile/pages/albums/albums.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/album_preview.page.dart';
|
import 'package:immich_mobile/pages/backup/album_preview.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/drift_backup_album_selection.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/backup/drift_backup.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
|
import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/backup/drift_backup.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/backup/drift_backup_album_selection.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/drift_backup_options.page.dart';
|
import 'package:immich_mobile/pages/backup/drift_backup_options.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/drift_upload_detail.page.dart';
|
import 'package:immich_mobile/pages/backup/drift_upload_detail.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
||||||
@@ -81,6 +81,7 @@ import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.da
|
|||||||
import 'package:immich_mobile/presentation/pages/dev/main_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/media_stat.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/drift_album_options.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_archive.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_archive.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.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_create_album.page.dart';
|
||||||
@@ -95,9 +96,9 @@ import 'package:immich_mobile/presentation/pages/drift_person.page.dart';
|
|||||||
import 'package:immich_mobile/presentation/pages/drift_place.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_place.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_place_detail.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_place_detail.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
|
|
||||||
import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_trash.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_trash.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_video.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_video.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/local_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/local_timeline.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart';
|
import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart';
|
||||||
@@ -329,6 +330,7 @@ class AppRouter extends RootStackRouter {
|
|||||||
AutoRoute(page: DriftPeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: DriftPeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: DriftPersonRoute.page, guards: [_authGuard]),
|
AutoRoute(page: DriftPersonRoute.page, guards: [_authGuard]),
|
||||||
AutoRoute(page: DriftBackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: DriftBackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
|
AutoRoute(page: DriftAlbumOptionsRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
// required to handle all deeplinks in deep_link.service.dart
|
// required to handle all deeplinks in deep_link.service.dart
|
||||||
// auto_route_library#1722
|
// auto_route_library#1722
|
||||||
RedirectRoute(path: '*', redirectTo: '/'),
|
RedirectRoute(path: '*', redirectTo: '/'),
|
||||||
|
|||||||
@@ -667,6 +667,22 @@ class CropImageRouteArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftAlbumOptionsPage]
|
||||||
|
class DriftAlbumOptionsRoute extends PageRouteInfo<void> {
|
||||||
|
const DriftAlbumOptionsRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DriftAlbumOptionsRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DriftAlbumOptionsRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DriftAlbumOptionsPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [DriftAlbumsPage]
|
/// [DriftAlbumsPage]
|
||||||
class DriftAlbumsRoute extends PageRouteInfo<void> {
|
class DriftAlbumsRoute extends PageRouteInfo<void> {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:immich_mobile/domain/models/device_asset.model.dart';
|
|||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/device_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/device_asset.provider.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/backgroundLegacy.service.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
class HashService {
|
class HashService {
|
||||||
|
|||||||
@@ -11,11 +11,15 @@ class WidgetService {
|
|||||||
|
|
||||||
const WidgetService(this._repository);
|
const WidgetService(this._repository);
|
||||||
|
|
||||||
Future<void> writeCredentials(String serverURL, String sessionKey) async {
|
Future<void> writeCredentials(String serverURL, String sessionKey, String? customHeaders) async {
|
||||||
await _repository.setAppGroupId(appShareGroupId);
|
await _repository.setAppGroupId(appShareGroupId);
|
||||||
await _repository.saveData(kWidgetServerEndpoint, serverURL);
|
await _repository.saveData(kWidgetServerEndpoint, serverURL);
|
||||||
await _repository.saveData(kWidgetAuthToken, sessionKey);
|
await _repository.saveData(kWidgetAuthToken, sessionKey);
|
||||||
|
|
||||||
|
if (customHeaders != null && customHeaders.isNotEmpty) {
|
||||||
|
await _repository.saveData(kWidgetCustomHeaders, customHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
// wait 3 seconds to ensure the widget is updated, dont block
|
// wait 3 seconds to ensure the widget is updated, dont block
|
||||||
Future.delayed(const Duration(seconds: 3), refreshWidgets);
|
Future.delayed(const Duration(seconds: 3), refreshWidgets);
|
||||||
}
|
}
|
||||||
@@ -24,6 +28,7 @@ class WidgetService {
|
|||||||
await _repository.setAppGroupId(appShareGroupId);
|
await _repository.setAppGroupId(appShareGroupId);
|
||||||
await _repository.saveData(kWidgetServerEndpoint, "");
|
await _repository.saveData(kWidgetServerEndpoint, "");
|
||||||
await _repository.saveData(kWidgetAuthToken, "");
|
await _repository.saveData(kWidgetAuthToken, "");
|
||||||
|
await _repository.saveData(kWidgetCustomHeaders, "");
|
||||||
|
|
||||||
// wait 3 seconds to ensure the widget is updated, dont block
|
// wait 3 seconds to ensure the widget is updated, dont block
|
||||||
Future.delayed(const Duration(seconds: 3), refreshWidgets);
|
Future.delayed(const Duration(seconds: 3), refreshWidgets);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
||||||
|
|
||||||
class RemoteAlbumSharedUserIcons extends ConsumerWidget {
|
class RemoteAlbumSharedUserIcons extends ConsumerWidget {
|
||||||
@@ -22,17 +24,20 @@ class RemoteAlbumSharedUserIcons extends ConsumerWidget {
|
|||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
return SizedBox(
|
return GestureDetector(
|
||||||
height: 50,
|
onTap: () => context.pushRoute(const DriftAlbumOptionsRoute()),
|
||||||
child: ListView.builder(
|
child: SizedBox(
|
||||||
scrollDirection: Axis.horizontal,
|
height: 50,
|
||||||
itemBuilder: ((context, index) {
|
child: ListView.builder(
|
||||||
return Padding(
|
scrollDirection: Axis.horizontal,
|
||||||
padding: const EdgeInsets.only(right: 4.0),
|
itemBuilder: ((context, index) {
|
||||||
child: UserCircleAvatar(user: sharedUsers[index], radius: 18, size: 36, hasBorder: true),
|
return Padding(
|
||||||
);
|
padding: const EdgeInsets.only(right: 4.0),
|
||||||
}),
|
child: UserCircleAvatar(user: sharedUsers[index], radius: 18, size: 36, hasBorder: true),
|
||||||
itemCount: sharedUsers.length,
|
);
|
||||||
|
}),
|
||||||
|
itemCount: sharedUsers.length,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+4
-1
@@ -477,7 +477,9 @@ class ServerApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /server/version-check' operation and returns the [Response].
|
/// This endpoint requires the `server.versionCheck` permission.
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
Future<Response> getVersionCheckWithHttpInfo() async {
|
Future<Response> getVersionCheckWithHttpInfo() async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/server/version-check';
|
final apiPath = r'/server/version-check';
|
||||||
@@ -503,6 +505,7 @@ class ServerApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This endpoint requires the `server.versionCheck` permission.
|
||||||
Future<VersionCheckStateResponseDto?> getVersionCheck() async {
|
Future<VersionCheckStateResponseDto?> getVersionCheck() async {
|
||||||
final response = await getVersionCheckWithHttpInfo();
|
final response = await getVersionCheckWithHttpInfo();
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
|||||||
Generated
+3
@@ -101,6 +101,7 @@ class Permission {
|
|||||||
static const serverPeriodApkLinks = Permission._(r'server.apkLinks');
|
static const serverPeriodApkLinks = Permission._(r'server.apkLinks');
|
||||||
static const serverPeriodStorage = Permission._(r'server.storage');
|
static const serverPeriodStorage = Permission._(r'server.storage');
|
||||||
static const serverPeriodStatistics = Permission._(r'server.statistics');
|
static const serverPeriodStatistics = Permission._(r'server.statistics');
|
||||||
|
static const serverPeriodVersionCheck = Permission._(r'server.versionCheck');
|
||||||
static const serverLicensePeriodRead = Permission._(r'serverLicense.read');
|
static const serverLicensePeriodRead = Permission._(r'serverLicense.read');
|
||||||
static const serverLicensePeriodUpdate = Permission._(r'serverLicense.update');
|
static const serverLicensePeriodUpdate = Permission._(r'serverLicense.update');
|
||||||
static const serverLicensePeriodDelete = Permission._(r'serverLicense.delete');
|
static const serverLicensePeriodDelete = Permission._(r'serverLicense.delete');
|
||||||
@@ -230,6 +231,7 @@ class Permission {
|
|||||||
serverPeriodApkLinks,
|
serverPeriodApkLinks,
|
||||||
serverPeriodStorage,
|
serverPeriodStorage,
|
||||||
serverPeriodStatistics,
|
serverPeriodStatistics,
|
||||||
|
serverPeriodVersionCheck,
|
||||||
serverLicensePeriodRead,
|
serverLicensePeriodRead,
|
||||||
serverLicensePeriodUpdate,
|
serverLicensePeriodUpdate,
|
||||||
serverLicensePeriodDelete,
|
serverLicensePeriodDelete,
|
||||||
@@ -394,6 +396,7 @@ class PermissionTypeTransformer {
|
|||||||
case r'server.apkLinks': return Permission.serverPeriodApkLinks;
|
case r'server.apkLinks': return Permission.serverPeriodApkLinks;
|
||||||
case r'server.storage': return Permission.serverPeriodStorage;
|
case r'server.storage': return Permission.serverPeriodStorage;
|
||||||
case r'server.statistics': return Permission.serverPeriodStatistics;
|
case r'server.statistics': return Permission.serverPeriodStatistics;
|
||||||
|
case r'server.versionCheck': return Permission.serverPeriodVersionCheck;
|
||||||
case r'serverLicense.read': return Permission.serverLicensePeriodRead;
|
case r'serverLicense.read': return Permission.serverLicensePeriodRead;
|
||||||
case r'serverLicense.update': return Permission.serverLicensePeriodUpdate;
|
case r'serverLicense.update': return Permission.serverLicensePeriodUpdate;
|
||||||
case r'serverLicense.delete': return Permission.serverLicensePeriodDelete;
|
case r'serverLicense.delete': return Permission.serverLicensePeriodDelete;
|
||||||
|
|||||||
+12
-6
@@ -44,9 +44,11 @@ class SyncEntityType {
|
|||||||
static const albumUserV1 = SyncEntityType._(r'AlbumUserV1');
|
static const albumUserV1 = SyncEntityType._(r'AlbumUserV1');
|
||||||
static const albumUserBackfillV1 = SyncEntityType._(r'AlbumUserBackfillV1');
|
static const albumUserBackfillV1 = SyncEntityType._(r'AlbumUserBackfillV1');
|
||||||
static const albumUserDeleteV1 = SyncEntityType._(r'AlbumUserDeleteV1');
|
static const albumUserDeleteV1 = SyncEntityType._(r'AlbumUserDeleteV1');
|
||||||
static const albumAssetV1 = SyncEntityType._(r'AlbumAssetV1');
|
static const albumAssetCreateV1 = SyncEntityType._(r'AlbumAssetCreateV1');
|
||||||
|
static const albumAssetUpdateV1 = SyncEntityType._(r'AlbumAssetUpdateV1');
|
||||||
static const albumAssetBackfillV1 = SyncEntityType._(r'AlbumAssetBackfillV1');
|
static const albumAssetBackfillV1 = SyncEntityType._(r'AlbumAssetBackfillV1');
|
||||||
static const albumAssetExifV1 = SyncEntityType._(r'AlbumAssetExifV1');
|
static const albumAssetExifCreateV1 = SyncEntityType._(r'AlbumAssetExifCreateV1');
|
||||||
|
static const albumAssetExifUpdateV1 = SyncEntityType._(r'AlbumAssetExifUpdateV1');
|
||||||
static const albumAssetExifBackfillV1 = SyncEntityType._(r'AlbumAssetExifBackfillV1');
|
static const albumAssetExifBackfillV1 = SyncEntityType._(r'AlbumAssetExifBackfillV1');
|
||||||
static const albumToAssetV1 = SyncEntityType._(r'AlbumToAssetV1');
|
static const albumToAssetV1 = SyncEntityType._(r'AlbumToAssetV1');
|
||||||
static const albumToAssetDeleteV1 = SyncEntityType._(r'AlbumToAssetDeleteV1');
|
static const albumToAssetDeleteV1 = SyncEntityType._(r'AlbumToAssetDeleteV1');
|
||||||
@@ -89,9 +91,11 @@ class SyncEntityType {
|
|||||||
albumUserV1,
|
albumUserV1,
|
||||||
albumUserBackfillV1,
|
albumUserBackfillV1,
|
||||||
albumUserDeleteV1,
|
albumUserDeleteV1,
|
||||||
albumAssetV1,
|
albumAssetCreateV1,
|
||||||
|
albumAssetUpdateV1,
|
||||||
albumAssetBackfillV1,
|
albumAssetBackfillV1,
|
||||||
albumAssetExifV1,
|
albumAssetExifCreateV1,
|
||||||
|
albumAssetExifUpdateV1,
|
||||||
albumAssetExifBackfillV1,
|
albumAssetExifBackfillV1,
|
||||||
albumToAssetV1,
|
albumToAssetV1,
|
||||||
albumToAssetDeleteV1,
|
albumToAssetDeleteV1,
|
||||||
@@ -169,9 +173,11 @@ class SyncEntityTypeTypeTransformer {
|
|||||||
case r'AlbumUserV1': return SyncEntityType.albumUserV1;
|
case r'AlbumUserV1': return SyncEntityType.albumUserV1;
|
||||||
case r'AlbumUserBackfillV1': return SyncEntityType.albumUserBackfillV1;
|
case r'AlbumUserBackfillV1': return SyncEntityType.albumUserBackfillV1;
|
||||||
case r'AlbumUserDeleteV1': return SyncEntityType.albumUserDeleteV1;
|
case r'AlbumUserDeleteV1': return SyncEntityType.albumUserDeleteV1;
|
||||||
case r'AlbumAssetV1': return SyncEntityType.albumAssetV1;
|
case r'AlbumAssetCreateV1': return SyncEntityType.albumAssetCreateV1;
|
||||||
|
case r'AlbumAssetUpdateV1': return SyncEntityType.albumAssetUpdateV1;
|
||||||
case r'AlbumAssetBackfillV1': return SyncEntityType.albumAssetBackfillV1;
|
case r'AlbumAssetBackfillV1': return SyncEntityType.albumAssetBackfillV1;
|
||||||
case r'AlbumAssetExifV1': return SyncEntityType.albumAssetExifV1;
|
case r'AlbumAssetExifCreateV1': return SyncEntityType.albumAssetExifCreateV1;
|
||||||
|
case r'AlbumAssetExifUpdateV1': return SyncEntityType.albumAssetExifUpdateV1;
|
||||||
case r'AlbumAssetExifBackfillV1': return SyncEntityType.albumAssetExifBackfillV1;
|
case r'AlbumAssetExifBackfillV1': return SyncEntityType.albumAssetExifBackfillV1;
|
||||||
case r'AlbumToAssetV1': return SyncEntityType.albumToAssetV1;
|
case r'AlbumToAssetV1': return SyncEntityType.albumToAssetV1;
|
||||||
case r'AlbumToAssetDeleteV1': return SyncEntityType.albumToAssetDeleteV1;
|
case r'AlbumToAssetDeleteV1': return SyncEntityType.albumToAssetDeleteV1;
|
||||||
|
|||||||
+2
-2
@@ -1416,10 +1416,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: pigeon
|
name: pigeon
|
||||||
sha256: a093af76026160bb5ff6eb98e3e678a301ffd1001ac0d90be558bc133a0c73f5
|
sha256: b65acb352dc5a5f8615d074a83419388cbcc249f07c6d8c78b5bc16680a55dda
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "25.3.2"
|
version: "26.0.0"
|
||||||
pinput:
|
pinput:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
+1
-1
@@ -116,7 +116,7 @@ dev_dependencies:
|
|||||||
# Drift generator
|
# Drift generator
|
||||||
drift_dev: ^2.23.1
|
drift_dev: ^2.23.1
|
||||||
# Type safe platform code
|
# Type safe platform code
|
||||||
pigeon: ^25.3.1
|
pigeon: ^26.0.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/backgroundLegacy.service.dart';
|
||||||
import 'package:immich_mobile/services/backup.service.dart';
|
import 'package:immich_mobile/services/backup.service.dart';
|
||||||
import 'package:immich_mobile/services/entity.service.dart';
|
import 'package:immich_mobile/services/entity.service.dart';
|
||||||
import 'package:immich_mobile/services/hash.service.dart';
|
import 'package:immich_mobile/services/hash.service.dart';
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:immich_mobile/domain/models/device_asset.model.dart';
|
import 'package:immich_mobile/domain/models/device_asset.model.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/backgroundLegacy.service.dart';
|
||||||
import 'package:immich_mobile/services/hash.service.dart';
|
import 'package:immich_mobile/services/hash.service.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|||||||
@@ -6506,7 +6506,9 @@
|
|||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"Server"
|
"Server"
|
||||||
]
|
],
|
||||||
|
"x-immich-permission": "server.versionCheck",
|
||||||
|
"description": "This endpoint requires the `server.versionCheck` permission."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/server/version-history": {
|
"/server/version-history": {
|
||||||
@@ -12631,6 +12633,7 @@
|
|||||||
"server.apkLinks",
|
"server.apkLinks",
|
||||||
"server.storage",
|
"server.storage",
|
||||||
"server.statistics",
|
"server.statistics",
|
||||||
|
"server.versionCheck",
|
||||||
"serverLicense.read",
|
"serverLicense.read",
|
||||||
"serverLicense.update",
|
"serverLicense.update",
|
||||||
"serverLicense.delete",
|
"serverLicense.delete",
|
||||||
@@ -14941,9 +14944,11 @@
|
|||||||
"AlbumUserV1",
|
"AlbumUserV1",
|
||||||
"AlbumUserBackfillV1",
|
"AlbumUserBackfillV1",
|
||||||
"AlbumUserDeleteV1",
|
"AlbumUserDeleteV1",
|
||||||
"AlbumAssetV1",
|
"AlbumAssetCreateV1",
|
||||||
|
"AlbumAssetUpdateV1",
|
||||||
"AlbumAssetBackfillV1",
|
"AlbumAssetBackfillV1",
|
||||||
"AlbumAssetExifV1",
|
"AlbumAssetExifCreateV1",
|
||||||
|
"AlbumAssetExifUpdateV1",
|
||||||
"AlbumAssetExifBackfillV1",
|
"AlbumAssetExifBackfillV1",
|
||||||
"AlbumToAssetV1",
|
"AlbumToAssetV1",
|
||||||
"AlbumToAssetDeleteV1",
|
"AlbumToAssetDeleteV1",
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
22.17.1
|
22.18.0
|
||||||
|
|||||||
+4
-4
@@ -12,7 +12,7 @@
|
|||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.16.5",
|
"version": "22.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.0.tgz",
|
||||||
"integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==",
|
"integrity": "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -28,6 +28,6 @@
|
|||||||
"directory": "open-api/typescript-sdk"
|
"directory": "open-api/typescript-sdk"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.17.1"
|
"node": "22.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3552,6 +3552,9 @@ export function getServerVersion(opts?: Oazapfts.RequestOpts) {
|
|||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This endpoint requires the `server.versionCheck` permission.
|
||||||
|
*/
|
||||||
export function getVersionCheck(opts?: Oazapfts.RequestOpts) {
|
export function getVersionCheck(opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
@@ -4616,6 +4619,7 @@ export enum Permission {
|
|||||||
ServerApkLinks = "server.apkLinks",
|
ServerApkLinks = "server.apkLinks",
|
||||||
ServerStorage = "server.storage",
|
ServerStorage = "server.storage",
|
||||||
ServerStatistics = "server.statistics",
|
ServerStatistics = "server.statistics",
|
||||||
|
ServerVersionCheck = "server.versionCheck",
|
||||||
ServerLicenseRead = "serverLicense.read",
|
ServerLicenseRead = "serverLicense.read",
|
||||||
ServerLicenseUpdate = "serverLicense.update",
|
ServerLicenseUpdate = "serverLicense.update",
|
||||||
ServerLicenseDelete = "serverLicense.delete",
|
ServerLicenseDelete = "serverLicense.delete",
|
||||||
@@ -4766,9 +4770,11 @@ export enum SyncEntityType {
|
|||||||
AlbumUserV1 = "AlbumUserV1",
|
AlbumUserV1 = "AlbumUserV1",
|
||||||
AlbumUserBackfillV1 = "AlbumUserBackfillV1",
|
AlbumUserBackfillV1 = "AlbumUserBackfillV1",
|
||||||
AlbumUserDeleteV1 = "AlbumUserDeleteV1",
|
AlbumUserDeleteV1 = "AlbumUserDeleteV1",
|
||||||
AlbumAssetV1 = "AlbumAssetV1",
|
AlbumAssetCreateV1 = "AlbumAssetCreateV1",
|
||||||
|
AlbumAssetUpdateV1 = "AlbumAssetUpdateV1",
|
||||||
AlbumAssetBackfillV1 = "AlbumAssetBackfillV1",
|
AlbumAssetBackfillV1 = "AlbumAssetBackfillV1",
|
||||||
AlbumAssetExifV1 = "AlbumAssetExifV1",
|
AlbumAssetExifCreateV1 = "AlbumAssetExifCreateV1",
|
||||||
|
AlbumAssetExifUpdateV1 = "AlbumAssetExifUpdateV1",
|
||||||
AlbumAssetExifBackfillV1 = "AlbumAssetExifBackfillV1",
|
AlbumAssetExifBackfillV1 = "AlbumAssetExifBackfillV1",
|
||||||
AlbumToAssetV1 = "AlbumToAssetV1",
|
AlbumToAssetV1 = "AlbumToAssetV1",
|
||||||
AlbumToAssetDeleteV1 = "AlbumToAssetDeleteV1",
|
AlbumToAssetDeleteV1 = "AlbumToAssetDeleteV1",
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
22.17.1
|
22.18.0
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
# dev build
|
# dev build
|
||||||
FROM ghcr.io/immich-app/base-server-dev:202507162011@sha256:85d4230c2208646bd6c528db41b2213d780b11b7a311397ca6a2aaba7cf697c8 AS dev
|
FROM ghcr.io/immich-app/base-server-dev:202507291116@sha256:e38543bdd77a02ed156cd9175ed11e9c16dccf48c418d46ecda48ce684de456a AS dev
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY ./server/package* ./server/
|
COPY ./server/package* ./server/
|
||||||
@@ -96,7 +96,7 @@ WORKDIR /usr/src/app/web
|
|||||||
RUN npm ci && npm run build
|
RUN npm ci && npm run build
|
||||||
|
|
||||||
# prod build
|
# prod build
|
||||||
FROM ghcr.io/immich-app/base-server-prod:202507162011@sha256:636f3ddb6106628ef851d51c23f3fa2c6e4829390cc315b27b38c288c82b23a7
|
FROM ghcr.io/immich-app/base-server-prod:202507291116@sha256:6e80f884c6e4f05cefe4b4fc4cc06a15bdb6ec9bd7b6e9eadf996a13b69494b6
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
|
|||||||
Generated
+418
-656
File diff suppressed because it is too large
Load Diff
+5
-4
@@ -110,6 +110,7 @@
|
|||||||
"thumbhash": "^0.1.1",
|
"thumbhash": "^0.1.1",
|
||||||
"typeorm": "^0.3.17",
|
"typeorm": "^0.3.17",
|
||||||
"ua-parser-js": "^2.0.0",
|
"ua-parser-js": "^2.0.0",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
"validator": "^13.12.0"
|
"validator": "^13.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -123,7 +124,7 @@
|
|||||||
"@testcontainers/redis": "^11.0.0",
|
"@testcontainers/redis": "^11.0.0",
|
||||||
"@types/archiver": "^6.0.0",
|
"@types/archiver": "^6.0.0",
|
||||||
"@types/async-lock": "^1.4.2",
|
"@types/async-lock": "^1.4.2",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/body-parser": "^1.19.6",
|
"@types/body-parser": "^1.19.6",
|
||||||
"@types/compression": "^1.7.5",
|
"@types/compression": "^1.7.5",
|
||||||
"@types/cookie-parser": "^1.4.8",
|
"@types/cookie-parser": "^1.4.8",
|
||||||
@@ -134,7 +135,7 @@
|
|||||||
"@types/luxon": "^3.6.2",
|
"@types/luxon": "^3.6.2",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/picomatch": "^4.0.0",
|
"@types/picomatch": "^4.0.0",
|
||||||
"@types/pngjs": "^6.0.5",
|
"@types/pngjs": "^6.0.5",
|
||||||
@@ -149,7 +150,7 @@
|
|||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^59.0.0",
|
"eslint-plugin-unicorn": "^60.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"node-addon-api": "^8.3.1",
|
"node-addon-api": "^8.3.1",
|
||||||
@@ -172,7 +173,7 @@
|
|||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.17.1"
|
"node": "22.18.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"sharp": "^0.34.2"
|
"sharp": "^0.34.2"
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export class ServerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get('version-check')
|
@Get('version-check')
|
||||||
@Authenticated()
|
@Authenticated({ permission: Permission.ServerVersionCheck })
|
||||||
getVersionCheck(): Promise<VersionCheckStateResponseDto> {
|
getVersionCheck(): Promise<VersionCheckStateResponseDto> {
|
||||||
return this.systemMetadataService.getVersionCheckState();
|
return this.systemMetadataService.getVersionCheckState();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,7 +202,6 @@ export type Album = Selectable<AlbumTable> & {
|
|||||||
|
|
||||||
export type AuthSession = {
|
export type AuthSession = {
|
||||||
id: string;
|
id: string;
|
||||||
isPendingSyncReset: boolean;
|
|
||||||
hasElevatedPermission: boolean;
|
hasElevatedPermission: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -309,7 +308,7 @@ export const columns = {
|
|||||||
assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'],
|
assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'],
|
||||||
authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'],
|
authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'],
|
||||||
authApiKey: ['api_key.id', 'api_key.permissions'],
|
authApiKey: ['api_key.id', 'api_key.permissions'],
|
||||||
authSession: ['session.id', 'session.isPendingSyncReset', 'session.updatedAt', 'session.pinExpiresAt'],
|
authSession: ['session.id', 'session.updatedAt', 'session.pinExpiresAt'],
|
||||||
authSharedLink: [
|
authSharedLink: [
|
||||||
'shared_link.id',
|
'shared_link.id',
|
||||||
'shared_link.userId',
|
'shared_link.userId',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IsInt, IsPositive, IsString } from 'class-validator';
|
import { Equals, IsInt, IsPositive, IsString } from 'class-validator';
|
||||||
import { Session } from 'src/database';
|
import { Session } from 'src/database';
|
||||||
import { Optional, ValidateBoolean } from 'src/validation';
|
import { Optional, ValidateBoolean } from 'src/validation';
|
||||||
|
|
||||||
@@ -22,7 +22,8 @@ export class SessionCreateDto {
|
|||||||
|
|
||||||
export class SessionUpdateDto {
|
export class SessionUpdateDto {
|
||||||
@ValidateBoolean({ optional: true })
|
@ValidateBoolean({ optional: true })
|
||||||
isPendingSyncReset?: boolean;
|
@Equals(true)
|
||||||
|
isPendingSyncReset?: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SessionResponseDto {
|
export class SessionResponseDto {
|
||||||
|
|||||||
@@ -339,9 +339,11 @@ export type SyncItem = {
|
|||||||
[SyncEntityType.AlbumUserV1]: SyncAlbumUserV1;
|
[SyncEntityType.AlbumUserV1]: SyncAlbumUserV1;
|
||||||
[SyncEntityType.AlbumUserBackfillV1]: SyncAlbumUserV1;
|
[SyncEntityType.AlbumUserBackfillV1]: SyncAlbumUserV1;
|
||||||
[SyncEntityType.AlbumUserDeleteV1]: SyncAlbumUserDeleteV1;
|
[SyncEntityType.AlbumUserDeleteV1]: SyncAlbumUserDeleteV1;
|
||||||
[SyncEntityType.AlbumAssetV1]: SyncAssetV1;
|
[SyncEntityType.AlbumAssetCreateV1]: SyncAssetV1;
|
||||||
|
[SyncEntityType.AlbumAssetUpdateV1]: SyncAssetV1;
|
||||||
[SyncEntityType.AlbumAssetBackfillV1]: SyncAssetV1;
|
[SyncEntityType.AlbumAssetBackfillV1]: SyncAssetV1;
|
||||||
[SyncEntityType.AlbumAssetExifV1]: SyncAssetExifV1;
|
[SyncEntityType.AlbumAssetExifCreateV1]: SyncAssetExifV1;
|
||||||
|
[SyncEntityType.AlbumAssetExifUpdateV1]: SyncAssetExifV1;
|
||||||
[SyncEntityType.AlbumAssetExifBackfillV1]: SyncAssetExifV1;
|
[SyncEntityType.AlbumAssetExifBackfillV1]: SyncAssetExifV1;
|
||||||
[SyncEntityType.AlbumToAssetV1]: SyncAlbumToAssetV1;
|
[SyncEntityType.AlbumToAssetV1]: SyncAlbumToAssetV1;
|
||||||
[SyncEntityType.AlbumToAssetBackfillV1]: SyncAlbumToAssetV1;
|
[SyncEntityType.AlbumToAssetBackfillV1]: SyncAlbumToAssetV1;
|
||||||
|
|||||||
+5
-2
@@ -172,6 +172,7 @@ export enum Permission {
|
|||||||
ServerApkLinks = 'server.apkLinks',
|
ServerApkLinks = 'server.apkLinks',
|
||||||
ServerStorage = 'server.storage',
|
ServerStorage = 'server.storage',
|
||||||
ServerStatistics = 'server.statistics',
|
ServerStatistics = 'server.statistics',
|
||||||
|
ServerVersionCheck = 'server.versionCheck',
|
||||||
|
|
||||||
ServerLicenseRead = 'serverLicense.read',
|
ServerLicenseRead = 'serverLicense.read',
|
||||||
ServerLicenseUpdate = 'serverLicense.update',
|
ServerLicenseUpdate = 'serverLicense.update',
|
||||||
@@ -667,9 +668,11 @@ export enum SyncEntityType {
|
|||||||
AlbumUserBackfillV1 = 'AlbumUserBackfillV1',
|
AlbumUserBackfillV1 = 'AlbumUserBackfillV1',
|
||||||
AlbumUserDeleteV1 = 'AlbumUserDeleteV1',
|
AlbumUserDeleteV1 = 'AlbumUserDeleteV1',
|
||||||
|
|
||||||
AlbumAssetV1 = 'AlbumAssetV1',
|
AlbumAssetCreateV1 = 'AlbumAssetCreateV1',
|
||||||
|
AlbumAssetUpdateV1 = 'AlbumAssetUpdateV1',
|
||||||
AlbumAssetBackfillV1 = 'AlbumAssetBackfillV1',
|
AlbumAssetBackfillV1 = 'AlbumAssetBackfillV1',
|
||||||
AlbumAssetExifV1 = 'AlbumAssetExifV1',
|
AlbumAssetExifCreateV1 = 'AlbumAssetExifCreateV1',
|
||||||
|
AlbumAssetExifUpdateV1 = 'AlbumAssetExifUpdateV1',
|
||||||
AlbumAssetExifBackfillV1 = 'AlbumAssetExifBackfillV1',
|
AlbumAssetExifBackfillV1 = 'AlbumAssetExifBackfillV1',
|
||||||
|
|
||||||
AlbumToAssetV1 = 'AlbumToAssetV1',
|
AlbumToAssetV1 = 'AlbumToAssetV1',
|
||||||
|
|||||||
@@ -10,10 +10,17 @@ from
|
|||||||
where
|
where
|
||||||
"id" = $1
|
"id" = $1
|
||||||
|
|
||||||
|
-- SessionRepository.isPendingSyncReset
|
||||||
|
select
|
||||||
|
"isPendingSyncReset"
|
||||||
|
from
|
||||||
|
"session"
|
||||||
|
where
|
||||||
|
"id" = $1
|
||||||
|
|
||||||
-- SessionRepository.getByToken
|
-- SessionRepository.getByToken
|
||||||
select
|
select
|
||||||
"session"."id",
|
"session"."id",
|
||||||
"session"."isPendingSyncReset",
|
|
||||||
"session"."updatedAt",
|
"session"."updatedAt",
|
||||||
"session"."pinExpiresAt",
|
"session"."pinExpiresAt",
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -13,3 +13,7 @@ where
|
|||||||
delete from "session_sync_checkpoint"
|
delete from "session_sync_checkpoint"
|
||||||
where
|
where
|
||||||
"sessionId" = $1
|
"sessionId" = $1
|
||||||
|
|
||||||
|
-- SyncCheckpointRepository.getNow
|
||||||
|
select
|
||||||
|
immich_uuid_v7 (now() - interval '1 millisecond') as "nowId"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from
|
|||||||
where
|
where
|
||||||
"usersId" = $1
|
"usersId" = $1
|
||||||
and "createId" >= $2
|
and "createId" >= $2
|
||||||
and "createdAt" < now() - interval '1 millisecond'
|
and "createId" < $3
|
||||||
order by
|
order by
|
||||||
"createId" asc
|
"createId" asc
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ from
|
|||||||
"album_audit"
|
"album_audit"
|
||||||
where
|
where
|
||||||
"userId" = $1
|
"userId" = $1
|
||||||
and "deletedAt" < now() - interval '1 millisecond'
|
and "id" < $2
|
||||||
order by
|
order by
|
||||||
"id" asc
|
"id" asc
|
||||||
|
|
||||||
@@ -41,10 +41,10 @@ from
|
|||||||
"album"
|
"album"
|
||||||
left join "album_user" as "album_users" on "album"."id" = "album_users"."albumsId"
|
left join "album_user" as "album_users" on "album"."id" = "album_users"."albumsId"
|
||||||
where
|
where
|
||||||
"album"."updatedAt" < now() - interval '1 millisecond'
|
"album"."updateId" < $1
|
||||||
and (
|
and (
|
||||||
"album"."ownerId" = $1
|
"album"."ownerId" = $2
|
||||||
or "album_users"."usersId" = $2
|
or "album_users"."usersId" = $3
|
||||||
)
|
)
|
||||||
order by
|
order by
|
||||||
"album"."updateId" asc
|
"album"."updateId" asc
|
||||||
@@ -67,20 +67,21 @@ select
|
|||||||
"asset"."livePhotoVideoId",
|
"asset"."livePhotoVideoId",
|
||||||
"asset"."stackId",
|
"asset"."stackId",
|
||||||
"asset"."libraryId",
|
"asset"."libraryId",
|
||||||
"asset"."updateId"
|
"album_asset"."updateId"
|
||||||
from
|
from
|
||||||
"asset"
|
"album_asset"
|
||||||
inner join "album_asset" on "album_asset"."assetsId" = "asset"."id"
|
inner join "asset" on "asset"."id" = "album_asset"."assetsId"
|
||||||
where
|
where
|
||||||
"album_asset"."albumsId" = $1
|
"album_asset"."albumsId" = $1
|
||||||
and "asset"."updatedAt" < now() - interval '1 millisecond'
|
and "album_asset"."updateId" < $2
|
||||||
and "asset"."updateId" <= $2
|
and "album_asset"."updateId" <= $3
|
||||||
and "asset"."updateId" >= $3
|
and "album_asset"."updateId" >= $4
|
||||||
order by
|
order by
|
||||||
"asset"."updateId" asc
|
"album_asset"."updateId" asc
|
||||||
|
|
||||||
-- SyncRepository.albumAsset.getUpserts
|
-- SyncRepository.albumAsset.getCreates
|
||||||
select
|
select
|
||||||
|
"album_asset"."updateId",
|
||||||
"asset"."id",
|
"asset"."id",
|
||||||
"asset"."ownerId",
|
"asset"."ownerId",
|
||||||
"asset"."originalFileName",
|
"asset"."originalFileName",
|
||||||
@@ -96,21 +97,20 @@ select
|
|||||||
"asset"."duration",
|
"asset"."duration",
|
||||||
"asset"."livePhotoVideoId",
|
"asset"."livePhotoVideoId",
|
||||||
"asset"."stackId",
|
"asset"."stackId",
|
||||||
"asset"."libraryId",
|
"asset"."libraryId"
|
||||||
"asset"."updateId"
|
|
||||||
from
|
from
|
||||||
"asset"
|
"album_asset"
|
||||||
inner join "album_asset" on "album_asset"."assetsId" = "asset"."id"
|
inner join "asset" on "asset"."id" = "album_asset"."assetsId"
|
||||||
inner join "album" on "album"."id" = "album_asset"."albumsId"
|
inner join "album" on "album"."id" = "album_asset"."albumsId"
|
||||||
left join "album_user" on "album_user"."albumsId" = "album_asset"."albumsId"
|
left join "album_user" on "album_user"."albumsId" = "album_asset"."albumsId"
|
||||||
where
|
where
|
||||||
"asset"."updatedAt" < now() - interval '1 millisecond'
|
"album_asset"."updateId" < $1
|
||||||
and (
|
and (
|
||||||
"album"."ownerId" = $1
|
"album"."ownerId" = $2
|
||||||
or "album_user"."usersId" = $2
|
or "album_user"."usersId" = $3
|
||||||
)
|
)
|
||||||
order by
|
order by
|
||||||
"asset"."updateId" asc
|
"album_asset"."updateId" asc
|
||||||
|
|
||||||
-- SyncRepository.albumAssetExif.getBackfill
|
-- SyncRepository.albumAssetExif.getBackfill
|
||||||
select
|
select
|
||||||
@@ -139,20 +139,21 @@ select
|
|||||||
"asset_exif"."profileDescription",
|
"asset_exif"."profileDescription",
|
||||||
"asset_exif"."rating",
|
"asset_exif"."rating",
|
||||||
"asset_exif"."fps",
|
"asset_exif"."fps",
|
||||||
"asset_exif"."updateId"
|
"album_asset"."updateId"
|
||||||
from
|
from
|
||||||
"asset_exif"
|
"album_asset"
|
||||||
inner join "album_asset" on "album_asset"."assetsId" = "asset_exif"."assetId"
|
inner join "asset_exif" on "asset_exif"."assetId" = "album_asset"."assetsId"
|
||||||
where
|
where
|
||||||
"album_asset"."albumsId" = $1
|
"album_asset"."albumsId" = $1
|
||||||
and "asset_exif"."updatedAt" < now() - interval '1 millisecond'
|
and "album_asset"."updateId" < $2
|
||||||
and "asset_exif"."updateId" <= $2
|
and "album_asset"."updateId" <= $3
|
||||||
and "asset_exif"."updateId" >= $3
|
and "album_asset"."updateId" >= $4
|
||||||
order by
|
order by
|
||||||
"asset_exif"."updateId" asc
|
"album_asset"."updateId" asc
|
||||||
|
|
||||||
-- SyncRepository.albumAssetExif.getUpserts
|
-- SyncRepository.albumAssetExif.getCreates
|
||||||
select
|
select
|
||||||
|
"album_asset"."updateId",
|
||||||
"asset_exif"."assetId",
|
"asset_exif"."assetId",
|
||||||
"asset_exif"."description",
|
"asset_exif"."description",
|
||||||
"asset_exif"."exifImageWidth",
|
"asset_exif"."exifImageWidth",
|
||||||
@@ -177,21 +178,20 @@ select
|
|||||||
"asset_exif"."exposureTime",
|
"asset_exif"."exposureTime",
|
||||||
"asset_exif"."profileDescription",
|
"asset_exif"."profileDescription",
|
||||||
"asset_exif"."rating",
|
"asset_exif"."rating",
|
||||||
"asset_exif"."fps",
|
"asset_exif"."fps"
|
||||||
"asset_exif"."updateId"
|
|
||||||
from
|
from
|
||||||
"asset_exif"
|
"album_asset"
|
||||||
inner join "album_asset" on "album_asset"."assetsId" = "asset_exif"."assetId"
|
inner join "asset_exif" on "asset_exif"."assetId" = "album_asset"."assetsId"
|
||||||
inner join "album" on "album"."id" = "album_asset"."albumsId"
|
inner join "album" on "album"."id" = "album_asset"."albumsId"
|
||||||
left join "album_user" on "album_user"."albumsId" = "album_asset"."albumsId"
|
left join "album_user" on "album_user"."albumsId" = "album_asset"."albumsId"
|
||||||
where
|
where
|
||||||
"asset_exif"."updatedAt" < now() - interval '1 millisecond'
|
"album_asset"."updateId" < $1
|
||||||
and (
|
and (
|
||||||
"album"."ownerId" = $1
|
"album"."ownerId" = $2
|
||||||
or "album_user"."usersId" = $2
|
or "album_user"."usersId" = $3
|
||||||
)
|
)
|
||||||
order by
|
order by
|
||||||
"asset_exif"."updateId" asc
|
"album_asset"."updateId" asc
|
||||||
|
|
||||||
-- SyncRepository.albumToAsset.getBackfill
|
-- SyncRepository.albumToAsset.getBackfill
|
||||||
select
|
select
|
||||||
@@ -202,9 +202,9 @@ from
|
|||||||
"album_asset" as "album_assets"
|
"album_asset" as "album_assets"
|
||||||
where
|
where
|
||||||
"album_assets"."albumsId" = $1
|
"album_assets"."albumsId" = $1
|
||||||
and "album_assets"."updatedAt" < now() - interval '1 millisecond'
|
and "album_assets"."updateId" < $2
|
||||||
and "album_assets"."updateId" <= $2
|
and "album_assets"."updateId" <= $3
|
||||||
and "album_assets"."updateId" >= $3
|
and "album_assets"."updateId" >= $4
|
||||||
order by
|
order by
|
||||||
"album_assets"."updateId" asc
|
"album_assets"."updateId" asc
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ where
|
|||||||
"album_user"."usersId" = $2
|
"album_user"."usersId" = $2
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
and "deletedAt" < now() - interval '1 millisecond'
|
and "id" < $3
|
||||||
order by
|
order by
|
||||||
"id" asc
|
"id" asc
|
||||||
|
|
||||||
@@ -247,10 +247,10 @@ from
|
|||||||
inner join "album" on "album"."id" = "album_asset"."albumsId"
|
inner join "album" on "album"."id" = "album_asset"."albumsId"
|
||||||
left join "album_user" on "album_user"."albumsId" = "album_asset"."albumsId"
|
left join "album_user" on "album_user"."albumsId" = "album_asset"."albumsId"
|
||||||
where
|
where
|
||||||
"album_asset"."updatedAt" < now() - interval '1 millisecond'
|
"album_asset"."updateId" < $1
|
||||||
and (
|
and (
|
||||||
"album"."ownerId" = $1
|
"album"."ownerId" = $2
|
||||||
or "album_user"."usersId" = $2
|
or "album_user"."usersId" = $3
|
||||||
)
|
)
|
||||||
order by
|
order by
|
||||||
"album_asset"."updateId" asc
|
"album_asset"."updateId" asc
|
||||||
@@ -265,9 +265,9 @@ from
|
|||||||
"album_user"
|
"album_user"
|
||||||
where
|
where
|
||||||
"albumsId" = $1
|
"albumsId" = $1
|
||||||
and "updatedAt" < now() - interval '1 millisecond'
|
and "updateId" < $2
|
||||||
and "updateId" <= $2
|
and "updateId" <= $3
|
||||||
and "updateId" >= $3
|
and "updateId" >= $4
|
||||||
order by
|
order by
|
||||||
"updateId" asc
|
"updateId" asc
|
||||||
|
|
||||||
@@ -296,7 +296,7 @@ where
|
|||||||
"album_user"."usersId" = $2
|
"album_user"."usersId" = $2
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
and "deletedAt" < now() - interval '1 millisecond'
|
and "id" < $3
|
||||||
order by
|
order by
|
||||||
"id" asc
|
"id" asc
|
||||||
|
|
||||||
@@ -309,14 +309,14 @@ select
|
|||||||
from
|
from
|
||||||
"album_user"
|
"album_user"
|
||||||
where
|
where
|
||||||
"album_user"."updatedAt" < now() - interval '1 millisecond'
|
"album_user"."updateId" < $1
|
||||||
and "album_user"."albumsId" in (
|
and "album_user"."albumsId" in (
|
||||||
select
|
select
|
||||||
"id"
|
"id"
|
||||||
from
|
from
|
||||||
"album"
|
"album"
|
||||||
where
|
where
|
||||||
"ownerId" = $1
|
"ownerId" = $2
|
||||||
union
|
union
|
||||||
(
|
(
|
||||||
select
|
select
|
||||||
@@ -324,7 +324,7 @@ where
|
|||||||
from
|
from
|
||||||
"album_user" as "albumUsers"
|
"album_user" as "albumUsers"
|
||||||
where
|
where
|
||||||
"albumUsers"."usersId" = $2
|
"albumUsers"."usersId" = $3
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
order by
|
order by
|
||||||
@@ -338,7 +338,7 @@ from
|
|||||||
"asset_audit"
|
"asset_audit"
|
||||||
where
|
where
|
||||||
"ownerId" = $1
|
"ownerId" = $1
|
||||||
and "deletedAt" < now() - interval '1 millisecond'
|
and "id" < $2
|
||||||
order by
|
order by
|
||||||
"id" asc
|
"id" asc
|
||||||
|
|
||||||
@@ -365,7 +365,7 @@ from
|
|||||||
"asset"
|
"asset"
|
||||||
where
|
where
|
||||||
"ownerId" = $1
|
"ownerId" = $1
|
||||||
and "updatedAt" < now() - interval '1 millisecond'
|
and "updateId" < $2
|
||||||
order by
|
order by
|
||||||
"updateId" asc
|
"updateId" asc
|
||||||
|
|
||||||
@@ -408,7 +408,7 @@ where
|
|||||||
where
|
where
|
||||||
"ownerId" = $1
|
"ownerId" = $1
|
||||||
)
|
)
|
||||||
and "updatedAt" < now() - interval '1 millisecond'
|
and "updateId" < $2
|
||||||
order by
|
order by
|
||||||
"updateId" asc
|
"updateId" asc
|
||||||
|
|
||||||
@@ -421,7 +421,7 @@ from
|
|||||||
left join "asset" on "asset"."id" = "asset_face_audit"."assetId"
|
left join "asset" on "asset"."id" = "asset_face_audit"."assetId"
|
||||||
where
|
where
|
||||||
"asset"."ownerId" = $1
|
"asset"."ownerId" = $1
|
||||||
and "asset_face_audit"."deletedAt" < now() - interval '1 millisecond'
|
and "asset_face_audit"."id" < $2
|
||||||
order by
|
order by
|
||||||
"asset_face_audit"."id" asc
|
"asset_face_audit"."id" asc
|
||||||
|
|
||||||
@@ -442,34 +442,11 @@ from
|
|||||||
"asset_face"
|
"asset_face"
|
||||||
left join "asset" on "asset"."id" = "asset_face"."assetId"
|
left join "asset" on "asset"."id" = "asset_face"."assetId"
|
||||||
where
|
where
|
||||||
"asset_face"."updatedAt" < now() - interval '1 millisecond'
|
"asset_face"."updateId" < $1
|
||||||
and "asset"."ownerId" = $1
|
and "asset"."ownerId" = $2
|
||||||
order by
|
order by
|
||||||
"asset_face"."updateId" asc
|
"asset_face"."updateId" asc
|
||||||
|
|
||||||
-- SyncRepository.authUser.getUpserts
|
|
||||||
select
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"email",
|
|
||||||
"avatarColor",
|
|
||||||
"deletedAt",
|
|
||||||
"updateId",
|
|
||||||
"profileImagePath",
|
|
||||||
"profileChangedAt",
|
|
||||||
"isAdmin",
|
|
||||||
"pinCode",
|
|
||||||
"oauthId",
|
|
||||||
"storageLabel",
|
|
||||||
"quotaSizeInBytes",
|
|
||||||
"quotaUsageInBytes"
|
|
||||||
from
|
|
||||||
"user"
|
|
||||||
where
|
|
||||||
"updatedAt" < now() - interval '1 millisecond'
|
|
||||||
order by
|
|
||||||
"updateId" asc
|
|
||||||
|
|
||||||
-- SyncRepository.memory.getDeletes
|
-- SyncRepository.memory.getDeletes
|
||||||
select
|
select
|
||||||
"id",
|
"id",
|
||||||
@@ -478,7 +455,7 @@ from
|
|||||||
"memory_audit"
|
"memory_audit"
|
||||||
where
|
where
|
||||||
"userId" = $1
|
"userId" = $1
|
||||||
and "deletedAt" < now() - interval '1 millisecond'
|
and "id" < $2
|
||||||
order by
|
order by
|
||||||
"id" asc
|
"id" asc
|
||||||
|
|
||||||
@@ -501,7 +478,7 @@ from
|
|||||||
"memory"
|
"memory"
|
||||||
where
|
where
|
||||||
"ownerId" = $1
|
"ownerId" = $1
|
||||||
and "updatedAt" < now() - interval '1 millisecond'
|
and "updateId" < $2
|
||||||
order by
|
order by
|
||||||
"updateId" asc
|
"updateId" asc
|
||||||
|
|
||||||
@@ -521,7 +498,7 @@ where
|
|||||||
where
|
where
|
||||||
"ownerId" = $1
|
"ownerId" = $1
|
||||||
)
|
)
|
||||||
and "deletedAt" < now() - interval '1 millisecond'
|
and "id" < $2
|
||||||
order by
|
order by
|
||||||
"id" asc
|
"id" asc
|
||||||
|
|
||||||
@@ -541,7 +518,7 @@ where
|
|||||||
where
|
where
|
||||||
"ownerId" = $1
|
"ownerId" = $1
|
||||||
)
|
)
|
||||||
and "updatedAt" < now() - interval '1 millisecond'
|
and "updateId" < $2
|
||||||
order by
|
order by
|
||||||
"updateId" asc
|
"updateId" asc
|
||||||
|
|
||||||
@@ -553,8 +530,7 @@ from
|
|||||||
"partner"
|
"partner"
|
||||||
where
|
where
|
||||||
"sharedWithId" = $1
|
"sharedWithId" = $1
|
||||||
and "createId" >= $2
|
and "createId" < $2
|
||||||
and "createdAt" < now() - interval '1 millisecond'
|
|
||||||
order by
|
order by
|
||||||
"partner"."createId" asc
|
"partner"."createId" asc
|
||||||
|
|
||||||
@@ -570,7 +546,7 @@ where
|
|||||||
"sharedById" = $1
|
"sharedById" = $1
|
||||||
or "sharedWithId" = $2
|
or "sharedWithId" = $2
|
||||||
)
|
)
|
||||||
and "deletedAt" < now() - interval '1 millisecond'
|
and "id" < $3
|
||||||
order by
|
order by
|
||||||
"id" asc
|
"id" asc
|
||||||
|
|
||||||
@@ -587,7 +563,7 @@ where
|
|||||||
"sharedById" = $1
|
"sharedById" = $1
|
||||||
or "sharedWithId" = $2
|
or "sharedWithId" = $2
|
||||||
)
|
)
|
||||||
and "updatedAt" < now() - interval '1 millisecond'
|
and "updateId" < $3
|
||||||
order by
|
order by
|
||||||
"updateId" asc
|
"updateId" asc
|
||||||
|
|
||||||
@@ -614,9 +590,9 @@ from
|
|||||||
"asset"
|
"asset"
|
||||||
where
|
where
|
||||||
"ownerId" = $1
|
"ownerId" = $1
|
||||||
and "updatedAt" < now() - interval '1 millisecond'
|
and "updateId" < $2
|
||||||
and "updateId" <= $2
|
and "updateId" <= $3
|
||||||
and "updateId" >= $3
|
and "updateId" >= $4
|
||||||
order by
|
order by
|
||||||
"updateId" asc
|
"updateId" asc
|
||||||
|
|
||||||
@@ -635,7 +611,7 @@ where
|
|||||||
where
|
where
|
||||||
"sharedWithId" = $1
|
"sharedWithId" = $1
|
||||||
)
|
)
|
||||||
and "deletedAt" < now() - interval '1 millisecond'
|
and "id" < $2
|
||||||
order by
|
order by
|
||||||
"id" asc
|
"id" asc
|
||||||
|
|
||||||
@@ -669,7 +645,7 @@ where
|
|||||||
where
|
where
|
||||||
"sharedWithId" = $1
|
"sharedWithId" = $1
|
||||||
)
|
)
|
||||||
and "updatedAt" < now() - interval '1 millisecond'
|
and "updateId" < $2
|
||||||
order by
|
order by
|
||||||
"updateId" asc
|
"updateId" asc
|
||||||
|
|
||||||
@@ -706,9 +682,9 @@ from
|
|||||||
inner join "asset" on "asset"."id" = "asset_exif"."assetId"
|
inner join "asset" on "asset"."id" = "asset_exif"."assetId"
|
||||||
where
|
where
|
||||||
"asset"."ownerId" = $1
|
"asset"."ownerId" = $1
|
||||||
and "asset_exif"."updatedAt" < now() - interval '1 millisecond'
|
and "asset_exif"."updateId" < $2
|
||||||
and "asset_exif"."updateId" <= $2
|
and "asset_exif"."updateId" <= $3
|
||||||
and "asset_exif"."updateId" >= $3
|
and "asset_exif"."updateId" >= $4
|
||||||
order by
|
order by
|
||||||
"asset_exif"."updateId" asc
|
"asset_exif"."updateId" asc
|
||||||
|
|
||||||
@@ -758,7 +734,7 @@ where
|
|||||||
"sharedWithId" = $1
|
"sharedWithId" = $1
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
and "updatedAt" < now() - interval '1 millisecond'
|
and "updateId" < $2
|
||||||
order by
|
order by
|
||||||
"updateId" asc
|
"updateId" asc
|
||||||
|
|
||||||
@@ -777,7 +753,7 @@ where
|
|||||||
where
|
where
|
||||||
"sharedWithId" = $1
|
"sharedWithId" = $1
|
||||||
)
|
)
|
||||||
and "deletedAt" < now() - interval '1 millisecond'
|
and "id" < $2
|
||||||
order by
|
order by
|
||||||
"id" asc
|
"id" asc
|
||||||
|
|
||||||
@@ -793,9 +769,9 @@ from
|
|||||||
"stack"
|
"stack"
|
||||||
where
|
where
|
||||||
"ownerId" = $1
|
"ownerId" = $1
|
||||||
and "updatedAt" < now() - interval '1 millisecond'
|
and "updateId" < $2
|
||||||
and "updateId" <= $2
|
and "updateId" <= $3
|
||||||
and "updateId" >= $3
|
and "updateId" >= $4
|
||||||
order by
|
order by
|
||||||
"updateId" asc
|
"updateId" asc
|
||||||
|
|
||||||
@@ -818,7 +794,7 @@ where
|
|||||||
where
|
where
|
||||||
"sharedWithId" = $1
|
"sharedWithId" = $1
|
||||||
)
|
)
|
||||||
and "updatedAt" < now() - interval '1 millisecond'
|
and "updateId" < $2
|
||||||
order by
|
order by
|
||||||
"updateId" asc
|
"updateId" asc
|
||||||
|
|
||||||
@@ -830,7 +806,7 @@ from
|
|||||||
"person_audit"
|
"person_audit"
|
||||||
where
|
where
|
||||||
"ownerId" = $1
|
"ownerId" = $1
|
||||||
and "deletedAt" < now() - interval '1 millisecond'
|
and "id" < $2
|
||||||
order by
|
order by
|
||||||
"id" asc
|
"id" asc
|
||||||
|
|
||||||
@@ -851,7 +827,7 @@ from
|
|||||||
"person"
|
"person"
|
||||||
where
|
where
|
||||||
"ownerId" = $1
|
"ownerId" = $1
|
||||||
and "updatedAt" < now() - interval '1 millisecond'
|
and "updateId" < $2
|
||||||
order by
|
order by
|
||||||
"updateId" asc
|
"updateId" asc
|
||||||
|
|
||||||
@@ -863,7 +839,7 @@ from
|
|||||||
"stack_audit"
|
"stack_audit"
|
||||||
where
|
where
|
||||||
"userId" = $1
|
"userId" = $1
|
||||||
and "deletedAt" < now() - interval '1 millisecond'
|
and "id" < $2
|
||||||
order by
|
order by
|
||||||
"id" asc
|
"id" asc
|
||||||
|
|
||||||
@@ -879,7 +855,7 @@ from
|
|||||||
"stack"
|
"stack"
|
||||||
where
|
where
|
||||||
"ownerId" = $1
|
"ownerId" = $1
|
||||||
and "updatedAt" < now() - interval '1 millisecond'
|
and "updateId" < $2
|
||||||
order by
|
order by
|
||||||
"updateId" asc
|
"updateId" asc
|
||||||
|
|
||||||
@@ -890,7 +866,7 @@ select
|
|||||||
from
|
from
|
||||||
"user_audit"
|
"user_audit"
|
||||||
where
|
where
|
||||||
"deletedAt" < now() - interval '1 millisecond'
|
"id" < $1
|
||||||
order by
|
order by
|
||||||
"id" asc
|
"id" asc
|
||||||
|
|
||||||
@@ -907,7 +883,7 @@ select
|
|||||||
from
|
from
|
||||||
"user"
|
"user"
|
||||||
where
|
where
|
||||||
"updatedAt" < now() - interval '1 millisecond'
|
"updateId" < $1
|
||||||
order by
|
order by
|
||||||
"updateId" asc
|
"updateId" asc
|
||||||
|
|
||||||
@@ -920,7 +896,7 @@ from
|
|||||||
"user_metadata_audit"
|
"user_metadata_audit"
|
||||||
where
|
where
|
||||||
"userId" = $1
|
"userId" = $1
|
||||||
and "deletedAt" < now() - interval '1 millisecond'
|
and "id" < $2
|
||||||
order by
|
order by
|
||||||
"id" asc
|
"id" asc
|
||||||
|
|
||||||
@@ -934,6 +910,6 @@ from
|
|||||||
"user_metadata"
|
"user_metadata"
|
||||||
where
|
where
|
||||||
"userId" = $1
|
"userId" = $1
|
||||||
and "updatedAt" < now() - interval '1 millisecond'
|
and "updateId" < $2
|
||||||
order by
|
order by
|
||||||
"updateId" asc
|
"updateId" asc
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ export class SessionRepository {
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
async isPendingSyncReset(id: string) {
|
||||||
|
const result = await this.db
|
||||||
|
.selectFrom('session')
|
||||||
|
.select(['isPendingSyncReset'])
|
||||||
|
.where('id', '=', id)
|
||||||
|
.executeTakeFirst();
|
||||||
|
return result?.isPendingSyncReset ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.STRING] })
|
@GenerateSql({ params: [DummyValue.STRING] })
|
||||||
getByToken(token: string) {
|
getByToken(token: string) {
|
||||||
return this.db
|
return this.db
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Insertable, Kysely } from 'kysely';
|
import { Insertable, Kysely, sql } from 'kysely';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { SyncEntityType } from 'src/enum';
|
import { SyncEntityType } from 'src/enum';
|
||||||
@@ -39,4 +39,13 @@ export class SyncCheckpointRepository {
|
|||||||
.$if(!!types, (qb) => qb.where('type', 'in', types!))
|
.$if(!!types, (qb) => qb.where('type', 'in', types!))
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql()
|
||||||
|
getNow() {
|
||||||
|
return this.db
|
||||||
|
.selectNoFrom((eb) => [
|
||||||
|
eb.fn<string>('immich_uuid_v7', [sql.raw<Date>("now() - interval '1 millisecond'")]).as('nowId'),
|
||||||
|
])
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Kysely, SelectQueryBuilder, sql } from 'kysely';
|
import { Kysely, SelectQueryBuilder } from 'kysely';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { columns } from 'src/database';
|
import { columns } from 'src/database';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
@@ -33,6 +33,11 @@ type UpsertTables =
|
|||||||
| 'user_metadata'
|
| 'user_metadata'
|
||||||
| 'asset_face';
|
| 'asset_face';
|
||||||
|
|
||||||
|
export type SyncQueryOptions = {
|
||||||
|
nowId: string;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SyncRepository {
|
export class SyncRepository {
|
||||||
album: AlbumSync;
|
album: AlbumSync;
|
||||||
@@ -81,21 +86,21 @@ export class SyncRepository {
|
|||||||
class BaseSync {
|
class BaseSync {
|
||||||
constructor(protected db: Kysely<DB>) {}
|
constructor(protected db: Kysely<DB>) {}
|
||||||
|
|
||||||
protected auditTableFilters(ack?: SyncAck) {
|
protected auditTableFilters(nowId: string, ack?: SyncAck) {
|
||||||
return <T extends keyof Pick<DB, AuditTables>, D>(qb: SelectQueryBuilder<DB, T, D>) => {
|
return <T extends keyof Pick<DB, AuditTables>, D>(qb: SelectQueryBuilder<DB, T, D>) => {
|
||||||
const builder = qb as SelectQueryBuilder<DB, AuditTables, D>;
|
const builder = qb as SelectQueryBuilder<DB, AuditTables, D>;
|
||||||
return builder
|
return builder
|
||||||
.where('deletedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('id', '<', nowId)
|
||||||
.$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId))
|
.$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId))
|
||||||
.orderBy('id', 'asc') as SelectQueryBuilder<DB, T, D>;
|
.orderBy('id', 'asc') as SelectQueryBuilder<DB, T, D>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected upsertTableFilters(ack?: SyncAck) {
|
protected upsertTableFilters(nowId: string, ack?: SyncAck) {
|
||||||
return <T extends keyof Pick<DB, UpsertTables>, D>(qb: SelectQueryBuilder<DB, T, D>) => {
|
return <T extends keyof Pick<DB, UpsertTables>, D>(qb: SelectQueryBuilder<DB, T, D>) => {
|
||||||
const builder = qb as SelectQueryBuilder<DB, UpsertTables, D>;
|
const builder = qb as SelectQueryBuilder<DB, UpsertTables, D>;
|
||||||
return builder
|
return builder
|
||||||
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('updateId', '<', nowId)
|
||||||
.$if(!!ack, (qb) => qb.where('updateId', '>', ack!.updateId))
|
.$if(!!ack, (qb) => qb.where('updateId', '>', ack!.updateId))
|
||||||
.orderBy('updateId', 'asc') as SelectQueryBuilder<DB, T, D>;
|
.orderBy('updateId', 'asc') as SelectQueryBuilder<DB, T, D>;
|
||||||
};
|
};
|
||||||
@@ -103,34 +108,34 @@ class BaseSync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AlbumSync extends BaseSync {
|
class AlbumSync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }, DummyValue.UUID] })
|
||||||
getCreatedAfter(userId: string, afterCreateId?: string) {
|
getCreatedAfter({ nowId, userId }: SyncQueryOptions, afterCreateId?: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('album_user')
|
.selectFrom('album_user')
|
||||||
.select(['albumsId as id', 'createId'])
|
.select(['albumsId as id', 'createId'])
|
||||||
.where('usersId', '=', userId)
|
.where('usersId', '=', userId)
|
||||||
.$if(!!afterCreateId, (qb) => qb.where('createId', '>=', afterCreateId!))
|
.$if(!!afterCreateId, (qb) => qb.where('createId', '>=', afterCreateId!))
|
||||||
.where('createdAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('createId', '<', nowId)
|
||||||
.orderBy('createId', 'asc')
|
.orderBy('createId', 'asc')
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getDeletes(userId: string, ack?: SyncAck) {
|
getDeletes({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('album_audit')
|
.selectFrom('album_audit')
|
||||||
.select(['id', 'albumId'])
|
.select(['id', 'albumId'])
|
||||||
.where('userId', '=', userId)
|
.where('userId', '=', userId)
|
||||||
.$call(this.auditTableFilters(ack))
|
.$call(this.auditTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpserts({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('album')
|
.selectFrom('album')
|
||||||
.distinctOn(['album.id', 'album.updateId'])
|
.distinctOn(['album.id', 'album.updateId'])
|
||||||
.where('album.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('album.updateId', '<', nowId)
|
||||||
.$if(!!ack, (qb) => qb.where('album.updateId', '>', ack!.updateId))
|
.$if(!!ack, (qb) => qb.where('album.updateId', '>', ack!.updateId))
|
||||||
.orderBy('album.updateId', 'asc')
|
.orderBy('album.updateId', 'asc')
|
||||||
.leftJoin('album_user as album_users', 'album.id', 'album_users.albumsId')
|
.leftJoin('album_user as album_users', 'album.id', 'album_users.albumsId')
|
||||||
@@ -152,29 +157,33 @@ class AlbumSync extends BaseSync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AlbumAssetSync extends BaseSync {
|
class AlbumAssetSync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
@GenerateSql({
|
||||||
getBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }, DummyValue.UUID, DummyValue.UUID, DummyValue.UUID],
|
||||||
|
stream: true,
|
||||||
|
})
|
||||||
|
getBackfill({ nowId }: SyncQueryOptions, albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset')
|
.selectFrom('album_asset')
|
||||||
.innerJoin('album_asset', 'album_asset.assetsId', 'asset.id')
|
.innerJoin('asset', 'asset.id', 'album_asset.assetsId')
|
||||||
.select(columns.syncAsset)
|
.select(columns.syncAsset)
|
||||||
.select('asset.updateId')
|
.select('album_asset.updateId')
|
||||||
.where('album_asset.albumsId', '=', albumId)
|
.where('album_asset.albumsId', '=', albumId)
|
||||||
.where('asset.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('album_asset.updateId', '<', nowId)
|
||||||
.where('asset.updateId', '<=', beforeUpdateId)
|
.where('album_asset.updateId', '<=', beforeUpdateId)
|
||||||
.$if(!!afterUpdateId, (eb) => eb.where('asset.updateId', '>=', afterUpdateId!))
|
.$if(!!afterUpdateId, (eb) => eb.where('album_asset.updateId', '>=', afterUpdateId!))
|
||||||
.orderBy('asset.updateId', 'asc')
|
.orderBy('album_asset.updateId', 'asc')
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpdates({ nowId, userId }: SyncQueryOptions, albumToAssetAck: SyncAck, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset')
|
.selectFrom('asset')
|
||||||
.innerJoin('album_asset', 'album_asset.assetsId', 'asset.id')
|
.innerJoin('album_asset', 'album_asset.assetsId', 'asset.id')
|
||||||
.select(columns.syncAsset)
|
.select(columns.syncAsset)
|
||||||
.select('asset.updateId')
|
.select('asset.updateId')
|
||||||
.where('asset.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('asset.updateId', '<', nowId)
|
||||||
|
.where('album_asset.updateId', '<=', albumToAssetAck.updateId) // Ensure we only send updates for assets that the client already knows about
|
||||||
.$if(!!ack, (qb) => qb.where('asset.updateId', '>', ack!.updateId))
|
.$if(!!ack, (qb) => qb.where('asset.updateId', '>', ack!.updateId))
|
||||||
.orderBy('asset.updateId', 'asc')
|
.orderBy('asset.updateId', 'asc')
|
||||||
.innerJoin('album', 'album.id', 'album_asset.albumsId')
|
.innerJoin('album', 'album.id', 'album_asset.albumsId')
|
||||||
@@ -182,32 +191,52 @@ class AlbumAssetSync extends BaseSync {
|
|||||||
.where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('album_user.usersId', '=', userId)]))
|
.where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('album_user.usersId', '=', userId)]))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
|
getCreates({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('album_asset')
|
||||||
|
.select('album_asset.updateId')
|
||||||
|
.innerJoin('asset', 'asset.id', 'album_asset.assetsId')
|
||||||
|
.select(columns.syncAsset)
|
||||||
|
.innerJoin('album', 'album.id', 'album_asset.albumsId')
|
||||||
|
.leftJoin('album_user', 'album_user.albumsId', 'album_asset.albumsId')
|
||||||
|
.where('album_asset.updateId', '<', nowId)
|
||||||
|
.where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('album_user.usersId', '=', userId)]))
|
||||||
|
.$if(!!ack, (qb) => qb.where('album_asset.updateId', '>', ack!.updateId))
|
||||||
|
.orderBy('album_asset.updateId', 'asc')
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AlbumAssetExifSync extends BaseSync {
|
class AlbumAssetExifSync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
@GenerateSql({
|
||||||
getBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }, DummyValue.UUID, DummyValue.UUID, DummyValue.UUID],
|
||||||
|
stream: true,
|
||||||
|
})
|
||||||
|
getBackfill({ nowId }: SyncQueryOptions, albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_exif')
|
.selectFrom('album_asset')
|
||||||
.innerJoin('album_asset', 'album_asset.assetsId', 'asset_exif.assetId')
|
.innerJoin('asset_exif', 'asset_exif.assetId', 'album_asset.assetsId')
|
||||||
.select(columns.syncAssetExif)
|
.select(columns.syncAssetExif)
|
||||||
.select('asset_exif.updateId')
|
.select('album_asset.updateId')
|
||||||
.where('album_asset.albumsId', '=', albumId)
|
.where('album_asset.albumsId', '=', albumId)
|
||||||
.where('asset_exif.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('album_asset.updateId', '<', nowId)
|
||||||
.where('asset_exif.updateId', '<=', beforeUpdateId)
|
.where('album_asset.updateId', '<=', beforeUpdateId)
|
||||||
.$if(!!afterUpdateId, (eb) => eb.where('asset_exif.updateId', '>=', afterUpdateId!))
|
.$if(!!afterUpdateId, (eb) => eb.where('album_asset.updateId', '>=', afterUpdateId!))
|
||||||
.orderBy('asset_exif.updateId', 'asc')
|
.orderBy('album_asset.updateId', 'asc')
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpdates({ nowId, userId }: SyncQueryOptions, albumToAssetAck: SyncAck, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_exif')
|
.selectFrom('asset_exif')
|
||||||
.innerJoin('album_asset', 'album_asset.assetsId', 'asset_exif.assetId')
|
.innerJoin('album_asset', 'album_asset.assetsId', 'asset_exif.assetId')
|
||||||
.select(columns.syncAssetExif)
|
.select(columns.syncAssetExif)
|
||||||
.select('asset_exif.updateId')
|
.select('asset_exif.updateId')
|
||||||
.where('asset_exif.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('album_asset.updateId', '<=', albumToAssetAck.updateId) // Ensure we only send exif updates for assets that the client already knows about
|
||||||
|
.where('asset_exif.updateId', '<', nowId)
|
||||||
.$if(!!ack, (qb) => qb.where('asset_exif.updateId', '>', ack!.updateId))
|
.$if(!!ack, (qb) => qb.where('asset_exif.updateId', '>', ack!.updateId))
|
||||||
.orderBy('asset_exif.updateId', 'asc')
|
.orderBy('asset_exif.updateId', 'asc')
|
||||||
.innerJoin('album', 'album.id', 'album_asset.albumsId')
|
.innerJoin('album', 'album.id', 'album_asset.albumsId')
|
||||||
@@ -215,24 +244,43 @@ class AlbumAssetExifSync extends BaseSync {
|
|||||||
.where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('album_user.usersId', '=', userId)]))
|
.where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('album_user.usersId', '=', userId)]))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
|
getCreates({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('album_asset')
|
||||||
|
.select('album_asset.updateId')
|
||||||
|
.innerJoin('asset_exif', 'asset_exif.assetId', 'album_asset.assetsId')
|
||||||
|
.select(columns.syncAssetExif)
|
||||||
|
.innerJoin('album', 'album.id', 'album_asset.albumsId')
|
||||||
|
.leftJoin('album_user', 'album_user.albumsId', 'album_asset.albumsId')
|
||||||
|
.where('album_asset.updateId', '<', nowId)
|
||||||
|
.where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('album_user.usersId', '=', userId)]))
|
||||||
|
.$if(!!ack, (qb) => qb.where('album_asset.updateId', '>', ack!.updateId))
|
||||||
|
.orderBy('album_asset.updateId', 'asc')
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AlbumToAssetSync extends BaseSync {
|
class AlbumToAssetSync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
@GenerateSql({
|
||||||
getBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }, DummyValue.UUID, DummyValue.UUID, DummyValue.UUID],
|
||||||
|
stream: true,
|
||||||
|
})
|
||||||
|
getBackfill({ nowId }: SyncQueryOptions, albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('album_asset as album_assets')
|
.selectFrom('album_asset as album_assets')
|
||||||
.select(['album_assets.assetsId as assetId', 'album_assets.albumsId as albumId', 'album_assets.updateId'])
|
.select(['album_assets.assetsId as assetId', 'album_assets.albumsId as albumId', 'album_assets.updateId'])
|
||||||
.where('album_assets.albumsId', '=', albumId)
|
.where('album_assets.albumsId', '=', albumId)
|
||||||
.where('album_assets.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('album_assets.updateId', '<', nowId)
|
||||||
.where('album_assets.updateId', '<=', beforeUpdateId)
|
.where('album_assets.updateId', '<=', beforeUpdateId)
|
||||||
.$if(!!afterUpdateId, (eb) => eb.where('album_assets.updateId', '>=', afterUpdateId!))
|
.$if(!!afterUpdateId, (eb) => eb.where('album_assets.updateId', '>=', afterUpdateId!))
|
||||||
.orderBy('album_assets.updateId', 'asc')
|
.orderBy('album_assets.updateId', 'asc')
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getDeletes(userId: string, ack?: SyncAck) {
|
getDeletes({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('album_asset_audit')
|
.selectFrom('album_asset_audit')
|
||||||
.select(['id', 'assetId', 'albumId'])
|
.select(['id', 'assetId', 'albumId'])
|
||||||
@@ -254,16 +302,16 @@ class AlbumToAssetSync extends BaseSync {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.$call(this.auditTableFilters(ack))
|
.$call(this.auditTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpserts({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('album_asset')
|
.selectFrom('album_asset')
|
||||||
.select(['album_asset.assetsId as assetId', 'album_asset.albumsId as albumId', 'album_asset.updateId'])
|
.select(['album_asset.assetsId as assetId', 'album_asset.albumsId as albumId', 'album_asset.updateId'])
|
||||||
.where('album_asset.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('album_asset.updateId', '<', nowId)
|
||||||
.$if(!!ack, (qb) => qb.where('album_asset.updateId', '>', ack!.updateId))
|
.$if(!!ack, (qb) => qb.where('album_asset.updateId', '>', ack!.updateId))
|
||||||
.orderBy('album_asset.updateId', 'asc')
|
.orderBy('album_asset.updateId', 'asc')
|
||||||
.innerJoin('album', 'album.id', 'album_asset.albumsId')
|
.innerJoin('album', 'album.id', 'album_asset.albumsId')
|
||||||
@@ -274,22 +322,25 @@ class AlbumToAssetSync extends BaseSync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AlbumUserSync extends BaseSync {
|
class AlbumUserSync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
@GenerateSql({
|
||||||
getBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }, DummyValue.UUID, DummyValue.UUID, DummyValue.UUID],
|
||||||
|
stream: true,
|
||||||
|
})
|
||||||
|
getBackfill({ nowId }: SyncQueryOptions, albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('album_user')
|
.selectFrom('album_user')
|
||||||
.select(columns.syncAlbumUser)
|
.select(columns.syncAlbumUser)
|
||||||
.select('album_user.updateId')
|
.select('album_user.updateId')
|
||||||
.where('albumsId', '=', albumId)
|
.where('albumsId', '=', albumId)
|
||||||
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('updateId', '<', nowId)
|
||||||
.where('updateId', '<=', beforeUpdateId)
|
.where('updateId', '<=', beforeUpdateId)
|
||||||
.$if(!!afterUpdateId, (eb) => eb.where('updateId', '>=', afterUpdateId!))
|
.$if(!!afterUpdateId, (eb) => eb.where('updateId', '>=', afterUpdateId!))
|
||||||
.orderBy('updateId', 'asc')
|
.orderBy('updateId', 'asc')
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getDeletes(userId: string, ack?: SyncAck) {
|
getDeletes({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('album_user_audit')
|
.selectFrom('album_user_audit')
|
||||||
.select(['id', 'userId', 'albumId'])
|
.select(['id', 'userId', 'albumId'])
|
||||||
@@ -311,17 +362,17 @@ class AlbumUserSync extends BaseSync {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.$call(this.auditTableFilters(ack))
|
.$call(this.auditTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpserts({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('album_user')
|
.selectFrom('album_user')
|
||||||
.select(columns.syncAlbumUser)
|
.select(columns.syncAlbumUser)
|
||||||
.select('album_user.updateId')
|
.select('album_user.updateId')
|
||||||
.where('album_user.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('album_user.updateId', '<', nowId)
|
||||||
.$if(!!ack, (qb) => qb.where('album_user.updateId', '>', ack!.updateId))
|
.$if(!!ack, (qb) => qb.where('album_user.updateId', '>', ack!.updateId))
|
||||||
.orderBy('album_user.updateId', 'asc')
|
.orderBy('album_user.updateId', 'asc')
|
||||||
.where((eb) =>
|
.where((eb) =>
|
||||||
@@ -347,53 +398,53 @@ class AlbumUserSync extends BaseSync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AssetSync extends BaseSync {
|
class AssetSync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getDeletes(userId: string, ack?: SyncAck) {
|
getDeletes({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_audit')
|
.selectFrom('asset_audit')
|
||||||
.select(['id', 'assetId'])
|
.select(['id', 'assetId'])
|
||||||
.where('ownerId', '=', userId)
|
.where('ownerId', '=', userId)
|
||||||
.$call(this.auditTableFilters(ack))
|
.$call(this.auditTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpserts({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset')
|
.selectFrom('asset')
|
||||||
.select(columns.syncAsset)
|
.select(columns.syncAsset)
|
||||||
.select('asset.updateId')
|
.select('asset.updateId')
|
||||||
.where('ownerId', '=', userId)
|
.where('ownerId', '=', userId)
|
||||||
.$call(this.upsertTableFilters(ack))
|
.$call(this.upsertTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthUserSync extends BaseSync {
|
class AuthUserSync extends BaseSync {
|
||||||
@GenerateSql({ params: [], stream: true })
|
@GenerateSql({ params: [], stream: true })
|
||||||
getUpserts(ack?: SyncAck) {
|
getUpserts({ nowId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('user')
|
.selectFrom('user')
|
||||||
.select(columns.syncUser)
|
.select(columns.syncUser)
|
||||||
.select(['isAdmin', 'pinCode', 'oauthId', 'storageLabel', 'quotaSizeInBytes', 'quotaUsageInBytes'])
|
.select(['isAdmin', 'pinCode', 'oauthId', 'storageLabel', 'quotaSizeInBytes', 'quotaUsageInBytes'])
|
||||||
.$call(this.upsertTableFilters(ack))
|
.$call(this.upsertTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PersonSync extends BaseSync {
|
class PersonSync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getDeletes(userId: string, ack?: SyncAck) {
|
getDeletes({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('person_audit')
|
.selectFrom('person_audit')
|
||||||
.select(['id', 'personId'])
|
.select(['id', 'personId'])
|
||||||
.where('ownerId', '=', userId)
|
.where('ownerId', '=', userId)
|
||||||
.$call(this.auditTableFilters(ack))
|
.$call(this.auditTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpserts({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('person')
|
.selectFrom('person')
|
||||||
.select([
|
.select([
|
||||||
@@ -410,27 +461,27 @@ class PersonSync extends BaseSync {
|
|||||||
'faceAssetId',
|
'faceAssetId',
|
||||||
])
|
])
|
||||||
.where('ownerId', '=', userId)
|
.where('ownerId', '=', userId)
|
||||||
.$call(this.upsertTableFilters(ack))
|
.$call(this.upsertTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AssetFaceSync extends BaseSync {
|
class AssetFaceSync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getDeletes(userId: string, ack?: SyncAck) {
|
getDeletes({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_face_audit')
|
.selectFrom('asset_face_audit')
|
||||||
.select(['asset_face_audit.id', 'assetFaceId'])
|
.select(['asset_face_audit.id', 'assetFaceId'])
|
||||||
.orderBy('asset_face_audit.id', 'asc')
|
.orderBy('asset_face_audit.id', 'asc')
|
||||||
.leftJoin('asset', 'asset.id', 'asset_face_audit.assetId')
|
.leftJoin('asset', 'asset.id', 'asset_face_audit.assetId')
|
||||||
.where('asset.ownerId', '=', userId)
|
.where('asset.ownerId', '=', userId)
|
||||||
.where('asset_face_audit.deletedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('asset_face_audit.id', '<', nowId)
|
||||||
.$if(!!ack, (qb) => qb.where('asset_face_audit.id', '>', ack!.updateId))
|
.$if(!!ack, (qb) => qb.where('asset_face_audit.id', '>', ack!.updateId))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpserts({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.select([
|
.select([
|
||||||
@@ -446,7 +497,7 @@ class AssetFaceSync extends BaseSync {
|
|||||||
'sourceType',
|
'sourceType',
|
||||||
'asset_face.updateId',
|
'asset_face.updateId',
|
||||||
])
|
])
|
||||||
.where('asset_face.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('asset_face.updateId', '<', nowId)
|
||||||
.$if(!!ack, (qb) => qb.where('asset_face.updateId', '>', ack!.updateId))
|
.$if(!!ack, (qb) => qb.where('asset_face.updateId', '>', ack!.updateId))
|
||||||
.orderBy('asset_face.updateId', 'asc')
|
.orderBy('asset_face.updateId', 'asc')
|
||||||
.leftJoin('asset', 'asset.id', 'asset_face.assetId')
|
.leftJoin('asset', 'asset.id', 'asset_face.assetId')
|
||||||
@@ -456,31 +507,31 @@ class AssetFaceSync extends BaseSync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AssetExifSync extends BaseSync {
|
class AssetExifSync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpserts({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_exif')
|
.selectFrom('asset_exif')
|
||||||
.select(columns.syncAssetExif)
|
.select(columns.syncAssetExif)
|
||||||
.select('asset_exif.updateId')
|
.select('asset_exif.updateId')
|
||||||
.where('assetId', 'in', (eb) => eb.selectFrom('asset').select('id').where('ownerId', '=', userId))
|
.where('assetId', 'in', (eb) => eb.selectFrom('asset').select('id').where('ownerId', '=', userId))
|
||||||
.$call(this.upsertTableFilters(ack))
|
.$call(this.upsertTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MemorySync extends BaseSync {
|
class MemorySync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getDeletes(userId: string, ack?: SyncAck) {
|
getDeletes({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('memory_audit')
|
.selectFrom('memory_audit')
|
||||||
.select(['id', 'memoryId'])
|
.select(['id', 'memoryId'])
|
||||||
.where('userId', '=', userId)
|
.where('userId', '=', userId)
|
||||||
.$call(this.auditTableFilters(ack))
|
.$call(this.auditTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpserts({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('memory')
|
.selectFrom('memory')
|
||||||
.select([
|
.select([
|
||||||
@@ -499,97 +550,105 @@ class MemorySync extends BaseSync {
|
|||||||
])
|
])
|
||||||
.select('updateId')
|
.select('updateId')
|
||||||
.where('ownerId', '=', userId)
|
.where('ownerId', '=', userId)
|
||||||
.$call(this.upsertTableFilters(ack))
|
.$call(this.upsertTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MemoryToAssetSync extends BaseSync {
|
class MemoryToAssetSync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getDeletes(userId: string, ack?: SyncAck) {
|
getDeletes({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('memory_asset_audit')
|
.selectFrom('memory_asset_audit')
|
||||||
.select(['id', 'memoryId', 'assetId'])
|
.select(['id', 'memoryId', 'assetId'])
|
||||||
.where('memoryId', 'in', (eb) => eb.selectFrom('memory').select('id').where('ownerId', '=', userId))
|
.where('memoryId', 'in', (eb) => eb.selectFrom('memory').select('id').where('ownerId', '=', userId))
|
||||||
.$call(this.auditTableFilters(ack))
|
.$call(this.auditTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpserts({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('memory_asset')
|
.selectFrom('memory_asset')
|
||||||
.select(['memoriesId as memoryId', 'assetsId as assetId'])
|
.select(['memoriesId as memoryId', 'assetsId as assetId'])
|
||||||
.select('updateId')
|
.select('updateId')
|
||||||
.where('memoriesId', 'in', (eb) => eb.selectFrom('memory').select('id').where('ownerId', '=', userId))
|
.where('memoriesId', 'in', (eb) => eb.selectFrom('memory').select('id').where('ownerId', '=', userId))
|
||||||
.$call(this.upsertTableFilters(ack))
|
.$call(this.upsertTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PartnerSync extends BaseSync {
|
class PartnerSync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }] })
|
||||||
getCreatedAfter(userId: string, afterCreateId?: string) {
|
getCreatedAfter({ nowId, userId }: SyncQueryOptions, afterCreateId?: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('partner')
|
.selectFrom('partner')
|
||||||
.select(['sharedById', 'createId'])
|
.select(['sharedById', 'createId'])
|
||||||
.where('sharedWithId', '=', userId)
|
.where('sharedWithId', '=', userId)
|
||||||
.$if(!!afterCreateId, (qb) => qb.where('createId', '>=', afterCreateId!))
|
.$if(!!afterCreateId, (qb) => qb.where('createId', '>=', afterCreateId!))
|
||||||
.where('createdAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('createId', '<', nowId)
|
||||||
.orderBy('partner.createId', 'asc')
|
.orderBy('partner.createId', 'asc')
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getDeletes(userId: string, ack?: SyncAck) {
|
getDeletes({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('partner_audit')
|
.selectFrom('partner_audit')
|
||||||
.select(['id', 'sharedById', 'sharedWithId'])
|
.select(['id', 'sharedById', 'sharedWithId'])
|
||||||
.where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)]))
|
.where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)]))
|
||||||
.$call(this.auditTableFilters(ack))
|
.$call(this.auditTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpserts({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('partner')
|
.selectFrom('partner')
|
||||||
.select(['sharedById', 'sharedWithId', 'inTimeline', 'updateId'])
|
.select(['sharedById', 'sharedWithId', 'inTimeline', 'updateId'])
|
||||||
.where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)]))
|
.where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)]))
|
||||||
.$call(this.upsertTableFilters(ack))
|
.$call(this.upsertTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PartnerAssetsSync extends BaseSync {
|
class PartnerAssetsSync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
@GenerateSql({
|
||||||
getBackfill(partnerId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }, DummyValue.UUID, DummyValue.UUID, DummyValue.UUID],
|
||||||
|
stream: true,
|
||||||
|
})
|
||||||
|
getBackfill(
|
||||||
|
{ nowId }: SyncQueryOptions,
|
||||||
|
partnerId: string,
|
||||||
|
afterUpdateId: string | undefined,
|
||||||
|
beforeUpdateId: string,
|
||||||
|
) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset')
|
.selectFrom('asset')
|
||||||
.select(columns.syncAsset)
|
.select(columns.syncAsset)
|
||||||
.select('asset.updateId')
|
.select('asset.updateId')
|
||||||
.where('ownerId', '=', partnerId)
|
.where('ownerId', '=', partnerId)
|
||||||
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('updateId', '<', nowId)
|
||||||
.where('updateId', '<=', beforeUpdateId)
|
.where('updateId', '<=', beforeUpdateId)
|
||||||
.$if(!!afterUpdateId, (eb) => eb.where('updateId', '>=', afterUpdateId!))
|
.$if(!!afterUpdateId, (eb) => eb.where('updateId', '>=', afterUpdateId!))
|
||||||
.orderBy('updateId', 'asc')
|
.orderBy('updateId', 'asc')
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getDeletes(userId: string, ack?: SyncAck) {
|
getDeletes({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_audit')
|
.selectFrom('asset_audit')
|
||||||
.select(['id', 'assetId'])
|
.select(['id', 'assetId'])
|
||||||
.where('ownerId', 'in', (eb) =>
|
.where('ownerId', 'in', (eb) =>
|
||||||
eb.selectFrom('partner').select(['sharedById']).where('sharedWithId', '=', userId),
|
eb.selectFrom('partner').select(['sharedById']).where('sharedWithId', '=', userId),
|
||||||
)
|
)
|
||||||
.$call(this.auditTableFilters(ack))
|
.$call(this.auditTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpserts({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset')
|
.selectFrom('asset')
|
||||||
.select(columns.syncAsset)
|
.select(columns.syncAsset)
|
||||||
@@ -597,29 +656,37 @@ class PartnerAssetsSync extends BaseSync {
|
|||||||
.where('ownerId', 'in', (eb) =>
|
.where('ownerId', 'in', (eb) =>
|
||||||
eb.selectFrom('partner').select(['sharedById']).where('sharedWithId', '=', userId),
|
eb.selectFrom('partner').select(['sharedById']).where('sharedWithId', '=', userId),
|
||||||
)
|
)
|
||||||
.$call(this.upsertTableFilters(ack))
|
.$call(this.upsertTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PartnerAssetExifsSync extends BaseSync {
|
class PartnerAssetExifsSync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
@GenerateSql({
|
||||||
getBackfill(partnerId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }, DummyValue.UUID, DummyValue.UUID, DummyValue.UUID],
|
||||||
|
stream: true,
|
||||||
|
})
|
||||||
|
getBackfill(
|
||||||
|
{ nowId }: SyncQueryOptions,
|
||||||
|
partnerId: string,
|
||||||
|
afterUpdateId: string | undefined,
|
||||||
|
beforeUpdateId: string,
|
||||||
|
) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_exif')
|
.selectFrom('asset_exif')
|
||||||
.select(columns.syncAssetExif)
|
.select(columns.syncAssetExif)
|
||||||
.select('asset_exif.updateId')
|
.select('asset_exif.updateId')
|
||||||
.innerJoin('asset', 'asset.id', 'asset_exif.assetId')
|
.innerJoin('asset', 'asset.id', 'asset_exif.assetId')
|
||||||
.where('asset.ownerId', '=', partnerId)
|
.where('asset.ownerId', '=', partnerId)
|
||||||
.where('asset_exif.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('asset_exif.updateId', '<', nowId)
|
||||||
.where('asset_exif.updateId', '<=', beforeUpdateId)
|
.where('asset_exif.updateId', '<=', beforeUpdateId)
|
||||||
.$if(!!afterUpdateId, (eb) => eb.where('asset_exif.updateId', '>=', afterUpdateId!))
|
.$if(!!afterUpdateId, (eb) => eb.where('asset_exif.updateId', '>=', afterUpdateId!))
|
||||||
.orderBy('asset_exif.updateId', 'asc')
|
.orderBy('asset_exif.updateId', 'asc')
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpserts({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_exif')
|
.selectFrom('asset_exif')
|
||||||
.select(columns.syncAssetExif)
|
.select(columns.syncAssetExif)
|
||||||
@@ -632,61 +699,69 @@ class PartnerAssetExifsSync extends BaseSync {
|
|||||||
eb.selectFrom('partner').select(['sharedById']).where('sharedWithId', '=', userId),
|
eb.selectFrom('partner').select(['sharedById']).where('sharedWithId', '=', userId),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.$call(this.upsertTableFilters(ack))
|
.$call(this.upsertTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StackSync extends BaseSync {
|
class StackSync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getDeletes(userId: string, ack?: SyncAck) {
|
getDeletes({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('stack_audit')
|
.selectFrom('stack_audit')
|
||||||
.select(['id', 'stackId'])
|
.select(['id', 'stackId'])
|
||||||
.where('userId', '=', userId)
|
.where('userId', '=', userId)
|
||||||
.$call(this.auditTableFilters(ack))
|
.$call(this.auditTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpserts({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('stack')
|
.selectFrom('stack')
|
||||||
.select(columns.syncStack)
|
.select(columns.syncStack)
|
||||||
.select('updateId')
|
.select('updateId')
|
||||||
.where('ownerId', '=', userId)
|
.where('ownerId', '=', userId)
|
||||||
.$call(this.upsertTableFilters(ack))
|
.$call(this.upsertTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PartnerStackSync extends BaseSync {
|
class PartnerStackSync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getDeletes(userId: string, ack?: SyncAck) {
|
getDeletes({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('stack_audit')
|
.selectFrom('stack_audit')
|
||||||
.select(['id', 'stackId'])
|
.select(['id', 'stackId'])
|
||||||
.where('userId', 'in', (eb) => eb.selectFrom('partner').select(['sharedById']).where('sharedWithId', '=', userId))
|
.where('userId', 'in', (eb) => eb.selectFrom('partner').select(['sharedById']).where('sharedWithId', '=', userId))
|
||||||
.$call(this.auditTableFilters(ack))
|
.$call(this.auditTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
@GenerateSql({
|
||||||
getBackfill(partnerId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }, DummyValue.UUID, DummyValue.UUID, DummyValue.UUID],
|
||||||
|
stream: true,
|
||||||
|
})
|
||||||
|
getBackfill(
|
||||||
|
{ nowId }: SyncQueryOptions,
|
||||||
|
partnerId: string,
|
||||||
|
afterUpdateId: string | undefined,
|
||||||
|
beforeUpdateId: string,
|
||||||
|
) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('stack')
|
.selectFrom('stack')
|
||||||
.select(columns.syncStack)
|
.select(columns.syncStack)
|
||||||
.select('updateId')
|
.select('updateId')
|
||||||
.where('ownerId', '=', partnerId)
|
.where('ownerId', '=', partnerId)
|
||||||
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('updateId', '<', nowId)
|
||||||
.where('updateId', '<=', beforeUpdateId)
|
.where('updateId', '<=', beforeUpdateId)
|
||||||
.$if(!!afterUpdateId, (eb) => eb.where('updateId', '>=', afterUpdateId!))
|
.$if(!!afterUpdateId, (eb) => eb.where('updateId', '>=', afterUpdateId!))
|
||||||
.orderBy('updateId', 'asc')
|
.orderBy('updateId', 'asc')
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpserts({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('stack')
|
.selectFrom('stack')
|
||||||
.select(columns.syncStack)
|
.select(columns.syncStack)
|
||||||
@@ -694,41 +769,41 @@ class PartnerStackSync extends BaseSync {
|
|||||||
.where('ownerId', 'in', (eb) =>
|
.where('ownerId', 'in', (eb) =>
|
||||||
eb.selectFrom('partner').select(['sharedById']).where('sharedWithId', '=', userId),
|
eb.selectFrom('partner').select(['sharedById']).where('sharedWithId', '=', userId),
|
||||||
)
|
)
|
||||||
.$call(this.upsertTableFilters(ack))
|
.$call(this.upsertTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserSync extends BaseSync {
|
class UserSync extends BaseSync {
|
||||||
@GenerateSql({ params: [], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getDeletes(ack?: SyncAck) {
|
getDeletes({ nowId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db.selectFrom('user_audit').select(['id', 'userId']).$call(this.auditTableFilters(ack)).stream();
|
return this.db.selectFrom('user_audit').select(['id', 'userId']).$call(this.auditTableFilters(nowId, ack)).stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(ack?: SyncAck) {
|
getUpserts({ nowId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db.selectFrom('user').select(columns.syncUser).$call(this.upsertTableFilters(ack)).stream();
|
return this.db.selectFrom('user').select(columns.syncUser).$call(this.upsertTableFilters(nowId, ack)).stream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserMetadataSync extends BaseSync {
|
class UserMetadataSync extends BaseSync {
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getDeletes(userId: string, ack?: SyncAck) {
|
getDeletes({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('user_metadata_audit')
|
.selectFrom('user_metadata_audit')
|
||||||
.select(['id', 'userId', 'key'])
|
.select(['id', 'userId', 'key'])
|
||||||
.where('userId', '=', userId)
|
.where('userId', '=', userId)
|
||||||
.$call(this.auditTableFilters(ack))
|
.$call(this.auditTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [{ nowId: DummyValue.UUID, userId: DummyValue.UUID }], stream: true })
|
||||||
getUpserts(userId: string, ack?: SyncAck) {
|
getUpserts({ nowId, userId }: SyncQueryOptions, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('user_metadata')
|
.selectFrom('user_metadata')
|
||||||
.select(['userId', 'key', 'value', 'updateId'])
|
.select(['userId', 'key', 'value', 'updateId'])
|
||||||
.where('userId', '=', userId)
|
.where('userId', '=', userId)
|
||||||
.$call(this.upsertTableFilters(ack))
|
.$call(this.upsertTableFilters(nowId, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`DELETE FROM session_sync_checkpoint WHERE type IN ('AlbumAssetBackfillV1', 'AlbumAssetExifV1', 'AlbumAssetV1')`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(): Promise<void> {}
|
||||||
@@ -76,23 +76,36 @@ export class ApiService {
|
|||||||
let status = 200;
|
let status = 200;
|
||||||
let html = index;
|
let html = index;
|
||||||
|
|
||||||
const shareMatches = request.url.match(/^\/share\/(.+)$/);
|
const defaultDomain = request.host ? `${request.protocol}://${request.host}` : undefined;
|
||||||
if (shareMatches) {
|
|
||||||
|
let meta: OpenGraphTags | null = null;
|
||||||
|
|
||||||
|
const shareKey = request.url.match(/^\/share\/(.+)$/);
|
||||||
|
if (shareKey) {
|
||||||
try {
|
try {
|
||||||
const key = shareMatches[1];
|
const key = shareKey[1];
|
||||||
const auth = await this.authService.validateSharedLinkKey(key);
|
const auth = await this.authService.validateSharedLinkKey(key);
|
||||||
const meta = await this.sharedLinkService.getMetadataTags(
|
meta = await this.sharedLinkService.getMetadataTags(auth, defaultDomain);
|
||||||
auth,
|
|
||||||
request.host ? `${request.protocol}://${request.host}` : undefined,
|
|
||||||
);
|
|
||||||
if (meta) {
|
|
||||||
html = render(index, meta);
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
status = 404;
|
status = 404;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shareSlug = request.url.match(/^\/s\/(.+)$/);
|
||||||
|
if (shareSlug) {
|
||||||
|
try {
|
||||||
|
const slug = shareSlug[1];
|
||||||
|
const auth = await this.authService.validateSharedLinkSlug(slug);
|
||||||
|
meta = await this.sharedLinkService.getMetadataTags(auth, defaultDomain);
|
||||||
|
} catch {
|
||||||
|
status = 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meta) {
|
||||||
|
html = render(index, meta);
|
||||||
|
}
|
||||||
|
|
||||||
res.status(status).type('text/html').header('Cache-Control', 'no-store').send(html);
|
res.status(status).type('text/html').header('Cache-Control', 'no-store').send(html);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,7 +241,6 @@ describe(AuthService.name, () => {
|
|||||||
const sessionWithToken = {
|
const sessionWithToken = {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
updatedAt: session.updatedAt,
|
updatedAt: session.updatedAt,
|
||||||
isPendingSyncReset: false,
|
|
||||||
user: factory.authUser(),
|
user: factory.authUser(),
|
||||||
pinExpiresAt: null,
|
pinExpiresAt: null,
|
||||||
};
|
};
|
||||||
@@ -259,7 +258,6 @@ describe(AuthService.name, () => {
|
|||||||
session: {
|
session: {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
hasElevatedPermission: false,
|
hasElevatedPermission: false,
|
||||||
isPendingSyncReset: session.isPendingSyncReset,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -409,7 +407,6 @@ describe(AuthService.name, () => {
|
|||||||
id: session.id,
|
id: session.id,
|
||||||
updatedAt: session.updatedAt,
|
updatedAt: session.updatedAt,
|
||||||
user: factory.authUser(),
|
user: factory.authUser(),
|
||||||
isPendingSyncReset: false,
|
|
||||||
pinExpiresAt: null,
|
pinExpiresAt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -426,7 +423,6 @@ describe(AuthService.name, () => {
|
|||||||
session: {
|
session: {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
hasElevatedPermission: false,
|
hasElevatedPermission: false,
|
||||||
isPendingSyncReset: session.isPendingSyncReset,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -487,7 +487,6 @@ export class AuthService extends BaseService {
|
|||||||
user: session.user,
|
user: session.user,
|
||||||
session: {
|
session: {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
isPendingSyncReset: session.isPendingSyncReset,
|
|
||||||
hasElevatedPermission,
|
hasElevatedPermission,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export class BackupService extends BaseService {
|
|||||||
return oldBackupStyle || newBackupStyle;
|
return oldBackupStyle || newBackupStyle;
|
||||||
})
|
})
|
||||||
.sort()
|
.sort()
|
||||||
.reverse();
|
.toReversed();
|
||||||
|
|
||||||
const toDelete = backups.slice(config.keepLastAmount);
|
const toDelete = backups.slice(config.keepLastAmount);
|
||||||
toDelete.push(...failedBackups);
|
toDelete.push(...failedBackups);
|
||||||
|
|||||||
+164
-101
@@ -16,13 +16,14 @@ import {
|
|||||||
SyncStreamDto,
|
SyncStreamDto,
|
||||||
} from 'src/dtos/sync.dto';
|
} from 'src/dtos/sync.dto';
|
||||||
import { AssetVisibility, DatabaseAction, EntityType, Permission, SyncEntityType, SyncRequestType } from 'src/enum';
|
import { AssetVisibility, DatabaseAction, EntityType, Permission, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { SyncQueryOptions } from 'src/repositories/sync.repository';
|
||||||
import { SessionSyncCheckpointTable } from 'src/schema/tables/sync-checkpoint.table';
|
import { SessionSyncCheckpointTable } from 'src/schema/tables/sync-checkpoint.table';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { SyncAck } from 'src/types';
|
import { SyncAck } from 'src/types';
|
||||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||||
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||||
import { setIsEqual } from 'src/utils/set';
|
import { setIsEqual } from 'src/utils/set';
|
||||||
import { fromAck, mapJsonLine, serialize, SerializeOptions, toAck } from 'src/utils/sync';
|
import { fromAck, serialize, SerializeOptions, toAck } from 'src/utils/sync';
|
||||||
|
|
||||||
type CheckpointMap = Partial<Record<SyncEntityType, SyncAck>>;
|
type CheckpointMap = Partial<Record<SyncEntityType, SyncAck>>;
|
||||||
type AssetLike = Omit<SyncAssetV1, 'checksum' | 'thumbhash'> & {
|
type AssetLike = Omit<SyncAssetV1, 'checksum' | 'thumbhash'> & {
|
||||||
@@ -99,6 +100,10 @@ export class SyncService extends BaseService {
|
|||||||
const checkpoints: Record<string, Insertable<SessionSyncCheckpointTable>> = {};
|
const checkpoints: Record<string, Insertable<SessionSyncCheckpointTable>> = {};
|
||||||
for (const ack of dto.acks) {
|
for (const ack of dto.acks) {
|
||||||
const { type } = fromAck(ack);
|
const { type } = fromAck(ack);
|
||||||
|
if (type === SyncEntityType.SyncResetV1) {
|
||||||
|
await this.sessionRepository.resetSyncProgress(sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// TODO proper ack validation via class validator
|
// TODO proper ack validation via class validator
|
||||||
if (!Object.values(SyncEntityType).includes(type)) {
|
if (!Object.values(SyncEntityType).includes(type)) {
|
||||||
throw new BadRequestException(`Invalid ack type: ${type}`);
|
throw new BadRequestException(`Invalid ack type: ${type}`);
|
||||||
@@ -128,39 +133,44 @@ export class SyncService extends BaseService {
|
|||||||
|
|
||||||
if (dto.reset) {
|
if (dto.reset) {
|
||||||
await this.sessionRepository.resetSyncProgress(session.id);
|
await this.sessionRepository.resetSyncProgress(session.id);
|
||||||
session.isPendingSyncReset = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.isPendingSyncReset) {
|
const isPendingSyncReset = await this.sessionRepository.isPendingSyncReset(session.id);
|
||||||
response.write(mapJsonLine({ type: SyncEntityType.SyncResetV1, data: {} }));
|
|
||||||
|
if (isPendingSyncReset) {
|
||||||
|
send(response, { type: SyncEntityType.SyncResetV1, ids: ['reset'], data: {} });
|
||||||
response.end();
|
response.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { nowId } = await this.syncCheckpointRepository.getNow();
|
||||||
|
const options: SyncQueryOptions = { nowId, userId: auth.user.id };
|
||||||
|
|
||||||
const checkpoints = await this.syncCheckpointRepository.getAll(session.id);
|
const checkpoints = await this.syncCheckpointRepository.getAll(session.id);
|
||||||
const checkpointMap: CheckpointMap = Object.fromEntries(checkpoints.map(({ type, ack }) => [type, fromAck(ack)]));
|
const checkpointMap: CheckpointMap = Object.fromEntries(checkpoints.map(({ type, ack }) => [type, fromAck(ack)]));
|
||||||
|
|
||||||
const handlers: Record<SyncRequestType, () => Promise<void>> = {
|
const handlers: Record<SyncRequestType, () => Promise<void>> = {
|
||||||
[SyncRequestType.AuthUsersV1]: () => this.syncAuthUsersV1(response, checkpointMap),
|
[SyncRequestType.AuthUsersV1]: () => this.syncAuthUsersV1(options, response, checkpointMap),
|
||||||
[SyncRequestType.UsersV1]: () => this.syncUsersV1(response, checkpointMap),
|
[SyncRequestType.UsersV1]: () => this.syncUsersV1(options, response, checkpointMap),
|
||||||
[SyncRequestType.PartnersV1]: () => this.syncPartnersV1(response, checkpointMap, auth),
|
[SyncRequestType.PartnersV1]: () => this.syncPartnersV1(options, response, checkpointMap),
|
||||||
[SyncRequestType.AssetsV1]: () => this.syncAssetsV1(response, checkpointMap, auth),
|
[SyncRequestType.AssetsV1]: () => this.syncAssetsV1(options, response, checkpointMap),
|
||||||
[SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(response, checkpointMap, auth),
|
[SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(options, response, checkpointMap),
|
||||||
[SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(response, checkpointMap, auth, session.id),
|
[SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(options, response, checkpointMap, session.id),
|
||||||
[SyncRequestType.PartnerAssetExifsV1]: () =>
|
[SyncRequestType.PartnerAssetExifsV1]: () =>
|
||||||
this.syncPartnerAssetExifsV1(response, checkpointMap, auth, session.id),
|
this.syncPartnerAssetExifsV1(options, response, checkpointMap, session.id),
|
||||||
[SyncRequestType.AlbumsV1]: () => this.syncAlbumsV1(response, checkpointMap, auth),
|
[SyncRequestType.AlbumsV1]: () => this.syncAlbumsV1(options, response, checkpointMap),
|
||||||
[SyncRequestType.AlbumUsersV1]: () => this.syncAlbumUsersV1(response, checkpointMap, auth, session.id),
|
[SyncRequestType.AlbumUsersV1]: () => this.syncAlbumUsersV1(options, response, checkpointMap, session.id),
|
||||||
[SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(response, checkpointMap, auth, session.id),
|
[SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(options, response, checkpointMap, session.id),
|
||||||
[SyncRequestType.AlbumToAssetsV1]: () => this.syncAlbumToAssetsV1(response, checkpointMap, auth, session.id),
|
[SyncRequestType.AlbumToAssetsV1]: () => this.syncAlbumToAssetsV1(options, response, checkpointMap, session.id),
|
||||||
[SyncRequestType.AlbumAssetExifsV1]: () => this.syncAlbumAssetExifsV1(response, checkpointMap, auth, session.id),
|
[SyncRequestType.AlbumAssetExifsV1]: () =>
|
||||||
[SyncRequestType.MemoriesV1]: () => this.syncMemoriesV1(response, checkpointMap, auth),
|
this.syncAlbumAssetExifsV1(options, response, checkpointMap, session.id),
|
||||||
[SyncRequestType.MemoryToAssetsV1]: () => this.syncMemoryAssetsV1(response, checkpointMap, auth),
|
[SyncRequestType.MemoriesV1]: () => this.syncMemoriesV1(options, response, checkpointMap),
|
||||||
[SyncRequestType.StacksV1]: () => this.syncStackV1(response, checkpointMap, auth),
|
[SyncRequestType.MemoryToAssetsV1]: () => this.syncMemoryAssetsV1(options, response, checkpointMap),
|
||||||
[SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(response, checkpointMap, auth, session.id),
|
[SyncRequestType.StacksV1]: () => this.syncStackV1(options, response, checkpointMap),
|
||||||
[SyncRequestType.PeopleV1]: () => this.syncPeopleV1(response, checkpointMap, auth),
|
[SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(options, response, checkpointMap, session.id),
|
||||||
[SyncRequestType.AssetFacesV1]: async () => this.syncAssetFacesV1(response, checkpointMap, auth),
|
[SyncRequestType.PeopleV1]: () => this.syncPeopleV1(options, response, checkpointMap),
|
||||||
[SyncRequestType.UserMetadataV1]: () => this.syncUserMetadataV1(response, checkpointMap, auth),
|
[SyncRequestType.AssetFacesV1]: async () => this.syncAssetFacesV1(options, response, checkpointMap),
|
||||||
|
[SyncRequestType.UserMetadataV1]: () => this.syncUserMetadataV1(options, response, checkpointMap),
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const type of SYNC_TYPES_ORDER.filter((type) => dto.types.includes(type))) {
|
for (const type of SYNC_TYPES_ORDER.filter((type) => dto.types.includes(type))) {
|
||||||
@@ -171,71 +181,71 @@ export class SyncService extends BaseService {
|
|||||||
response.end();
|
response.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncAuthUsersV1(response: Writable, checkpointMap: CheckpointMap) {
|
private async syncAuthUsersV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||||
const upsertType = SyncEntityType.AuthUserV1;
|
const upsertType = SyncEntityType.AuthUserV1;
|
||||||
const upserts = this.syncRepository.authUser.getUpserts(checkpointMap[upsertType]);
|
const upserts = this.syncRepository.authUser.getUpserts(options, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, profileImagePath, ...data } of upserts) {
|
for await (const { updateId, profileImagePath, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data: { ...data, hasProfileImage: !!profileImagePath } });
|
send(response, { type: upsertType, ids: [updateId], data: { ...data, hasProfileImage: !!profileImagePath } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncUsersV1(response: Writable, checkpointMap: CheckpointMap) {
|
private async syncUsersV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||||
const deleteType = SyncEntityType.UserDeleteV1;
|
const deleteType = SyncEntityType.UserDeleteV1;
|
||||||
const deletes = this.syncRepository.user.getDeletes(checkpointMap[deleteType]);
|
const deletes = this.syncRepository.user.getDeletes(options, checkpointMap[deleteType]);
|
||||||
for await (const { id, ...data } of deletes) {
|
for await (const { id, ...data } of deletes) {
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
}
|
}
|
||||||
|
|
||||||
const upsertType = SyncEntityType.UserV1;
|
const upsertType = SyncEntityType.UserV1;
|
||||||
const upserts = this.syncRepository.user.getUpserts(checkpointMap[upsertType]);
|
const upserts = this.syncRepository.user.getUpserts(options, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, profileImagePath, ...data } of upserts) {
|
for await (const { updateId, profileImagePath, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data: { ...data, hasProfileImage: !!profileImagePath } });
|
send(response, { type: upsertType, ids: [updateId], data: { ...data, hasProfileImage: !!profileImagePath } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncPartnersV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
private async syncPartnersV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||||
const deleteType = SyncEntityType.PartnerDeleteV1;
|
const deleteType = SyncEntityType.PartnerDeleteV1;
|
||||||
const deletes = this.syncRepository.partner.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
const deletes = this.syncRepository.partner.getDeletes(options, checkpointMap[deleteType]);
|
||||||
for await (const { id, ...data } of deletes) {
|
for await (const { id, ...data } of deletes) {
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
}
|
}
|
||||||
|
|
||||||
const upsertType = SyncEntityType.PartnerV1;
|
const upsertType = SyncEntityType.PartnerV1;
|
||||||
const upserts = this.syncRepository.partner.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
const upserts = this.syncRepository.partner.getUpserts(options, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncAssetsV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
private async syncAssetsV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||||
const deleteType = SyncEntityType.AssetDeleteV1;
|
const deleteType = SyncEntityType.AssetDeleteV1;
|
||||||
const deletes = this.syncRepository.asset.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
const deletes = this.syncRepository.asset.getDeletes(options, checkpointMap[deleteType]);
|
||||||
for await (const { id, ...data } of deletes) {
|
for await (const { id, ...data } of deletes) {
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
}
|
}
|
||||||
|
|
||||||
const upsertType = SyncEntityType.AssetV1;
|
const upsertType = SyncEntityType.AssetV1;
|
||||||
const upserts = this.syncRepository.asset.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
const upserts = this.syncRepository.asset.getUpserts(options, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV1(data) });
|
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncPartnerAssetsV1(
|
private async syncPartnerAssetsV1(
|
||||||
|
options: SyncQueryOptions,
|
||||||
response: Writable,
|
response: Writable,
|
||||||
checkpointMap: CheckpointMap,
|
checkpointMap: CheckpointMap,
|
||||||
auth: AuthDto,
|
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
) {
|
) {
|
||||||
const deleteType = SyncEntityType.PartnerAssetDeleteV1;
|
const deleteType = SyncEntityType.PartnerAssetDeleteV1;
|
||||||
const deletes = this.syncRepository.partnerAsset.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
const deletes = this.syncRepository.partnerAsset.getDeletes(options, checkpointMap[deleteType]);
|
||||||
for await (const { id, ...data } of deletes) {
|
for await (const { id, ...data } of deletes) {
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
}
|
}
|
||||||
|
|
||||||
const backfillType = SyncEntityType.PartnerAssetBackfillV1;
|
const backfillType = SyncEntityType.PartnerAssetBackfillV1;
|
||||||
const backfillCheckpoint = checkpointMap[backfillType];
|
const backfillCheckpoint = checkpointMap[backfillType];
|
||||||
const partners = await this.syncRepository.partner.getCreatedAfter(auth.user.id, backfillCheckpoint?.updateId);
|
const partners = await this.syncRepository.partner.getCreatedAfter(options, backfillCheckpoint?.updateId);
|
||||||
const upsertType = SyncEntityType.PartnerAssetV1;
|
const upsertType = SyncEntityType.PartnerAssetV1;
|
||||||
const upsertCheckpoint = checkpointMap[upsertType];
|
const upsertCheckpoint = checkpointMap[upsertType];
|
||||||
if (upsertCheckpoint) {
|
if (upsertCheckpoint) {
|
||||||
@@ -248,7 +258,7 @@ export class SyncService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startId = getStartId(createId, backfillCheckpoint);
|
const startId = getStartId(createId, backfillCheckpoint);
|
||||||
const backfill = this.syncRepository.partnerAsset.getBackfill(partner.sharedById, startId, endId);
|
const backfill = this.syncRepository.partnerAsset.getBackfill(options, partner.sharedById, startId, endId);
|
||||||
|
|
||||||
for await (const { updateId, ...data } of backfill) {
|
for await (const { updateId, ...data } of backfill) {
|
||||||
send(response, {
|
send(response, {
|
||||||
@@ -268,29 +278,29 @@ export class SyncService extends BaseService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const upserts = this.syncRepository.partnerAsset.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
const upserts = this.syncRepository.partnerAsset.getUpserts(options, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV1(data) });
|
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncAssetExifsV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
private async syncAssetExifsV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||||
const upsertType = SyncEntityType.AssetExifV1;
|
const upsertType = SyncEntityType.AssetExifV1;
|
||||||
const upserts = this.syncRepository.assetExif.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
const upserts = this.syncRepository.assetExif.getUpserts(options, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncPartnerAssetExifsV1(
|
private async syncPartnerAssetExifsV1(
|
||||||
|
options: SyncQueryOptions,
|
||||||
response: Writable,
|
response: Writable,
|
||||||
checkpointMap: CheckpointMap,
|
checkpointMap: CheckpointMap,
|
||||||
auth: AuthDto,
|
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
) {
|
) {
|
||||||
const backfillType = SyncEntityType.PartnerAssetExifBackfillV1;
|
const backfillType = SyncEntityType.PartnerAssetExifBackfillV1;
|
||||||
const backfillCheckpoint = checkpointMap[backfillType];
|
const backfillCheckpoint = checkpointMap[backfillType];
|
||||||
const partners = await this.syncRepository.partner.getCreatedAfter(auth.user.id, backfillCheckpoint?.updateId);
|
const partners = await this.syncRepository.partner.getCreatedAfter(options, backfillCheckpoint?.updateId);
|
||||||
|
|
||||||
const upsertType = SyncEntityType.PartnerAssetExifV1;
|
const upsertType = SyncEntityType.PartnerAssetExifV1;
|
||||||
const upsertCheckpoint = checkpointMap[upsertType];
|
const upsertCheckpoint = checkpointMap[upsertType];
|
||||||
@@ -304,7 +314,7 @@ export class SyncService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startId = getStartId(createId, backfillCheckpoint);
|
const startId = getStartId(createId, backfillCheckpoint);
|
||||||
const backfill = this.syncRepository.partnerAssetExif.getBackfill(partner.sharedById, startId, endId);
|
const backfill = this.syncRepository.partnerAssetExif.getBackfill(options, partner.sharedById, startId, endId);
|
||||||
|
|
||||||
for await (const { updateId, ...data } of backfill) {
|
for await (const { updateId, ...data } of backfill) {
|
||||||
send(response, { type: backfillType, ids: [partner.createId, updateId], data });
|
send(response, { type: backfillType, ids: [partner.createId, updateId], data });
|
||||||
@@ -320,36 +330,41 @@ export class SyncService extends BaseService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const upserts = this.syncRepository.partnerAssetExif.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
const upserts = this.syncRepository.partnerAssetExif.getUpserts(options, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncAlbumsV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
private async syncAlbumsV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||||
const deleteType = SyncEntityType.AlbumDeleteV1;
|
const deleteType = SyncEntityType.AlbumDeleteV1;
|
||||||
const deletes = this.syncRepository.album.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
const deletes = this.syncRepository.album.getDeletes(options, checkpointMap[deleteType]);
|
||||||
for await (const { id, ...data } of deletes) {
|
for await (const { id, ...data } of deletes) {
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
}
|
}
|
||||||
|
|
||||||
const upsertType = SyncEntityType.AlbumV1;
|
const upsertType = SyncEntityType.AlbumV1;
|
||||||
const upserts = this.syncRepository.album.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
const upserts = this.syncRepository.album.getUpserts(options, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncAlbumUsersV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto, sessionId: string) {
|
private async syncAlbumUsersV1(
|
||||||
|
options: SyncQueryOptions,
|
||||||
|
response: Writable,
|
||||||
|
checkpointMap: CheckpointMap,
|
||||||
|
sessionId: string,
|
||||||
|
) {
|
||||||
const deleteType = SyncEntityType.AlbumUserDeleteV1;
|
const deleteType = SyncEntityType.AlbumUserDeleteV1;
|
||||||
const deletes = this.syncRepository.albumUser.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
const deletes = this.syncRepository.albumUser.getDeletes(options, checkpointMap[deleteType]);
|
||||||
for await (const { id, ...data } of deletes) {
|
for await (const { id, ...data } of deletes) {
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
}
|
}
|
||||||
|
|
||||||
const backfillType = SyncEntityType.AlbumUserBackfillV1;
|
const backfillType = SyncEntityType.AlbumUserBackfillV1;
|
||||||
const backfillCheckpoint = checkpointMap[backfillType];
|
const backfillCheckpoint = checkpointMap[backfillType];
|
||||||
const albums = await this.syncRepository.album.getCreatedAfter(auth.user.id, backfillCheckpoint?.updateId);
|
const albums = await this.syncRepository.album.getCreatedAfter(options, backfillCheckpoint?.updateId);
|
||||||
const upsertType = SyncEntityType.AlbumUserV1;
|
const upsertType = SyncEntityType.AlbumUserV1;
|
||||||
const upsertCheckpoint = checkpointMap[upsertType];
|
const upsertCheckpoint = checkpointMap[upsertType];
|
||||||
if (upsertCheckpoint) {
|
if (upsertCheckpoint) {
|
||||||
@@ -362,7 +377,7 @@ export class SyncService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startId = getStartId(createId, backfillCheckpoint);
|
const startId = getStartId(createId, backfillCheckpoint);
|
||||||
const backfill = this.syncRepository.albumUser.getBackfill(album.id, startId, endId);
|
const backfill = this.syncRepository.albumUser.getBackfill(options, album.id, startId, endId);
|
||||||
|
|
||||||
for await (const { updateId, ...data } of backfill) {
|
for await (const { updateId, ...data } of backfill) {
|
||||||
send(response, { type: backfillType, ids: [createId, updateId], data });
|
send(response, { type: backfillType, ids: [createId, updateId], data });
|
||||||
@@ -378,20 +393,27 @@ export class SyncService extends BaseService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const upserts = this.syncRepository.albumUser.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
const upserts = this.syncRepository.albumUser.getUpserts(options, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncAlbumAssetsV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto, sessionId: string) {
|
private async syncAlbumAssetsV1(
|
||||||
|
options: SyncQueryOptions,
|
||||||
|
response: Writable,
|
||||||
|
checkpointMap: CheckpointMap,
|
||||||
|
sessionId: string,
|
||||||
|
) {
|
||||||
const backfillType = SyncEntityType.AlbumAssetBackfillV1;
|
const backfillType = SyncEntityType.AlbumAssetBackfillV1;
|
||||||
const backfillCheckpoint = checkpointMap[backfillType];
|
const backfillCheckpoint = checkpointMap[backfillType];
|
||||||
const albums = await this.syncRepository.album.getCreatedAfter(auth.user.id, backfillCheckpoint?.updateId);
|
const albums = await this.syncRepository.album.getCreatedAfter(options, backfillCheckpoint?.updateId);
|
||||||
const upsertType = SyncEntityType.AlbumAssetV1;
|
const updateType = SyncEntityType.AlbumAssetUpdateV1;
|
||||||
const upsertCheckpoint = checkpointMap[upsertType];
|
const createType = SyncEntityType.AlbumAssetCreateV1;
|
||||||
if (upsertCheckpoint) {
|
const updateCheckpoint = checkpointMap[updateType];
|
||||||
const endId = upsertCheckpoint.updateId;
|
const createCheckpoint = checkpointMap[createType];
|
||||||
|
if (createCheckpoint) {
|
||||||
|
const endId = createCheckpoint.updateId;
|
||||||
|
|
||||||
for (const album of albums) {
|
for (const album of albums) {
|
||||||
const createId = album.createId;
|
const createId = album.createId;
|
||||||
@@ -400,7 +422,7 @@ export class SyncService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startId = getStartId(createId, backfillCheckpoint);
|
const startId = getStartId(createId, backfillCheckpoint);
|
||||||
const backfill = this.syncRepository.albumAsset.getBackfill(album.id, startId, endId);
|
const backfill = this.syncRepository.albumAsset.getBackfill(options, album.id, startId, endId);
|
||||||
|
|
||||||
for await (const { updateId, ...data } of backfill) {
|
for await (const { updateId, ...data } of backfill) {
|
||||||
send(response, { type: backfillType, ids: [createId, updateId], data: mapSyncAssetV1(data) });
|
send(response, { type: backfillType, ids: [createId, updateId], data: mapSyncAssetV1(data) });
|
||||||
@@ -416,25 +438,44 @@ export class SyncService extends BaseService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const upserts = this.syncRepository.albumAsset.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
if (createCheckpoint) {
|
||||||
for await (const { updateId, ...data } of upserts) {
|
const updates = this.syncRepository.albumAsset.getUpdates(options, createCheckpoint, updateCheckpoint);
|
||||||
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV1(data) });
|
for await (const { updateId, ...data } of updates) {
|
||||||
|
send(response, { type: updateType, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const creates = this.syncRepository.albumAsset.getCreates(options, createCheckpoint);
|
||||||
|
let first = true;
|
||||||
|
for await (const { updateId, ...data } of creates) {
|
||||||
|
if (first) {
|
||||||
|
send(response, {
|
||||||
|
type: SyncEntityType.SyncAckV1,
|
||||||
|
data: {},
|
||||||
|
ackType: SyncEntityType.AlbumAssetUpdateV1,
|
||||||
|
ids: [options.nowId],
|
||||||
|
});
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
send(response, { type: createType, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncAlbumAssetExifsV1(
|
private async syncAlbumAssetExifsV1(
|
||||||
|
options: SyncQueryOptions,
|
||||||
response: Writable,
|
response: Writable,
|
||||||
checkpointMap: CheckpointMap,
|
checkpointMap: CheckpointMap,
|
||||||
auth: AuthDto,
|
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
) {
|
) {
|
||||||
const backfillType = SyncEntityType.AlbumAssetExifBackfillV1;
|
const backfillType = SyncEntityType.AlbumAssetExifBackfillV1;
|
||||||
const backfillCheckpoint = checkpointMap[backfillType];
|
const backfillCheckpoint = checkpointMap[backfillType];
|
||||||
const albums = await this.syncRepository.album.getCreatedAfter(auth.user.id, backfillCheckpoint?.updateId);
|
const albums = await this.syncRepository.album.getCreatedAfter(options, backfillCheckpoint?.updateId);
|
||||||
const upsertType = SyncEntityType.AlbumAssetExifV1;
|
const updateType = SyncEntityType.AlbumAssetExifUpdateV1;
|
||||||
const upsertCheckpoint = checkpointMap[upsertType];
|
const createType = SyncEntityType.AlbumAssetExifCreateV1;
|
||||||
if (upsertCheckpoint) {
|
const upsertCheckpoint = checkpointMap[updateType];
|
||||||
const endId = upsertCheckpoint.updateId;
|
const createCheckpoint = checkpointMap[createType];
|
||||||
|
if (createCheckpoint) {
|
||||||
|
const endId = createCheckpoint.updateId;
|
||||||
|
|
||||||
for (const album of albums) {
|
for (const album of albums) {
|
||||||
const createId = album.createId;
|
const createId = album.createId;
|
||||||
@@ -443,7 +484,7 @@ export class SyncService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startId = getStartId(createId, backfillCheckpoint);
|
const startId = getStartId(createId, backfillCheckpoint);
|
||||||
const backfill = this.syncRepository.albumAssetExif.getBackfill(album.id, startId, endId);
|
const backfill = this.syncRepository.albumAssetExif.getBackfill(options, album.id, startId, endId);
|
||||||
|
|
||||||
for await (const { updateId, ...data } of backfill) {
|
for await (const { updateId, ...data } of backfill) {
|
||||||
send(response, { type: backfillType, ids: [createId, updateId], data });
|
send(response, { type: backfillType, ids: [createId, updateId], data });
|
||||||
@@ -459,27 +500,44 @@ export class SyncService extends BaseService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const upserts = this.syncRepository.albumAssetExif.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
if (createCheckpoint) {
|
||||||
for await (const { updateId, ...data } of upserts) {
|
const updates = this.syncRepository.albumAssetExif.getUpdates(options, createCheckpoint, upsertCheckpoint);
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
for await (const { updateId, ...data } of updates) {
|
||||||
|
send(response, { type: updateType, ids: [updateId], data });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const creates = this.syncRepository.albumAssetExif.getCreates(options, createCheckpoint);
|
||||||
|
let first = true;
|
||||||
|
for await (const { updateId, ...data } of creates) {
|
||||||
|
if (first) {
|
||||||
|
send(response, {
|
||||||
|
type: SyncEntityType.SyncAckV1,
|
||||||
|
data: {},
|
||||||
|
ackType: SyncEntityType.AlbumAssetExifUpdateV1,
|
||||||
|
ids: [options.nowId],
|
||||||
|
});
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
send(response, { type: createType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncAlbumToAssetsV1(
|
private async syncAlbumToAssetsV1(
|
||||||
|
options: SyncQueryOptions,
|
||||||
response: Writable,
|
response: Writable,
|
||||||
checkpointMap: CheckpointMap,
|
checkpointMap: CheckpointMap,
|
||||||
auth: AuthDto,
|
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
) {
|
) {
|
||||||
const deleteType = SyncEntityType.AlbumToAssetDeleteV1;
|
const deleteType = SyncEntityType.AlbumToAssetDeleteV1;
|
||||||
const deletes = this.syncRepository.albumToAsset.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
const deletes = this.syncRepository.albumToAsset.getDeletes(options, checkpointMap[deleteType]);
|
||||||
for await (const { id, ...data } of deletes) {
|
for await (const { id, ...data } of deletes) {
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
}
|
}
|
||||||
|
|
||||||
const backfillType = SyncEntityType.AlbumToAssetBackfillV1;
|
const backfillType = SyncEntityType.AlbumToAssetBackfillV1;
|
||||||
const backfillCheckpoint = checkpointMap[backfillType];
|
const backfillCheckpoint = checkpointMap[backfillType];
|
||||||
const albums = await this.syncRepository.album.getCreatedAfter(auth.user.id, backfillCheckpoint?.updateId);
|
const albums = await this.syncRepository.album.getCreatedAfter(options, backfillCheckpoint?.updateId);
|
||||||
const upsertType = SyncEntityType.AlbumToAssetV1;
|
const upsertType = SyncEntityType.AlbumToAssetV1;
|
||||||
const upsertCheckpoint = checkpointMap[upsertType];
|
const upsertCheckpoint = checkpointMap[upsertType];
|
||||||
if (upsertCheckpoint) {
|
if (upsertCheckpoint) {
|
||||||
@@ -492,7 +550,7 @@ export class SyncService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startId = getStartId(createId, backfillCheckpoint);
|
const startId = getStartId(createId, backfillCheckpoint);
|
||||||
const backfill = this.syncRepository.albumToAsset.getBackfill(album.id, startId, endId);
|
const backfill = this.syncRepository.albumToAsset.getBackfill(options, album.id, startId, endId);
|
||||||
|
|
||||||
for await (const { updateId, ...data } of backfill) {
|
for await (const { updateId, ...data } of backfill) {
|
||||||
send(response, { type: backfillType, ids: [createId, updateId], data });
|
send(response, { type: backfillType, ids: [createId, updateId], data });
|
||||||
@@ -508,64 +566,69 @@ export class SyncService extends BaseService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const upserts = this.syncRepository.albumToAsset.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
const upserts = this.syncRepository.albumToAsset.getUpserts(options, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncMemoriesV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
private async syncMemoriesV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||||
const deleteType = SyncEntityType.MemoryDeleteV1;
|
const deleteType = SyncEntityType.MemoryDeleteV1;
|
||||||
const deletes = this.syncRepository.memory.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
const deletes = this.syncRepository.memory.getDeletes(options, checkpointMap[deleteType]);
|
||||||
for await (const { id, ...data } of deletes) {
|
for await (const { id, ...data } of deletes) {
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
}
|
}
|
||||||
|
|
||||||
const upsertType = SyncEntityType.MemoryV1;
|
const upsertType = SyncEntityType.MemoryV1;
|
||||||
const upserts = this.syncRepository.memory.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
const upserts = this.syncRepository.memory.getUpserts(options, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncMemoryAssetsV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
private async syncMemoryAssetsV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||||
const deleteType = SyncEntityType.MemoryToAssetDeleteV1;
|
const deleteType = SyncEntityType.MemoryToAssetDeleteV1;
|
||||||
const deletes = this.syncRepository.memoryToAsset.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
const deletes = this.syncRepository.memoryToAsset.getDeletes(options, checkpointMap[deleteType]);
|
||||||
for await (const { id, ...data } of deletes) {
|
for await (const { id, ...data } of deletes) {
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
}
|
}
|
||||||
|
|
||||||
const upsertType = SyncEntityType.MemoryToAssetV1;
|
const upsertType = SyncEntityType.MemoryToAssetV1;
|
||||||
const upserts = this.syncRepository.memoryToAsset.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
const upserts = this.syncRepository.memoryToAsset.getUpserts(options, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncStackV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
private async syncStackV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||||
const deleteType = SyncEntityType.StackDeleteV1;
|
const deleteType = SyncEntityType.StackDeleteV1;
|
||||||
const deletes = this.syncRepository.stack.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
const deletes = this.syncRepository.stack.getDeletes(options, checkpointMap[deleteType]);
|
||||||
for await (const { id, ...data } of deletes) {
|
for await (const { id, ...data } of deletes) {
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
}
|
}
|
||||||
|
|
||||||
const upsertType = SyncEntityType.StackV1;
|
const upsertType = SyncEntityType.StackV1;
|
||||||
const upserts = this.syncRepository.stack.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
const upserts = this.syncRepository.stack.getUpserts(options, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncPartnerStackV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto, sessionId: string) {
|
private async syncPartnerStackV1(
|
||||||
|
options: SyncQueryOptions,
|
||||||
|
response: Writable,
|
||||||
|
checkpointMap: CheckpointMap,
|
||||||
|
sessionId: string,
|
||||||
|
) {
|
||||||
const deleteType = SyncEntityType.PartnerStackDeleteV1;
|
const deleteType = SyncEntityType.PartnerStackDeleteV1;
|
||||||
const deletes = this.syncRepository.partnerStack.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
const deletes = this.syncRepository.partnerStack.getDeletes(options, checkpointMap[deleteType]);
|
||||||
for await (const { id, ...data } of deletes) {
|
for await (const { id, ...data } of deletes) {
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
}
|
}
|
||||||
|
|
||||||
const backfillType = SyncEntityType.PartnerStackBackfillV1;
|
const backfillType = SyncEntityType.PartnerStackBackfillV1;
|
||||||
const backfillCheckpoint = checkpointMap[backfillType];
|
const backfillCheckpoint = checkpointMap[backfillType];
|
||||||
const partners = await this.syncRepository.partner.getCreatedAfter(auth.user.id, backfillCheckpoint?.updateId);
|
const partners = await this.syncRepository.partner.getCreatedAfter(options, backfillCheckpoint?.updateId);
|
||||||
const upsertType = SyncEntityType.PartnerStackV1;
|
const upsertType = SyncEntityType.PartnerStackV1;
|
||||||
const upsertCheckpoint = checkpointMap[upsertType];
|
const upsertCheckpoint = checkpointMap[upsertType];
|
||||||
if (upsertCheckpoint) {
|
if (upsertCheckpoint) {
|
||||||
@@ -578,7 +641,7 @@ export class SyncService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startId = getStartId(createId, backfillCheckpoint);
|
const startId = getStartId(createId, backfillCheckpoint);
|
||||||
const backfill = this.syncRepository.partnerStack.getBackfill(partner.sharedById, startId, endId);
|
const backfill = this.syncRepository.partnerStack.getBackfill(options, partner.sharedById, startId, endId);
|
||||||
|
|
||||||
for await (const { updateId, ...data } of backfill) {
|
for await (const { updateId, ...data } of backfill) {
|
||||||
send(response, {
|
send(response, {
|
||||||
@@ -598,50 +661,50 @@ export class SyncService extends BaseService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const upserts = this.syncRepository.partnerStack.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
const upserts = this.syncRepository.partnerStack.getUpserts(options, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncPeopleV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
private async syncPeopleV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||||
const deleteType = SyncEntityType.PersonDeleteV1;
|
const deleteType = SyncEntityType.PersonDeleteV1;
|
||||||
const deletes = this.syncRepository.people.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
const deletes = this.syncRepository.people.getDeletes(options, checkpointMap[deleteType]);
|
||||||
for await (const { id, ...data } of deletes) {
|
for await (const { id, ...data } of deletes) {
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
}
|
}
|
||||||
|
|
||||||
const upsertType = SyncEntityType.PersonV1;
|
const upsertType = SyncEntityType.PersonV1;
|
||||||
const upserts = this.syncRepository.people.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
const upserts = this.syncRepository.people.getUpserts(options, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncAssetFacesV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
private async syncAssetFacesV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||||
const deleteType = SyncEntityType.AssetFaceDeleteV1;
|
const deleteType = SyncEntityType.AssetFaceDeleteV1;
|
||||||
const deletes = this.syncRepository.assetFace.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
const deletes = this.syncRepository.assetFace.getDeletes(options, checkpointMap[deleteType]);
|
||||||
for await (const { id, ...data } of deletes) {
|
for await (const { id, ...data } of deletes) {
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
}
|
}
|
||||||
|
|
||||||
const upsertType = SyncEntityType.AssetFaceV1;
|
const upsertType = SyncEntityType.AssetFaceV1;
|
||||||
const upserts = this.syncRepository.assetFace.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
const upserts = this.syncRepository.assetFace.getUpserts(options, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncUserMetadataV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
private async syncUserMetadataV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||||
const deleteType = SyncEntityType.UserMetadataDeleteV1;
|
const deleteType = SyncEntityType.UserMetadataDeleteV1;
|
||||||
const deletes = this.syncRepository.userMetadata.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
const deletes = this.syncRepository.userMetadata.getDeletes(options, checkpointMap[deleteType]);
|
||||||
|
|
||||||
for await (const { id, ...data } of deletes) {
|
for await (const { id, ...data } of deletes) {
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
}
|
}
|
||||||
|
|
||||||
const upsertType = SyncEntityType.UserMetadataV1;
|
const upsertType = SyncEntityType.UserMetadataV1;
|
||||||
const upserts = this.syncRepository.userMetadata.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
const upserts = this.syncRepository.userMetadata.getUpserts(options, checkpointMap[upsertType]);
|
||||||
|
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
|
|||||||
@@ -5,7 +5,15 @@ import { createHash, randomBytes } from 'node:crypto';
|
|||||||
import { Writable } from 'node:stream';
|
import { Writable } from 'node:stream';
|
||||||
import { AssetFace } from 'src/database';
|
import { AssetFace } from 'src/database';
|
||||||
import { AuthDto, LoginResponseDto } from 'src/dtos/auth.dto';
|
import { AuthDto, LoginResponseDto } from 'src/dtos/auth.dto';
|
||||||
import { AlbumUserRole, AssetType, AssetVisibility, MemoryType, SourceType, SyncRequestType } from 'src/enum';
|
import {
|
||||||
|
AlbumUserRole,
|
||||||
|
AssetType,
|
||||||
|
AssetVisibility,
|
||||||
|
MemoryType,
|
||||||
|
SourceType,
|
||||||
|
SyncEntityType,
|
||||||
|
SyncRequestType,
|
||||||
|
} from 'src/enum';
|
||||||
import { AccessRepository } from 'src/repositories/access.repository';
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||||
@@ -251,11 +259,16 @@ export class SyncTestContext extends MediumTestContext<SyncService> {
|
|||||||
|
|
||||||
async syncAckAll(auth: AuthDto, response: Array<{ type: string; ack: string }>) {
|
async syncAckAll(auth: AuthDto, response: Array<{ type: string; ack: string }>) {
|
||||||
const acks: Record<string, string> = {};
|
const acks: Record<string, string> = {};
|
||||||
|
const syncAcks: string[] = [];
|
||||||
for (const { type, ack } of response) {
|
for (const { type, ack } of response) {
|
||||||
|
if (type === SyncEntityType.SyncAckV1) {
|
||||||
|
syncAcks.push(ack);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
acks[type] = ack;
|
acks[type] = ack;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.sut.setAcks(auth, { acks: Object.values(acks) });
|
await this.sut.setAcks(auth, { acks: [...Object.values(acks), ...syncAcks] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,8 +481,6 @@ const personInsert = (person: Partial<Insertable<PersonTable>> & { ownerId: stri
|
|||||||
name: person.name || 'Test Name',
|
name: person.name || 'Test Name',
|
||||||
ownerId: person.ownerId || newUuid(),
|
ownerId: person.ownerId || newUuid(),
|
||||||
thumbnailPath: person.thumbnailPath || '/path/to/thumbnail.jpg',
|
thumbnailPath: person.thumbnailPath || '/path/to/thumbnail.jpg',
|
||||||
updatedAt: person.updatedAt || newDate(),
|
|
||||||
updateId: person.updateId || newUuid(),
|
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
...defaults,
|
...defaults,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { SyncTestContext } from 'test/medium.factory';
|
import { SyncTestContext } from 'test/medium.factory';
|
||||||
import { factory } from 'test/small.factory';
|
import { factory } from 'test/small.factory';
|
||||||
@@ -13,6 +14,18 @@ const setup = async (db?: Kysely<DB>) => {
|
|||||||
return { auth, user, session, ctx };
|
return { auth, user, session, ctx };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateSyncAck = {
|
||||||
|
ack: expect.stringContaining(SyncEntityType.AlbumAssetExifUpdateV1),
|
||||||
|
data: {},
|
||||||
|
type: SyncEntityType.SyncAckV1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const backfillSyncAck = {
|
||||||
|
ack: expect.stringContaining(SyncEntityType.AlbumAssetExifBackfillV1),
|
||||||
|
data: {},
|
||||||
|
type: SyncEntityType.SyncAckV1,
|
||||||
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
defaultDatabase = await getKyselyDB();
|
defaultDatabase = await getKyselyDB();
|
||||||
});
|
});
|
||||||
@@ -28,8 +41,8 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => {
|
|||||||
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||||
|
|
||||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||||
expect(response).toHaveLength(1);
|
|
||||||
expect(response).toEqual([
|
expect(response).toEqual([
|
||||||
|
updateSyncAck,
|
||||||
{
|
{
|
||||||
ack: expect.any(String),
|
ack: expect.any(String),
|
||||||
data: {
|
data: {
|
||||||
@@ -59,9 +72,10 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => {
|
|||||||
state: null,
|
state: null,
|
||||||
timeZone: null,
|
timeZone: null,
|
||||||
},
|
},
|
||||||
type: SyncEntityType.AlbumAssetExifV1,
|
type: SyncEntityType.AlbumAssetExifCreateV1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
expect(response).toHaveLength(2);
|
||||||
|
|
||||||
await ctx.syncAckAll(auth, response);
|
await ctx.syncAckAll(auth, response);
|
||||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]);
|
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]);
|
||||||
@@ -75,7 +89,7 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => {
|
|||||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||||
|
|
||||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toHaveLength(1);
|
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not sync album asset exif for unrelated user', async () => {
|
it('should not sync album asset exif for unrelated user', async () => {
|
||||||
@@ -97,55 +111,46 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => {
|
|||||||
it('should backfill album assets exif when a user shares an album with you', async () => {
|
it('should backfill album assets exif when a user shares an album with you', async () => {
|
||||||
const { auth, ctx } = await setup();
|
const { auth, ctx } = await setup();
|
||||||
const { user: user2 } = await ctx.newUser();
|
const { user: user2 } = await ctx.newUser();
|
||||||
const { asset: asset1Owner } = await ctx.newAsset({ ownerId: auth.user.id });
|
const { album: album1 } = await ctx.newAlbum({ ownerId: user2.id });
|
||||||
await ctx.newExif({ assetId: asset1Owner.id, make: 'asset1Owner' });
|
const { album: album2 } = await ctx.newAlbum({ ownerId: user2.id });
|
||||||
await wait(2);
|
|
||||||
const { asset: asset1User2 } = await ctx.newAsset({ ownerId: user2.id });
|
const { asset: asset1User2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||||
await ctx.newExif({ assetId: asset1User2.id, make: 'asset1User2' });
|
await ctx.newExif({ assetId: asset1User2.id, make: 'asset1User2' });
|
||||||
|
await ctx.newAlbumAsset({ albumId: album2.id, assetId: asset1User2.id });
|
||||||
await wait(2);
|
await wait(2);
|
||||||
const { asset: asset2User2 } = await ctx.newAsset({ ownerId: user2.id });
|
const { asset: asset2User2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||||
await ctx.newExif({ assetId: asset2User2.id, make: 'asset2User2' });
|
await ctx.newExif({ assetId: asset2User2.id, make: 'asset2User2' });
|
||||||
|
await ctx.newAlbumAsset({ albumId: album2.id, assetId: asset2User2.id });
|
||||||
|
await wait(2);
|
||||||
|
await ctx.newAlbumAsset({ albumId: album1.id, assetId: asset2User2.id });
|
||||||
await wait(2);
|
await wait(2);
|
||||||
const { asset: asset3User2 } = await ctx.newAsset({ ownerId: user2.id });
|
const { asset: asset3User2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||||
|
await ctx.newAlbumAsset({ albumId: album2.id, assetId: asset3User2.id });
|
||||||
await ctx.newExif({ assetId: asset3User2.id, make: 'asset3User2' });
|
await ctx.newExif({ assetId: asset3User2.id, make: 'asset3User2' });
|
||||||
const { album: album1 } = await ctx.newAlbum({ ownerId: user2.id });
|
await wait(2);
|
||||||
await ctx.newAlbumAsset({ albumId: album1.id, assetId: asset2User2.id });
|
|
||||||
await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||||
|
|
||||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||||
expect(response).toHaveLength(1);
|
|
||||||
expect(response).toEqual([
|
expect(response).toEqual([
|
||||||
|
updateSyncAck,
|
||||||
{
|
{
|
||||||
ack: expect.any(String),
|
ack: expect.any(String),
|
||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
assetId: asset2User2.id,
|
assetId: asset2User2.id,
|
||||||
}),
|
}),
|
||||||
type: SyncEntityType.AlbumAssetExifV1,
|
type: SyncEntityType.AlbumAssetExifCreateV1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
expect(response).toHaveLength(2);
|
||||||
|
|
||||||
// ack initial album asset exif sync
|
// ack initial album asset exif sync
|
||||||
await ctx.syncAckAll(auth, response);
|
await ctx.syncAckAll(auth, response);
|
||||||
|
|
||||||
// create a second album
|
// create a second album
|
||||||
const { album: album2 } = await ctx.newAlbum({ ownerId: user2.id });
|
|
||||||
await Promise.all(
|
|
||||||
[asset1User2.id, asset2User2.id, asset3User2.id, asset1Owner.id].map((assetId) =>
|
|
||||||
ctx.newAlbumAsset({ albumId: album2.id, assetId }),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||||
|
|
||||||
// should backfill the album user
|
// should backfill the album user
|
||||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||||
expect(newResponse).toEqual([
|
expect(newResponse).toEqual([
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: expect.objectContaining({
|
|
||||||
assetId: asset1Owner.id,
|
|
||||||
}),
|
|
||||||
type: SyncEntityType.AlbumAssetExifBackfillV1,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
ack: expect.any(String),
|
ack: expect.any(String),
|
||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
@@ -160,21 +165,194 @@ describe(SyncRequestType.AlbumAssetExifsV1, () => {
|
|||||||
}),
|
}),
|
||||||
type: SyncEntityType.AlbumAssetExifBackfillV1,
|
type: SyncEntityType.AlbumAssetExifBackfillV1,
|
||||||
},
|
},
|
||||||
{
|
backfillSyncAck,
|
||||||
ack: expect.stringContaining(SyncEntityType.AlbumAssetExifBackfillV1),
|
updateSyncAck,
|
||||||
data: {},
|
|
||||||
type: SyncEntityType.SyncAckV1,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
ack: expect.any(String),
|
ack: expect.any(String),
|
||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
assetId: asset3User2.id,
|
assetId: asset3User2.id,
|
||||||
}),
|
}),
|
||||||
type: SyncEntityType.AlbumAssetExifV1,
|
type: SyncEntityType.AlbumAssetExifCreateV1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
expect(newResponse).toHaveLength(5);
|
||||||
|
|
||||||
await ctx.syncAckAll(auth, newResponse);
|
await ctx.syncAckAll(auth, newResponse);
|
||||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]);
|
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should sync old asset exif when a user adds them to an album they share you', async () => {
|
||||||
|
const { auth, ctx } = await setup();
|
||||||
|
const { user: user2 } = await ctx.newUser();
|
||||||
|
const { asset: firstAsset } = await ctx.newAsset({ ownerId: user2.id, originalFileName: 'firstAsset' });
|
||||||
|
await ctx.newExif({ assetId: firstAsset.id, make: 'firstAsset' });
|
||||||
|
const { asset: secondAsset } = await ctx.newAsset({ ownerId: user2.id, originalFileName: 'secondAsset' });
|
||||||
|
await ctx.newExif({ assetId: secondAsset.id, make: 'secondAsset' });
|
||||||
|
const { asset: album1Asset } = await ctx.newAsset({ ownerId: user2.id, originalFileName: 'album1Asset' });
|
||||||
|
await ctx.newExif({ assetId: album1Asset.id, make: 'album1Asset' });
|
||||||
|
const { album: album1 } = await ctx.newAlbum({ ownerId: user2.id });
|
||||||
|
const { album: album2 } = await ctx.newAlbum({ ownerId: user2.id });
|
||||||
|
await ctx.newAlbumAsset({ albumId: album2.id, assetId: firstAsset.id });
|
||||||
|
await wait(2);
|
||||||
|
await ctx.newAlbumAsset({ albumId: album1.id, assetId: album1Asset.id });
|
||||||
|
await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||||
|
|
||||||
|
const firstAlbumResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||||
|
expect(firstAlbumResponse).toEqual([
|
||||||
|
updateSyncAck,
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
assetId: album1Asset.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetExifCreateV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(firstAlbumResponse).toHaveLength(2);
|
||||||
|
|
||||||
|
await ctx.syncAckAll(auth, firstAlbumResponse);
|
||||||
|
|
||||||
|
await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||||
|
|
||||||
|
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||||
|
expect(response).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
assetId: firstAsset.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetExifBackfillV1,
|
||||||
|
},
|
||||||
|
backfillSyncAck,
|
||||||
|
]);
|
||||||
|
expect(response).toHaveLength(2);
|
||||||
|
|
||||||
|
// ack initial album asset sync
|
||||||
|
await ctx.syncAckAll(auth, response);
|
||||||
|
|
||||||
|
await ctx.newAlbumAsset({ albumId: album2.id, assetId: secondAsset.id });
|
||||||
|
await wait(2);
|
||||||
|
|
||||||
|
// should backfill the new asset even though it's older than the first asset
|
||||||
|
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||||
|
expect(newResponse).toEqual([
|
||||||
|
updateSyncAck,
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
assetId: secondAsset.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetExifCreateV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(newResponse).toHaveLength(2);
|
||||||
|
|
||||||
|
await ctx.syncAckAll(auth, newResponse);
|
||||||
|
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sync asset exif updates for an album shared with you', async () => {
|
||||||
|
const { auth, ctx } = await setup();
|
||||||
|
const { user: user2 } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user2.id });
|
||||||
|
await ctx.newExif({ assetId: asset.id, make: 'asset' });
|
||||||
|
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||||
|
await wait(2);
|
||||||
|
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||||
|
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||||
|
|
||||||
|
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||||
|
expect(response).toHaveLength(2);
|
||||||
|
expect(response).toEqual([
|
||||||
|
updateSyncAck,
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
assetId: asset.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetExifCreateV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await ctx.syncAckAll(auth, response);
|
||||||
|
|
||||||
|
// update the asset
|
||||||
|
const assetRepository = ctx.get(AssetRepository);
|
||||||
|
await assetRepository.upsertExif({
|
||||||
|
assetId: asset.id,
|
||||||
|
city: 'New City',
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||||
|
expect(updateResponse).toHaveLength(1);
|
||||||
|
expect(updateResponse).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
assetId: asset.id,
|
||||||
|
city: 'New City',
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetExifUpdateV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sync delayed asset exif creates for an album shared with you', async () => {
|
||||||
|
const { auth, ctx } = await setup();
|
||||||
|
const { user: user2 } = await ctx.newUser();
|
||||||
|
const { asset: assetWithExif } = await ctx.newAsset({ ownerId: user2.id });
|
||||||
|
await ctx.newExif({ assetId: assetWithExif.id, make: 'assetWithExif' });
|
||||||
|
const { asset: assetDelayedExif } = await ctx.newAsset({ ownerId: user2.id });
|
||||||
|
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||||
|
const { asset: newerAsset } = await ctx.newAsset({ ownerId: user2.id });
|
||||||
|
await ctx.newExif({ assetId: newerAsset.id, make: 'newerAsset' });
|
||||||
|
await ctx.newAlbumAsset({ albumId: album.id, assetId: assetWithExif.id });
|
||||||
|
await wait(2);
|
||||||
|
await ctx.newAlbumAsset({ albumId: album.id, assetId: assetDelayedExif.id });
|
||||||
|
await wait(2);
|
||||||
|
await ctx.newAlbumAsset({ albumId: album.id, assetId: newerAsset.id });
|
||||||
|
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||||
|
|
||||||
|
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||||
|
expect(response).toEqual([
|
||||||
|
updateSyncAck,
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
assetId: assetWithExif.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetExifCreateV1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
assetId: newerAsset.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetExifCreateV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(response).toHaveLength(3);
|
||||||
|
|
||||||
|
await ctx.syncAckAll(auth, response);
|
||||||
|
|
||||||
|
// update the asset
|
||||||
|
const assetRepository = ctx.get(AssetRepository);
|
||||||
|
await assetRepository.upsertExif({
|
||||||
|
assetId: assetDelayedExif.id,
|
||||||
|
city: 'Delayed Exif',
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||||
|
expect(updateResponse).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
assetId: assetDelayedExif.id,
|
||||||
|
city: 'Delayed Exif',
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetExifUpdateV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(updateResponse).toHaveLength(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { SyncTestContext } from 'test/medium.factory';
|
import { SyncTestContext } from 'test/medium.factory';
|
||||||
import { factory } from 'test/small.factory';
|
import { factory } from 'test/small.factory';
|
||||||
@@ -13,6 +14,18 @@ const setup = async (db?: Kysely<DB>) => {
|
|||||||
return { auth, user, session, ctx };
|
return { auth, user, session, ctx };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateSyncAck = {
|
||||||
|
ack: expect.stringContaining(SyncEntityType.AlbumAssetUpdateV1),
|
||||||
|
data: {},
|
||||||
|
type: SyncEntityType.SyncAckV1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const backfillSyncAck = {
|
||||||
|
ack: expect.stringContaining(SyncEntityType.AlbumAssetBackfillV1),
|
||||||
|
data: {},
|
||||||
|
type: SyncEntityType.SyncAckV1,
|
||||||
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
defaultDatabase = await getKyselyDB();
|
defaultDatabase = await getKyselyDB();
|
||||||
});
|
});
|
||||||
@@ -45,8 +58,9 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
|||||||
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||||
|
|
||||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||||
expect(response).toHaveLength(1);
|
expect(response).toHaveLength(2);
|
||||||
expect(response).toEqual([
|
expect(response).toEqual([
|
||||||
|
updateSyncAck,
|
||||||
{
|
{
|
||||||
ack: expect.any(String),
|
ack: expect.any(String),
|
||||||
data: {
|
data: {
|
||||||
@@ -67,7 +81,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
|||||||
stackId: asset.stackId,
|
stackId: asset.stackId,
|
||||||
libraryId: asset.libraryId,
|
libraryId: asset.libraryId,
|
||||||
},
|
},
|
||||||
type: SyncEntityType.AlbumAssetV1,
|
type: SyncEntityType.AlbumAssetCreateV1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -82,7 +96,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
|||||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||||
|
|
||||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toHaveLength(1);
|
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not sync album asset for unrelated user', async () => {
|
it('should not sync album asset for unrelated user', async () => {
|
||||||
@@ -103,52 +117,42 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
|||||||
it('should backfill album assets when a user shares an album with you', async () => {
|
it('should backfill album assets when a user shares an album with you', async () => {
|
||||||
const { auth, ctx } = await setup();
|
const { auth, ctx } = await setup();
|
||||||
const { user: user2 } = await ctx.newUser();
|
const { user: user2 } = await ctx.newUser();
|
||||||
const { asset: asset1Owner } = await ctx.newAsset({ ownerId: auth.user.id });
|
const { album: album1 } = await ctx.newAlbum({ ownerId: user2.id });
|
||||||
await wait(2);
|
const { album: album2 } = await ctx.newAlbum({ ownerId: user2.id });
|
||||||
const { asset: asset1User2 } = await ctx.newAsset({ ownerId: user2.id });
|
const { asset: asset1User2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||||
|
await ctx.newAlbumAsset({ albumId: album2.id, assetId: asset1User2.id });
|
||||||
await wait(2);
|
await wait(2);
|
||||||
const { asset: asset2User2 } = await ctx.newAsset({ ownerId: user2.id });
|
const { asset: asset2User2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||||
|
await ctx.newAlbumAsset({ albumId: album2.id, assetId: asset2User2.id });
|
||||||
|
await wait(2);
|
||||||
|
await ctx.newAlbumAsset({ albumId: album1.id, assetId: asset2User2.id });
|
||||||
await wait(2);
|
await wait(2);
|
||||||
const { asset: asset3User2 } = await ctx.newAsset({ ownerId: user2.id });
|
const { asset: asset3User2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||||
|
await ctx.newAlbumAsset({ albumId: album2.id, assetId: asset3User2.id });
|
||||||
await wait(2);
|
await wait(2);
|
||||||
const { album: album1 } = await ctx.newAlbum({ ownerId: user2.id });
|
|
||||||
await ctx.newAlbumAsset({ albumId: album1.id, assetId: asset2User2.id });
|
|
||||||
await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||||
|
|
||||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||||
expect(response).toHaveLength(1);
|
expect(response).toHaveLength(2);
|
||||||
expect(response).toEqual([
|
expect(response).toEqual([
|
||||||
|
updateSyncAck,
|
||||||
{
|
{
|
||||||
ack: expect.any(String),
|
ack: expect.any(String),
|
||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
id: asset2User2.id,
|
id: asset2User2.id,
|
||||||
}),
|
}),
|
||||||
type: SyncEntityType.AlbumAssetV1,
|
type: SyncEntityType.AlbumAssetCreateV1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ack initial album asset sync
|
// ack initial album asset sync
|
||||||
await ctx.syncAckAll(auth, response);
|
await ctx.syncAckAll(auth, response);
|
||||||
|
|
||||||
// create a second album
|
|
||||||
const { album: album2 } = await ctx.newAlbum({ ownerId: user2.id });
|
|
||||||
await Promise.all(
|
|
||||||
[asset1User2.id, asset2User2.id, asset3User2.id, asset1Owner.id].map((assetId) =>
|
|
||||||
ctx.newAlbumAsset({ albumId: album2.id, assetId }),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||||
|
|
||||||
// should backfill the album user
|
// should backfill the album user
|
||||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||||
expect(newResponse).toEqual([
|
expect(newResponse).toEqual([
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: expect.objectContaining({
|
|
||||||
id: asset1Owner.id,
|
|
||||||
}),
|
|
||||||
type: SyncEntityType.AlbumAssetBackfillV1,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
ack: expect.any(String),
|
ack: expect.any(String),
|
||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
@@ -163,21 +167,129 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
|||||||
}),
|
}),
|
||||||
type: SyncEntityType.AlbumAssetBackfillV1,
|
type: SyncEntityType.AlbumAssetBackfillV1,
|
||||||
},
|
},
|
||||||
{
|
backfillSyncAck,
|
||||||
ack: expect.stringContaining(SyncEntityType.AlbumAssetBackfillV1),
|
updateSyncAck,
|
||||||
data: {},
|
|
||||||
type: SyncEntityType.SyncAckV1,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
ack: expect.any(String),
|
ack: expect.any(String),
|
||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
id: asset3User2.id,
|
id: asset3User2.id,
|
||||||
}),
|
}),
|
||||||
type: SyncEntityType.AlbumAssetV1,
|
type: SyncEntityType.AlbumAssetCreateV1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await ctx.syncAckAll(auth, newResponse);
|
await ctx.syncAckAll(auth, newResponse);
|
||||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([]);
|
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should sync old assets when a user adds them to an album they share you', async () => {
|
||||||
|
const { auth, ctx } = await setup();
|
||||||
|
const { user: user2 } = await ctx.newUser();
|
||||||
|
const { asset: firstAsset } = await ctx.newAsset({ ownerId: user2.id, originalFileName: 'firstAsset' });
|
||||||
|
const { asset: secondAsset } = await ctx.newAsset({ ownerId: user2.id, originalFileName: 'secondAsset' });
|
||||||
|
const { asset: album1Asset } = await ctx.newAsset({ ownerId: user2.id, originalFileName: 'album1Asset' });
|
||||||
|
const { album: album1 } = await ctx.newAlbum({ ownerId: user2.id });
|
||||||
|
const { album: album2 } = await ctx.newAlbum({ ownerId: user2.id });
|
||||||
|
await ctx.newAlbumAsset({ albumId: album2.id, assetId: firstAsset.id });
|
||||||
|
await wait(2);
|
||||||
|
await ctx.newAlbumAsset({ albumId: album1.id, assetId: album1Asset.id });
|
||||||
|
await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||||
|
|
||||||
|
const firstAlbumResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||||
|
expect(firstAlbumResponse).toHaveLength(2);
|
||||||
|
expect(firstAlbumResponse).toEqual([
|
||||||
|
updateSyncAck,
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
id: album1Asset.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetCreateV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await ctx.syncAckAll(auth, firstAlbumResponse);
|
||||||
|
|
||||||
|
await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||||
|
|
||||||
|
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||||
|
// expect(response).toHaveLength(2);
|
||||||
|
expect(response).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
id: firstAsset.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetBackfillV1,
|
||||||
|
},
|
||||||
|
backfillSyncAck,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ack initial album asset sync
|
||||||
|
await ctx.syncAckAll(auth, response);
|
||||||
|
|
||||||
|
await ctx.newAlbumAsset({ albumId: album2.id, assetId: secondAsset.id });
|
||||||
|
await wait(2);
|
||||||
|
|
||||||
|
// should backfill the new asset even though it's older than the first asset
|
||||||
|
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||||
|
expect(newResponse).toEqual([
|
||||||
|
updateSyncAck,
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
id: secondAsset.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetCreateV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await ctx.syncAckAll(auth, newResponse);
|
||||||
|
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sync asset updates for an album shared with you', async () => {
|
||||||
|
const { auth, ctx } = await setup();
|
||||||
|
const { user: user2 } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user2.id, isFavorite: false });
|
||||||
|
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||||
|
await wait(2);
|
||||||
|
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||||
|
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||||
|
|
||||||
|
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||||
|
expect(response).toHaveLength(2);
|
||||||
|
expect(response).toEqual([
|
||||||
|
updateSyncAck,
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
id: asset.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetCreateV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await ctx.syncAckAll(auth, response);
|
||||||
|
|
||||||
|
// update the asset
|
||||||
|
const assetRepository = ctx.get(AssetRepository);
|
||||||
|
await assetRepository.update({
|
||||||
|
id: asset.id,
|
||||||
|
isFavorite: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||||
|
expect(updateResponse).toHaveLength(1);
|
||||||
|
expect(updateResponse).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
id: asset.id,
|
||||||
|
isFavorite: true,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetUpdateV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { SessionRepository } from 'src/repositories/session.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { SyncTestContext } from 'test/medium.factory';
|
import { SyncTestContext } from 'test/medium.factory';
|
||||||
import { getKyselyDB } from 'test/utils';
|
import { getKyselyDB } from 'test/utils';
|
||||||
@@ -27,10 +28,12 @@ describe(SyncEntityType.SyncResetV1, () => {
|
|||||||
it('should detect a pending sync reset', async () => {
|
it('should detect a pending sync reset', async () => {
|
||||||
const { auth, ctx } = await setup();
|
const { auth, ctx } = await setup();
|
||||||
|
|
||||||
auth.session!.isPendingSyncReset = true;
|
await ctx.get(SessionRepository).update(auth.session!.id, {
|
||||||
|
isPendingSyncReset: true,
|
||||||
|
});
|
||||||
|
|
||||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
||||||
expect(response).toEqual([{ type: SyncEntityType.SyncResetV1, data: {} }]);
|
expect(response).toEqual([{ type: SyncEntityType.SyncResetV1, data: {}, ack: 'SyncResetV1|reset' }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not send other dtos when a reset is pending', async () => {
|
it('should not send other dtos when a reset is pending', async () => {
|
||||||
@@ -40,10 +43,12 @@ describe(SyncEntityType.SyncResetV1, () => {
|
|||||||
|
|
||||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||||
|
|
||||||
auth.session!.isPendingSyncReset = true;
|
await ctx.get(SessionRepository).update(auth.session!.id, {
|
||||||
|
isPendingSyncReset: true,
|
||||||
|
});
|
||||||
|
|
||||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([
|
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([
|
||||||
{ type: SyncEntityType.SyncResetV1, data: {} },
|
{ type: SyncEntityType.SyncResetV1, data: {}, ack: 'SyncResetV1|reset' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,7 +57,9 @@ describe(SyncEntityType.SyncResetV1, () => {
|
|||||||
|
|
||||||
await ctx.newAsset({ ownerId: user.id });
|
await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
auth.session!.isPendingSyncReset = true;
|
await ctx.get(SessionRepository).update(auth.session!.id, {
|
||||||
|
isPendingSyncReset: true,
|
||||||
|
});
|
||||||
|
|
||||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1], true)).resolves.toEqual([
|
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1], true)).resolves.toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -60,4 +67,28 @@ describe(SyncEntityType.SyncResetV1, () => {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should reset the sync progress', async () => {
|
||||||
|
const { auth, user, ctx } = await setup();
|
||||||
|
|
||||||
|
await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
||||||
|
await ctx.syncAckAll(auth, response);
|
||||||
|
|
||||||
|
await ctx.get(SessionRepository).update(auth.session!.id, {
|
||||||
|
isPendingSyncReset: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetResponse = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
||||||
|
|
||||||
|
await ctx.syncAckAll(auth, resetResponse);
|
||||||
|
|
||||||
|
const postResetResponse = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
||||||
|
expect(postResetResponse).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
type: SyncEntityType.AssetV1,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ const authFactory = ({
|
|||||||
if (session) {
|
if (session) {
|
||||||
auth.session = {
|
auth.session = {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
isPendingSyncReset: false,
|
|
||||||
hasElevatedPermission: false,
|
hasElevatedPermission: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-2
@@ -446,6 +446,16 @@ export async function* makeStream<T>(items: T[] = []): AsyncIterableIterator<T>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const wait = (ms: number) => {
|
export const wait = (ms: number): Promise<void> => {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => {
|
||||||
|
const target = performance.now() + ms;
|
||||||
|
const checkDone = () => {
|
||||||
|
if (performance.now() >= target) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
setTimeout(checkDone, 1); // Check again after 1ms
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setTimeout(checkDone, ms);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user