Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e62071fda0 | |||
| a1bc862a32 | |||
| 75bf3aa1be | |||
| 38e68d16f9 | |||
| caf11fbb96 | |||
| f99c6feac5 | |||
| 5122512f19 | |||
| 49ed212af8 | |||
| e29103b69f | |||
| 14b771d7c7 | |||
| 07aa51638c | |||
| 0a9a520ed2 | |||
| de81006367 | |||
| e0144b4ece |
@@ -55,7 +55,7 @@
|
|||||||
"userEnvProbe": "loginInteractiveShell",
|
"userEnvProbe": "loginInteractiveShell",
|
||||||
"remoteEnv": {
|
"remoteEnv": {
|
||||||
// The location where your uploaded files are stored
|
// The location where your uploaded files are stored
|
||||||
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:upload-devcontainer-volume}",
|
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:./library}",
|
||||||
// Connection secret for postgres. You should change it to a random password
|
// Connection secret for postgres. You should change it to a random password
|
||||||
// Please use only the characters `A-Za-z0-9`, without special characters or spaces
|
// Please use only the characters `A-Za-z0-9`, without special characters or spaces
|
||||||
"DB_PASSWORD": "${localEnv:DB_PASSWORD:postgres}",
|
"DB_PASSWORD": "${localEnv:DB_PASSWORD:postgres}",
|
||||||
|
|||||||
@@ -51,14 +51,19 @@ fix_permissions() {
|
|||||||
|
|
||||||
run_cmd sudo find "${IMMICH_WORKSPACE}/server/upload" -not -path "${IMMICH_WORKSPACE}/server/upload/postgres/*" -not -path "${IMMICH_WORKSPACE}/server/upload/postgres" -exec chown node {} +
|
run_cmd sudo find "${IMMICH_WORKSPACE}/server/upload" -not -path "${IMMICH_WORKSPACE}/server/upload/postgres/*" -not -path "${IMMICH_WORKSPACE}/server/upload/postgres" -exec chown node {} +
|
||||||
|
|
||||||
run_cmd sudo chown node -R "${IMMICH_WORKSPACE}/.vscode" \
|
# Change ownership for directories that exist
|
||||||
|
for dir in "${IMMICH_WORKSPACE}/.vscode" \
|
||||||
"${IMMICH_WORKSPACE}/cli/node_modules" \
|
"${IMMICH_WORKSPACE}/cli/node_modules" \
|
||||||
"${IMMICH_WORKSPACE}/e2e/node_modules" \
|
"${IMMICH_WORKSPACE}/e2e/node_modules" \
|
||||||
"${IMMICH_WORKSPACE}/open-api/typescript-sdk/node_modules" \
|
"${IMMICH_WORKSPACE}/open-api/typescript-sdk/node_modules" \
|
||||||
"${IMMICH_WORKSPACE}/server/node_modules" \
|
"${IMMICH_WORKSPACE}/server/node_modules" \
|
||||||
"${IMMICH_WORKSPACE}/server/dist" \
|
"${IMMICH_WORKSPACE}/server/dist" \
|
||||||
"${IMMICH_WORKSPACE}/web/node_modules" \
|
"${IMMICH_WORKSPACE}/web/node_modules" \
|
||||||
"${IMMICH_WORKSPACE}/web/dist"
|
"${IMMICH_WORKSPACE}/web/dist"; do
|
||||||
|
if [ -d "$dir" ]; then
|
||||||
|
run_cmd sudo chown node -R "$dir"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
log ""
|
log ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ services:
|
|||||||
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
|
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
|
||||||
- server_node_modules:/workspaces/immich/server/node_modules
|
- server_node_modules:/workspaces/immich/server/node_modules
|
||||||
- web_node_modules:/workspaces/immich/web/node_modules
|
- web_node_modules:/workspaces/immich/web/node_modules
|
||||||
- ${UPLOAD_LOCATION-./Library}/photos:/workspaces/immich/server/upload
|
- ${UPLOAD_LOCATION:-upload1-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/workspaces/immich/server/upload
|
||||||
- ${UPLOAD_LOCATION-./Library}/photos/upload:/workspaces/immich/server/upload/upload
|
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/workspaces/immich/server/upload/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
|
||||||
immich-web:
|
immich-web:
|
||||||
@@ -29,8 +29,9 @@ services:
|
|||||||
POSTGRES_USER: ${DB_USERNAME-postgres}
|
POSTGRES_USER: ${DB_USERNAME-postgres}
|
||||||
POSTGRES_DB: ${DB_DATABASE_NAME-immich}
|
POSTGRES_DB: ${DB_DATABASE_NAME-immich}
|
||||||
POSTGRES_INITDB_ARGS: '--data-checksums'
|
POSTGRES_INITDB_ARGS: '--data-checksums'
|
||||||
volumes:
|
POSTGRES_HOST_AUTH_METHOD: md5
|
||||||
- ${UPLOAD_LOCATION-./Library}/postgres:/var/lib/postgresql/data
|
volumes:
|
||||||
|
- ${UPLOAD_LOCATION:-postgres-devcontainer-volume}${UPLOAD_LOCATION:+/postgres}:/var/lib/postgresql/data
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
@@ -42,4 +43,6 @@ volumes:
|
|||||||
open_api_node_modules:
|
open_api_node_modules:
|
||||||
server_node_modules:
|
server_node_modules:
|
||||||
web_node_modules:
|
web_node_modules:
|
||||||
upload-devcontainer-volume:
|
upload1-devcontainer-volume:
|
||||||
|
upload2-devcontainer-volume:
|
||||||
|
postgres-devcontainer-volume:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ name: immich-dev
|
|||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
command: [ '/usr/src/app/bin/immich-dev' ]
|
command: ['/usr/src/app/bin/immich-dev']
|
||||||
image: immich-server-dev:latest
|
image: immich-server-dev:latest
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
@@ -70,7 +70,7 @@ services:
|
|||||||
# user: 0:0
|
# user: 0:0
|
||||||
build:
|
build:
|
||||||
context: ../web
|
context: ../web
|
||||||
command: [ '/usr/src/app/bin/immich-web' ]
|
command: ['/usr/src/app/bin/immich-web']
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
@@ -122,7 +122,7 @@ services:
|
|||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1-pgvectors0.2.0
|
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.2-pgvectors0.2.0
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ services:
|
|||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1-pgvectors0.2.0
|
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.2-pgvectors0.2.0
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@@ -91,7 +91,7 @@ services:
|
|||||||
# add data source for http://immich-prometheus:9090 to get started
|
# add data source for http://immich-prometheus:9090 to get started
|
||||||
immich-grafana:
|
immich-grafana:
|
||||||
container_name: immich_grafana
|
container_name: immich_grafana
|
||||||
command: [ './run.sh', '-disable-reporting' ]
|
command: ['./run.sh', '-disable-reporting']
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
image: grafana/grafana:12.0.1-ubuntu@sha256:65575bb9c761335e2ff30e364f21d38632e3b2e75f5f81d83cc92f44b9bbc055
|
image: grafana/grafana:12.0.1-ubuntu@sha256:65575bb9c761335e2ff30e364f21d38632e3b2e75f5f81d83cc92f44b9bbc055
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ services:
|
|||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1-pgvectors0.2.0
|
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.2-pgvectors0.2.0
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
POSTGRES_USER: ${DB_USERNAME}
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
|
|||||||
@@ -64,7 +64,13 @@ COMMIT;
|
|||||||
|
|
||||||
### Updating VectorChord
|
### Updating VectorChord
|
||||||
|
|
||||||
When installing a new version of VectorChord, you will need to manually update the extension by connecting to the Immich database and running `ALTER EXTENSION vchord UPDATE;`.
|
When installing a new version of VectorChord, you will need to manually update the extension and reindex by connecting to the Immich database and running:
|
||||||
|
|
||||||
|
```
|
||||||
|
ALTER EXTENSION vchord UPDATE;
|
||||||
|
REINDEX INDEX face_index;
|
||||||
|
REINDEX INDEX clip_index;
|
||||||
|
```
|
||||||
|
|
||||||
## Migrating to VectorChord
|
## Migrating to VectorChord
|
||||||
|
|
||||||
@@ -76,6 +82,8 @@ Support for pgvecto.rs will be dropped in a later release, hence we recommend al
|
|||||||
|
|
||||||
The easiest option is to have both extensions installed during the migration:
|
The easiest option is to have both extensions installed during the migration:
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Migration steps (automatic)</summary>
|
||||||
1. Ensure you still have pgvecto.rs installed
|
1. Ensure you still have pgvecto.rs installed
|
||||||
2. Install `pgvector` (`>= 0.7.0, < 1.0.0`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`)
|
2. Install `pgvector` (`>= 0.7.0, < 1.0.0`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`)
|
||||||
3. [Install VectorChord][vchord-install]
|
3. [Install VectorChord][vchord-install]
|
||||||
@@ -89,8 +97,12 @@ The easiest option is to have both extensions installed during the migration:
|
|||||||
11. Restart the Postgres database
|
11. Restart the Postgres database
|
||||||
12. Uninstall pgvecto.rs (e.g. `apt-get purge vectors-pg14` on Debian-based environments, replacing `pg14` as appropriate). `pgvector` must remain installed as it provides the data types used by `vchord`
|
12. Uninstall pgvecto.rs (e.g. `apt-get purge vectors-pg14` on Debian-based environments, replacing `pg14` as appropriate). `pgvector` must remain installed as it provides the data types used by `vchord`
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
If it is not possible to have both VectorChord and pgvecto.rs installed at the same time, you can perform the migration with more manual steps:
|
If it is not possible to have both VectorChord and pgvecto.rs installed at the same time, you can perform the migration with more manual steps:
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Migration steps (manual)</summary>
|
||||||
1. While pgvecto.rs is still installed, run the following SQL command using psql or your choice of database client. Take note of the number outputted by this command as you will need it later
|
1. While pgvecto.rs is still installed, run the following SQL command using psql or your choice of database client. Take note of the number outputted by this command as you will need it later
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
@@ -123,14 +135,20 @@ ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE vector(512);
|
|||||||
|
|
||||||
5. Start Immich and let it create new indices using VectorChord
|
5. Start Immich and let it create new indices using VectorChord
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
### Migrating from pgvector
|
### Migrating from pgvector
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Migration steps</summary>
|
||||||
1. Ensure you have at least 0.7.0 of pgvector installed. If it is below that, please upgrade it and run the SQL command `ALTER EXTENSION vector UPDATE;` using psql or your choice of database client
|
1. Ensure you have at least 0.7.0 of pgvector installed. If it is below that, please upgrade it and run the SQL command `ALTER EXTENSION vector UPDATE;` using psql or your choice of database client
|
||||||
2. Follow the Prerequisites to install VectorChord
|
2. Follow the Prerequisites to install VectorChord
|
||||||
3. If Immich does not have superuser permissions, run the SQL command `CREATE EXTENSION vchord CASCADE;`
|
3. If Immich does not have superuser permissions, run the SQL command `CREATE EXTENSION vchord CASCADE;`
|
||||||
4. Remove the `DB_VECTOR_EXTENSION=pgvector` environmental variable as it will make Immich still use pgvector if set
|
4. Remove the `DB_VECTOR_EXTENSION=pgvector` environmental variable as it will make Immich still use pgvector if set
|
||||||
5. Start Immich and let it create new indices using VectorChord
|
5. Start Immich and let it create new indices using VectorChord
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
Note that VectorChord itself uses pgvector types, so you should not uninstall pgvector after following these steps.
|
Note that VectorChord itself uses pgvector types, so you should not uninstall pgvector after following these steps.
|
||||||
|
|
||||||
[vchord-install]: https://docs.vectorchord.ai/vectorchord/getting-started/installation.html
|
[vchord-install]: https://docs.vectorchord.ai/vectorchord/getting-started/installation.html
|
||||||
|
|||||||
@@ -346,6 +346,7 @@
|
|||||||
};
|
};
|
||||||
F0B57D372DF764BD00DC5BCC = {
|
F0B57D372DF764BD00DC5BCC = {
|
||||||
CreatedOnToolsVersion = 16.4;
|
CreatedOnToolsVersion = 16.4;
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
FAC6F88F2D287C890078CB2F = {
|
FAC6F88F2D287C890078CB2F = {
|
||||||
CreatedOnToolsVersion = 16.0;
|
CreatedOnToolsVersion = 16.0;
|
||||||
@@ -648,7 +649,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 208;
|
CURRENT_PROJECT_VERSION = 209;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -792,7 +793,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 208;
|
CURRENT_PROJECT_VERSION = 209;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -822,7 +823,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 208;
|
CURRENT_PROJECT_VERSION = 209;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -856,7 +857,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 209;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -899,7 +900,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 209;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -939,7 +940,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 209;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -978,7 +979,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 208;
|
CURRENT_PROJECT_VERSION = 209;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -1022,7 +1023,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 208;
|
CURRENT_PROJECT_VERSION = 209;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -1063,7 +1064,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 208;
|
CURRENT_PROJECT_VERSION = 209;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.134.0</string>
|
<string>1.135.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>208</string>
|
<string>209</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true />
|
<true />
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|||||||
+3
@@ -38,6 +38,7 @@ class SyncEntityType {
|
|||||||
static const albumV1 = SyncEntityType._(r'AlbumV1');
|
static const albumV1 = SyncEntityType._(r'AlbumV1');
|
||||||
static const albumDeleteV1 = SyncEntityType._(r'AlbumDeleteV1');
|
static const albumDeleteV1 = SyncEntityType._(r'AlbumDeleteV1');
|
||||||
static const albumUserV1 = SyncEntityType._(r'AlbumUserV1');
|
static const albumUserV1 = SyncEntityType._(r'AlbumUserV1');
|
||||||
|
static const albumUserBackfillV1 = SyncEntityType._(r'AlbumUserBackfillV1');
|
||||||
static const albumUserDeleteV1 = SyncEntityType._(r'AlbumUserDeleteV1');
|
static const albumUserDeleteV1 = SyncEntityType._(r'AlbumUserDeleteV1');
|
||||||
static const syncAckV1 = SyncEntityType._(r'SyncAckV1');
|
static const syncAckV1 = SyncEntityType._(r'SyncAckV1');
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ class SyncEntityType {
|
|||||||
albumV1,
|
albumV1,
|
||||||
albumDeleteV1,
|
albumDeleteV1,
|
||||||
albumUserV1,
|
albumUserV1,
|
||||||
|
albumUserBackfillV1,
|
||||||
albumUserDeleteV1,
|
albumUserDeleteV1,
|
||||||
syncAckV1,
|
syncAckV1,
|
||||||
];
|
];
|
||||||
@@ -113,6 +115,7 @@ class SyncEntityTypeTypeTransformer {
|
|||||||
case r'AlbumV1': return SyncEntityType.albumV1;
|
case r'AlbumV1': return SyncEntityType.albumV1;
|
||||||
case r'AlbumDeleteV1': return SyncEntityType.albumDeleteV1;
|
case r'AlbumDeleteV1': return SyncEntityType.albumDeleteV1;
|
||||||
case r'AlbumUserV1': return SyncEntityType.albumUserV1;
|
case r'AlbumUserV1': return SyncEntityType.albumUserV1;
|
||||||
|
case r'AlbumUserBackfillV1': return SyncEntityType.albumUserBackfillV1;
|
||||||
case r'AlbumUserDeleteV1': return SyncEntityType.albumUserDeleteV1;
|
case r'AlbumUserDeleteV1': return SyncEntityType.albumUserDeleteV1;
|
||||||
case r'SyncAckV1': return SyncEntityType.syncAckV1;
|
case r'SyncAckV1': return SyncEntityType.syncAckV1;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -13706,6 +13706,7 @@
|
|||||||
"AlbumV1",
|
"AlbumV1",
|
||||||
"AlbumDeleteV1",
|
"AlbumDeleteV1",
|
||||||
"AlbumUserV1",
|
"AlbumUserV1",
|
||||||
|
"AlbumUserBackfillV1",
|
||||||
"AlbumUserDeleteV1",
|
"AlbumUserDeleteV1",
|
||||||
"SyncAckV1"
|
"SyncAckV1"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4059,6 +4059,7 @@ export enum SyncEntityType {
|
|||||||
AlbumV1 = "AlbumV1",
|
AlbumV1 = "AlbumV1",
|
||||||
AlbumDeleteV1 = "AlbumDeleteV1",
|
AlbumDeleteV1 = "AlbumDeleteV1",
|
||||||
AlbumUserV1 = "AlbumUserV1",
|
AlbumUserV1 = "AlbumUserV1",
|
||||||
|
AlbumUserBackfillV1 = "AlbumUserBackfillV1",
|
||||||
AlbumUserDeleteV1 = "AlbumUserDeleteV1",
|
AlbumUserDeleteV1 = "AlbumUserDeleteV1",
|
||||||
SyncAckV1 = "SyncAckV1"
|
SyncAckV1 = "SyncAckV1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -355,6 +355,12 @@ export const columns = {
|
|||||||
'updateId',
|
'updateId',
|
||||||
'duration',
|
'duration',
|
||||||
],
|
],
|
||||||
|
syncAlbumUser: [
|
||||||
|
'albums_shared_users_users.albumsId as albumId',
|
||||||
|
'albums_shared_users_users.usersId as userId',
|
||||||
|
'albums_shared_users_users.role',
|
||||||
|
'albums_shared_users_users.updateId',
|
||||||
|
],
|
||||||
stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'],
|
stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'],
|
||||||
syncAssetExif: [
|
syncAssetExif: [
|
||||||
'exif.assetId',
|
'exif.assetId',
|
||||||
|
|||||||
Vendored
+3
-1
@@ -98,8 +98,10 @@ export interface AlbumsSharedUsersUsers {
|
|||||||
albumsId: string;
|
albumsId: string;
|
||||||
role: Generated<AlbumUserRole>;
|
role: Generated<AlbumUserRole>;
|
||||||
usersId: string;
|
usersId: string;
|
||||||
updatedAt: Generated<Timestamp>;
|
createId: Generated<string>;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
updateId: Generated<string>;
|
updateId: Generated<string>;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiKeys {
|
export interface ApiKeys {
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ export type SyncItem = {
|
|||||||
[SyncEntityType.AlbumV1]: SyncAlbumV1;
|
[SyncEntityType.AlbumV1]: SyncAlbumV1;
|
||||||
[SyncEntityType.AlbumDeleteV1]: SyncAlbumDeleteV1;
|
[SyncEntityType.AlbumDeleteV1]: SyncAlbumDeleteV1;
|
||||||
[SyncEntityType.AlbumUserV1]: SyncAlbumUserV1;
|
[SyncEntityType.AlbumUserV1]: SyncAlbumUserV1;
|
||||||
|
[SyncEntityType.AlbumUserBackfillV1]: SyncAlbumUserV1;
|
||||||
[SyncEntityType.AlbumUserDeleteV1]: SyncAlbumUserDeleteV1;
|
[SyncEntityType.AlbumUserDeleteV1]: SyncAlbumUserDeleteV1;
|
||||||
[SyncEntityType.SyncAckV1]: object;
|
[SyncEntityType.SyncAckV1]: object;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -603,6 +603,7 @@ export enum SyncEntityType {
|
|||||||
AlbumV1 = 'AlbumV1',
|
AlbumV1 = 'AlbumV1',
|
||||||
AlbumDeleteV1 = 'AlbumDeleteV1',
|
AlbumDeleteV1 = 'AlbumDeleteV1',
|
||||||
AlbumUserV1 = 'AlbumUserV1',
|
AlbumUserV1 = 'AlbumUserV1',
|
||||||
|
AlbumUserBackfillV1 = 'AlbumUserBackfillV1',
|
||||||
AlbumUserDeleteV1 = 'AlbumUserDeleteV1',
|
AlbumUserDeleteV1 = 'AlbumUserDeleteV1',
|
||||||
|
|
||||||
SyncAckV1 = 'SyncAckV1',
|
SyncAckV1 = 'SyncAckV1',
|
||||||
|
|||||||
@@ -394,6 +394,35 @@ where
|
|||||||
order by
|
order by
|
||||||
"id" asc
|
"id" asc
|
||||||
|
|
||||||
|
-- SyncRepository.getAlbumBackfill
|
||||||
|
select
|
||||||
|
"albumsId" as "id",
|
||||||
|
"createId"
|
||||||
|
from
|
||||||
|
"albums_shared_users_users"
|
||||||
|
where
|
||||||
|
"usersId" = $1
|
||||||
|
and "createId" >= $2
|
||||||
|
and "createdAt" < now() - interval '1 millisecond'
|
||||||
|
order by
|
||||||
|
"createId" asc
|
||||||
|
|
||||||
|
-- SyncRepository.getAlbumUsersBackfill
|
||||||
|
select
|
||||||
|
"albums_shared_users_users"."albumsId" as "albumId",
|
||||||
|
"albums_shared_users_users"."usersId" as "userId",
|
||||||
|
"albums_shared_users_users"."role",
|
||||||
|
"albums_shared_users_users"."updateId"
|
||||||
|
from
|
||||||
|
"albums_shared_users_users"
|
||||||
|
where
|
||||||
|
"albumsId" = $1
|
||||||
|
and "updatedAt" < now() - interval '1 millisecond'
|
||||||
|
and "updateId" < $2
|
||||||
|
and "updateId" >= $3
|
||||||
|
order by
|
||||||
|
"updateId" asc
|
||||||
|
|
||||||
-- SyncRepository.getAlbumUserUpserts
|
-- SyncRepository.getAlbumUserUpserts
|
||||||
select
|
select
|
||||||
"albums_shared_users_users"."albumsId" as "albumId",
|
"albums_shared_users_users"."albumsId" as "albumId",
|
||||||
|
|||||||
@@ -119,8 +119,6 @@ export class DatabaseRepository {
|
|||||||
await sql`CREATE EXTENSION IF NOT EXISTS ${sql.raw(extension)} CASCADE`.execute(this.db);
|
await sql`CREATE EXTENSION IF NOT EXISTS ${sql.raw(extension)} CASCADE`.execute(this.db);
|
||||||
if (extension === DatabaseExtension.VECTORCHORD) {
|
if (extension === DatabaseExtension.VECTORCHORD) {
|
||||||
const dbName = sql.id(await this.getDatabaseName());
|
const dbName = sql.id(await this.getDatabaseName());
|
||||||
await sql`ALTER DATABASE ${dbName} SET vchordrq.prewarm_dim = '512,640,768,1024,1152,1536'`.execute(this.db);
|
|
||||||
await sql`SET vchordrq.prewarm_dim = '512,640,768,1024,1152,1536'`.execute(this.db);
|
|
||||||
await sql`ALTER DATABASE ${dbName} SET vchordrq.probes = 1`.execute(this.db);
|
await sql`ALTER DATABASE ${dbName} SET vchordrq.probes = 1`.execute(this.db);
|
||||||
await sql`SET vchordrq.probes = 1`.execute(this.db);
|
await sql`SET vchordrq.probes = 1`.execute(this.db);
|
||||||
}
|
}
|
||||||
@@ -142,21 +140,29 @@ export class DatabaseRepository {
|
|||||||
}
|
}
|
||||||
targetVersion ??= availableVersion;
|
targetVersion ??= availableVersion;
|
||||||
|
|
||||||
const isVectors = extension === DatabaseExtension.VECTORS;
|
|
||||||
let restartRequired = false;
|
let restartRequired = false;
|
||||||
const diff = semver.diff(installedVersion, targetVersion);
|
const diff = semver.diff(installedVersion, targetVersion);
|
||||||
|
if (!diff) {
|
||||||
|
return { restartRequired: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.db.schema.dropIndex(VectorIndex.CLIP).ifExists().execute(),
|
||||||
|
this.db.schema.dropIndex(VectorIndex.FACE).ifExists().execute(),
|
||||||
|
]);
|
||||||
|
|
||||||
await this.db.transaction().execute(async (tx) => {
|
await this.db.transaction().execute(async (tx) => {
|
||||||
await this.setSearchPath(tx);
|
await this.setSearchPath(tx);
|
||||||
|
|
||||||
await sql`ALTER EXTENSION ${sql.raw(extension)} UPDATE TO ${sql.lit(targetVersion)}`.execute(tx);
|
await sql`ALTER EXTENSION ${sql.raw(extension)} UPDATE TO ${sql.lit(targetVersion)}`.execute(tx);
|
||||||
|
|
||||||
if (isVectors && (diff === 'major' || diff === 'minor')) {
|
if (extension === DatabaseExtension.VECTORS && (diff === 'major' || diff === 'minor')) {
|
||||||
await sql`SELECT pgvectors_upgrade()`.execute(tx);
|
await sql`SELECT pgvectors_upgrade()`.execute(tx);
|
||||||
restartRequired = true;
|
restartRequired = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (diff && !restartRequired) {
|
if (!restartRequired) {
|
||||||
await Promise.all([this.reindexVectors(VectorIndex.CLIP), this.reindexVectors(VectorIndex.FACE)]);
|
await Promise.all([this.reindexVectors(VectorIndex.CLIP), this.reindexVectors(VectorIndex.FACE)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -254,16 +254,36 @@ export class SyncRepository {
|
|||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||||
|
getAlbumBackfill(userId: string, afterCreateId?: string) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('albums_shared_users_users')
|
||||||
|
.select(['albumsId as id', 'createId'])
|
||||||
|
.where('usersId', '=', userId)
|
||||||
|
.$if(!!afterCreateId, (qb) => qb.where('createId', '>=', afterCreateId!))
|
||||||
|
.where('createdAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
|
.orderBy('createId', 'asc')
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
||||||
|
getAlbumUsersBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('albums_shared_users_users')
|
||||||
|
.select(columns.syncAlbumUser)
|
||||||
|
.where('albumsId', '=', albumId)
|
||||||
|
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
|
.where('updateId', '<', beforeUpdateId)
|
||||||
|
.$if(!!afterUpdateId, (eb) => eb.where('updateId', '>=', afterUpdateId!))
|
||||||
|
.orderBy('updateId', 'asc')
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||||
getAlbumUserUpserts(userId: string, ack?: SyncAck) {
|
getAlbumUserUpserts(userId: string, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('albums_shared_users_users')
|
.selectFrom('albums_shared_users_users')
|
||||||
.select([
|
.select(columns.syncAlbumUser)
|
||||||
'albums_shared_users_users.albumsId as albumId',
|
|
||||||
'albums_shared_users_users.usersId as userId',
|
|
||||||
'albums_shared_users_users.role',
|
|
||||||
'albums_shared_users_users.updateId',
|
|
||||||
])
|
|
||||||
.where('albums_shared_users_users.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('albums_shared_users_users.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
.$if(!!ack, (qb) => qb.where('albums_shared_users_users.updateId', '>', ack!.updateId))
|
.$if(!!ack, (qb) => qb.where('albums_shared_users_users.updateId', '>', ack!.updateId))
|
||||||
.orderBy('albums_shared_users_users.updateId', 'asc')
|
.orderBy('albums_shared_users_users.updateId', 'asc')
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "albums_shared_users_users" ADD "createId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_shared_users_users" ADD "createdAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_album_users_create_id" ON "albums_shared_users_users" ("createId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_partners_create_id" ON "partners" ("createId")`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`DROP INDEX "IDX_partners_create_id";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_album_users_create_id";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_shared_users_users" DROP COLUMN "createId";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_shared_users_users" DROP COLUMN "createdAt";`.execute(db);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
const { rows } = await sql<{ db: string }>`SELECT current_database() as db;`.execute(db);
|
||||||
|
const databaseName = rows[0].db;
|
||||||
|
await sql.raw(`ALTER DATABASE "${databaseName}" RESET vchordrq.prewarm_dim;`).execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
const { rows } = await sql<{ db: string }>`SELECT current_database() as db;`.execute(db);
|
||||||
|
const databaseName = rows[0].db;
|
||||||
|
await sql
|
||||||
|
.raw(`ALTER DATABASE "${databaseName}" SET vchordrq.prewarm_dim = '512,640,768,1024,1152,1536';`)
|
||||||
|
.execute(db);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { AlbumUserRole } from 'src/enum';
|
import { AlbumUserRole } from 'src/enum';
|
||||||
import { album_user_after_insert, album_users_delete_audit } from 'src/schema/functions';
|
import { album_user_after_insert, album_users_delete_audit } from 'src/schema/functions';
|
||||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
AfterDeleteTrigger,
|
AfterDeleteTrigger,
|
||||||
AfterInsertTrigger,
|
AfterInsertTrigger,
|
||||||
Column,
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
ForeignKeyColumn,
|
ForeignKeyColumn,
|
||||||
Index,
|
Index,
|
||||||
Table,
|
Table,
|
||||||
@@ -51,6 +52,12 @@ export class AlbumUserTable {
|
|||||||
@Column({ type: 'character varying', default: AlbumUserRole.EDITOR })
|
@Column({ type: 'character varying', default: AlbumUserRole.EDITOR })
|
||||||
role!: AlbumUserRole;
|
role!: AlbumUserRole;
|
||||||
|
|
||||||
|
@CreateIdColumn({ indexName: 'IDX_album_users_create_id' })
|
||||||
|
createId?: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
@UpdateIdColumn({ indexName: 'IDX_album_users_update_id' })
|
@UpdateIdColumn({ indexName: 'IDX_album_users_update_id' })
|
||||||
updateId?: string;
|
updateId?: string;
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export class PartnerTable {
|
|||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
@CreateIdColumn()
|
@CreateIdColumn({ indexName: 'IDX_partners_create_id' })
|
||||||
createId!: string;
|
createId!: string;
|
||||||
|
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
|
|||||||
@@ -138,14 +138,14 @@ export class SyncService extends BaseService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case SyncRequestType.PartnerAssetsV1: {
|
case SyncRequestType.AssetExifsV1: {
|
||||||
await this.syncPartnerAssetsV1(response, checkpointMap, auth, sessionId);
|
await this.syncAssetExifsV1(response, checkpointMap, auth);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case SyncRequestType.AssetExifsV1: {
|
case SyncRequestType.PartnerAssetsV1: {
|
||||||
await this.syncAssetExifsV1(response, checkpointMap, auth);
|
await this.syncPartnerAssetsV1(response, checkpointMap, auth, sessionId);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ export class SyncService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case SyncRequestType.AlbumUsersV1: {
|
case SyncRequestType.AlbumUsersV1: {
|
||||||
await this.syncAlbumUsersV1(response, checkpointMap, auth);
|
await this.syncAlbumUsersV1(response, checkpointMap, auth, sessionId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,18 +330,50 @@ export class SyncService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncAlbumUsersV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
private async syncAlbumUsersV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto, sessionId: string) {
|
||||||
const deletes = this.syncRepository.getAlbumUserDeletes(
|
const backfillType = SyncEntityType.AlbumUserBackfillV1;
|
||||||
auth.user.id,
|
const upsertType = SyncEntityType.AlbumUserV1;
|
||||||
checkpointMap[SyncEntityType.AlbumUserDeleteV1],
|
const deleteType = SyncEntityType.AlbumUserDeleteV1;
|
||||||
);
|
|
||||||
|
const backfillCheckpoint = checkpointMap[backfillType];
|
||||||
|
const upsertCheckpoint = checkpointMap[upsertType];
|
||||||
|
|
||||||
|
const deletes = this.syncRepository.getAlbumUserDeletes(auth.user.id, checkpointMap[deleteType]);
|
||||||
|
|
||||||
for await (const { id, ...data } of deletes) {
|
for await (const { id, ...data } of deletes) {
|
||||||
send(response, { type: SyncEntityType.AlbumUserDeleteV1, ids: [id], data });
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
}
|
}
|
||||||
|
|
||||||
const upserts = this.syncRepository.getAlbumUserUpserts(auth.user.id, checkpointMap[SyncEntityType.AlbumUserV1]);
|
const albums = await this.syncRepository.getAlbumBackfill(auth.user.id, backfillCheckpoint?.updateId);
|
||||||
|
|
||||||
|
if (upsertCheckpoint) {
|
||||||
|
const endId = upsertCheckpoint.updateId;
|
||||||
|
|
||||||
|
for (const album of albums) {
|
||||||
|
if (isEntityBackfillComplete(album, backfillCheckpoint)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startId = getStartId(album, backfillCheckpoint);
|
||||||
|
const backfill = this.syncRepository.getAlbumUsersBackfill(album.id, startId, endId);
|
||||||
|
|
||||||
|
for await (const { updateId, ...data } of backfill) {
|
||||||
|
send(response, { type: backfillType, ids: [updateId], data });
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEntityBackfillCompleteAck(response, backfillType, album.id);
|
||||||
|
}
|
||||||
|
} else if (albums.length > 0) {
|
||||||
|
await this.upsertBackfillCheckpoint({
|
||||||
|
type: backfillType,
|
||||||
|
sessionId,
|
||||||
|
createId: albums.at(-1)!.createId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const upserts = this.syncRepository.getAlbumUserUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: SyncEntityType.AlbumUserV1, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { ColumnBaseOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
import { ColumnBaseOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||||
|
import { ForeignKeyAction } from 'src/sql-tools/from-code/decorators/foreign-key-constraint.decorator';
|
||||||
import { register } from 'src/sql-tools/from-code/register';
|
import { register } from 'src/sql-tools/from-code/register';
|
||||||
|
|
||||||
type Action = 'CASCADE' | 'SET NULL' | 'SET DEFAULT' | 'RESTRICT' | 'NO ACTION';
|
|
||||||
|
|
||||||
export type ForeignKeyColumnOptions = ColumnBaseOptions & {
|
export type ForeignKeyColumnOptions = ColumnBaseOptions & {
|
||||||
onUpdate?: Action;
|
onUpdate?: ForeignKeyAction;
|
||||||
onDelete?: Action;
|
onDelete?: ForeignKeyAction;
|
||||||
constraintName?: string;
|
constraintName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { register } from 'src/sql-tools/from-code/register';
|
||||||
|
|
||||||
|
export type ForeignKeyAction = 'CASCADE' | 'SET NULL' | 'SET DEFAULT' | 'RESTRICT' | 'NO ACTION';
|
||||||
|
|
||||||
|
export type ForeignKeyConstraintOptions = {
|
||||||
|
name?: string;
|
||||||
|
index?: boolean;
|
||||||
|
indexName?: string;
|
||||||
|
columns: string[];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
|
referenceTable: () => Function;
|
||||||
|
referenceColumns?: string[];
|
||||||
|
onUpdate?: ForeignKeyAction;
|
||||||
|
onDelete?: ForeignKeyAction;
|
||||||
|
synchronize?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ForeignKeyConstraint = (options: ForeignKeyConstraintOptions): ClassDecorator => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
|
return (target: Function) => {
|
||||||
|
register({ type: 'foreignKeyConstraint', item: { object: target, options } });
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -5,7 +5,8 @@ import { processConfigurationParameters } from 'src/sql-tools/from-code/processo
|
|||||||
import { processDatabases } from 'src/sql-tools/from-code/processors/database.processor';
|
import { processDatabases } from 'src/sql-tools/from-code/processors/database.processor';
|
||||||
import { processEnums } from 'src/sql-tools/from-code/processors/enum.processor';
|
import { processEnums } from 'src/sql-tools/from-code/processors/enum.processor';
|
||||||
import { processExtensions } from 'src/sql-tools/from-code/processors/extension.processor';
|
import { processExtensions } from 'src/sql-tools/from-code/processors/extension.processor';
|
||||||
import { processForeignKeyConstraints } from 'src/sql-tools/from-code/processors/foreign-key-constriant.processor';
|
import { processForeignKeyColumns } from 'src/sql-tools/from-code/processors/foreign-key-column.processor';
|
||||||
|
import { processForeignKeyConstraints } from 'src/sql-tools/from-code/processors/foreign-key-constraint.processor';
|
||||||
import { processFunctions } from 'src/sql-tools/from-code/processors/function.processor';
|
import { processFunctions } from 'src/sql-tools/from-code/processors/function.processor';
|
||||||
import { processIndexes } from 'src/sql-tools/from-code/processors/index.processor';
|
import { processIndexes } from 'src/sql-tools/from-code/processors/index.processor';
|
||||||
import { processPrimaryKeyConstraints } from 'src/sql-tools/from-code/processors/primary-key-contraint.processor';
|
import { processPrimaryKeyConstraints } from 'src/sql-tools/from-code/processors/primary-key-contraint.processor';
|
||||||
@@ -32,10 +33,11 @@ const processors: Processor[] = [
|
|||||||
processFunctions,
|
processFunctions,
|
||||||
processTables,
|
processTables,
|
||||||
processColumns,
|
processColumns,
|
||||||
|
processForeignKeyColumns,
|
||||||
|
processForeignKeyConstraints,
|
||||||
processUniqueConstraints,
|
processUniqueConstraints,
|
||||||
processCheckConstraints,
|
processCheckConstraints,
|
||||||
processPrimaryKeyConstraints,
|
processPrimaryKeyConstraints,
|
||||||
processForeignKeyConstraints,
|
|
||||||
processIndexes,
|
processIndexes,
|
||||||
processTriggers,
|
processTriggers,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
import { ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||||
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||||
import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type';
|
import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type';
|
||||||
import { asMetadataKey, fromColumnValue } from 'src/sql-tools/helpers';
|
import { addWarning, asMetadataKey, fromColumnValue } from 'src/sql-tools/helpers';
|
||||||
import { DatabaseColumn } from 'src/sql-tools/types';
|
import { DatabaseColumn } from 'src/sql-tools/types';
|
||||||
|
|
||||||
export const processColumns: Processor = (builder, items) => {
|
export const processColumns: Processor = (builder, items) => {
|
||||||
@@ -81,7 +81,7 @@ export const onMissingColumn = (
|
|||||||
propertyName?: symbol | string,
|
propertyName?: symbol | string,
|
||||||
) => {
|
) => {
|
||||||
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
|
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
|
||||||
builder.warnings.push(`[${context}] Unable to find column (${label})`);
|
addWarning(builder, context, `Unable to find column (${label})`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const METADATA_KEY = asMetadataKey('table-metadata');
|
const METADATA_KEY = asMetadataKey('table-metadata');
|
||||||
|
|||||||
+7
-5
@@ -1,10 +1,10 @@
|
|||||||
import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor';
|
import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor';
|
||||||
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||||
import { asKey } from 'src/sql-tools/helpers';
|
import { asForeignKeyConstraintName, asKey } from 'src/sql-tools/helpers';
|
||||||
import { DatabaseActionType, DatabaseConstraintType } from 'src/sql-tools/types';
|
import { DatabaseActionType, DatabaseConstraintType } from 'src/sql-tools/types';
|
||||||
|
|
||||||
export const processForeignKeyConstraints: Processor = (builder, items) => {
|
export const processForeignKeyColumns: Processor = (builder, items) => {
|
||||||
for (const {
|
for (const {
|
||||||
item: { object, propertyName, options, target },
|
item: { object, propertyName, options, target },
|
||||||
} of items.filter((item) => item.type === 'foreignKeyColumn')) {
|
} of items.filter((item) => item.type === 'foreignKeyColumn')) {
|
||||||
@@ -34,13 +34,16 @@ export const processForeignKeyConstraints: Processor = (builder, items) => {
|
|||||||
column.type = referenceColumns[0].type;
|
column.type = referenceColumns[0].type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const referenceColumnNames = referenceColumns.map((column) => column.name);
|
||||||
|
const name = options.constraintName || asForeignKeyConstraintName(table.name, columnNames);
|
||||||
|
|
||||||
table.constraints.push({
|
table.constraints.push({
|
||||||
name: options.constraintName || asForeignKeyConstraintName(table.name, columnNames),
|
name,
|
||||||
tableName: table.name,
|
tableName: table.name,
|
||||||
columnNames,
|
columnNames,
|
||||||
type: DatabaseConstraintType.FOREIGN_KEY,
|
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||||
referenceTableName: referenceTable.name,
|
referenceTableName: referenceTable.name,
|
||||||
referenceColumnNames: referenceColumns.map((column) => column.name),
|
referenceColumnNames,
|
||||||
onUpdate: options.onUpdate as DatabaseActionType,
|
onUpdate: options.onUpdate as DatabaseActionType,
|
||||||
onDelete: options.onDelete as DatabaseActionType,
|
onDelete: options.onDelete as DatabaseActionType,
|
||||||
synchronize: options.synchronize ?? true,
|
synchronize: options.synchronize ?? true,
|
||||||
@@ -58,5 +61,4 @@ export const processForeignKeyConstraints: Processor = (builder, items) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const asForeignKeyConstraintName = (table: string, columns: string[]) => asKey('FK_', table, columns);
|
|
||||||
const asRelationKeyConstraintName = (table: string, columns: string[]) => asKey('REL_', table, columns);
|
const asRelationKeyConstraintName = (table: string, columns: string[]) => asKey('REL_', table, columns);
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||||
|
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||||
|
import { addWarning, asForeignKeyConstraintName, asIndexName } from 'src/sql-tools/helpers';
|
||||||
|
import { DatabaseActionType, DatabaseConstraintType } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export const processForeignKeyConstraints: Processor = (builder, items, config) => {
|
||||||
|
for (const {
|
||||||
|
item: { object, options },
|
||||||
|
} of items.filter((item) => item.type === 'foreignKeyConstraint')) {
|
||||||
|
const table = resolveTable(builder, object);
|
||||||
|
if (!table) {
|
||||||
|
onMissingTable(builder, '@ForeignKeyConstraint', { name: 'referenceTable' });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const referenceTable = resolveTable(builder, options.referenceTable());
|
||||||
|
if (!referenceTable) {
|
||||||
|
const referenceTableName = options.referenceTable()?.name;
|
||||||
|
addWarning(
|
||||||
|
builder,
|
||||||
|
'@ForeignKeyConstraint.referenceTable',
|
||||||
|
`Unable to find table` + (referenceTableName ? ` (${referenceTableName})` : ''),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let missingColumn = false;
|
||||||
|
|
||||||
|
for (const columnName of options.columns) {
|
||||||
|
if (!table.columns.some(({ name }) => name === columnName)) {
|
||||||
|
addWarning(
|
||||||
|
builder,
|
||||||
|
'@ForeignKeyConstraint.columns',
|
||||||
|
`Unable to find column (${table.metadata.object.name}.${columnName})`,
|
||||||
|
);
|
||||||
|
missingColumn = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const columnName of options.referenceColumns || []) {
|
||||||
|
if (!referenceTable.columns.some(({ name }) => name === columnName)) {
|
||||||
|
addWarning(
|
||||||
|
builder,
|
||||||
|
'@ForeignKeyConstraint.referenceColumns',
|
||||||
|
`Unable to find column (${referenceTable.metadata.object.name}.${columnName})`,
|
||||||
|
);
|
||||||
|
missingColumn = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingColumn) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const referenceColumns =
|
||||||
|
options.referenceColumns || referenceTable.columns.filter(({ primary }) => primary).map(({ name }) => name);
|
||||||
|
|
||||||
|
const name = options.name || asForeignKeyConstraintName(table.name, options.columns);
|
||||||
|
|
||||||
|
table.constraints.push({
|
||||||
|
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||||
|
name,
|
||||||
|
tableName: table.name,
|
||||||
|
columnNames: options.columns,
|
||||||
|
referenceTableName: referenceTable.name,
|
||||||
|
referenceColumnNames: referenceColumns,
|
||||||
|
onUpdate: options.onUpdate as DatabaseActionType,
|
||||||
|
onDelete: options.onDelete as DatabaseActionType,
|
||||||
|
synchronize: options.synchronize ?? true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.index === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.index || options.indexName || config.createForeignKeyIndexes) {
|
||||||
|
table.indexes.push({
|
||||||
|
name: options.indexName || asIndexName(table.name, options.columns),
|
||||||
|
tableName: table.name,
|
||||||
|
columnNames: options.columns,
|
||||||
|
unique: false,
|
||||||
|
synchronize: options.synchronize ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor';
|
import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor';
|
||||||
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||||
import { asKey } from 'src/sql-tools/helpers';
|
import { asIndexName } from 'src/sql-tools/helpers';
|
||||||
|
|
||||||
export const processIndexes: Processor = (builder, items, config) => {
|
export const processIndexes: Processor = (builder, items, config) => {
|
||||||
for (const {
|
for (const {
|
||||||
@@ -75,16 +75,3 @@ export const processIndexes: Processor = (builder, items, config) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const asIndexName = (table: string, columns?: string[], where?: string) => {
|
|
||||||
const items: string[] = [];
|
|
||||||
for (const columnName of columns ?? []) {
|
|
||||||
items.push(columnName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (where) {
|
|
||||||
items.push(where);
|
|
||||||
}
|
|
||||||
|
|
||||||
return asKey('IDX_', table, items);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator';
|
import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator';
|
||||||
import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type';
|
import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type';
|
||||||
import { asMetadataKey, asSnakeCase } from 'src/sql-tools/helpers';
|
import { addWarning, asMetadataKey, asSnakeCase } from 'src/sql-tools/helpers';
|
||||||
|
|
||||||
export const processTables: Processor = (builder, items) => {
|
export const processTables: Processor = (builder, items) => {
|
||||||
for (const {
|
for (const {
|
||||||
@@ -45,7 +45,7 @@ export const onMissingTable = (
|
|||||||
propertyName?: symbol | string,
|
propertyName?: symbol | string,
|
||||||
) => {
|
) => {
|
||||||
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
|
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
|
||||||
builder.warnings.push(`[${context}] Unable to find table (${label})`);
|
addWarning(builder, context, `Unable to find table (${label})`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const METADATA_KEY = asMetadataKey('table-metadata');
|
const METADATA_KEY = asMetadataKey('table-metadata');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ConfigurationParameterOptions } from 'src/sql-tools/from-code/decorator
|
|||||||
import { DatabaseOptions } from 'src/sql-tools/from-code/decorators/database.decorator';
|
import { DatabaseOptions } from 'src/sql-tools/from-code/decorators/database.decorator';
|
||||||
import { ExtensionOptions } from 'src/sql-tools/from-code/decorators/extension.decorator';
|
import { ExtensionOptions } from 'src/sql-tools/from-code/decorators/extension.decorator';
|
||||||
import { ForeignKeyColumnOptions } from 'src/sql-tools/from-code/decorators/foreign-key-column.decorator';
|
import { ForeignKeyColumnOptions } from 'src/sql-tools/from-code/decorators/foreign-key-column.decorator';
|
||||||
|
import { ForeignKeyConstraintOptions } from 'src/sql-tools/from-code/decorators/foreign-key-constraint.decorator';
|
||||||
import { IndexOptions } from 'src/sql-tools/from-code/decorators/index.decorator';
|
import { IndexOptions } from 'src/sql-tools/from-code/decorators/index.decorator';
|
||||||
import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator';
|
import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator';
|
||||||
import { TriggerOptions } from 'src/sql-tools/from-code/decorators/trigger.decorator';
|
import { TriggerOptions } from 'src/sql-tools/from-code/decorators/trigger.decorator';
|
||||||
@@ -25,5 +26,6 @@ export type RegisterItem =
|
|||||||
| { type: 'trigger'; item: ClassBased<{ options: TriggerOptions }> }
|
| { type: 'trigger'; item: ClassBased<{ options: TriggerOptions }> }
|
||||||
| { type: 'extension'; item: ClassBased<{ options: ExtensionOptions }> }
|
| { type: 'extension'; item: ClassBased<{ options: ExtensionOptions }> }
|
||||||
| { type: 'configurationParameter'; item: ClassBased<{ options: ConfigurationParameterOptions }> }
|
| { type: 'configurationParameter'; item: ClassBased<{ options: ConfigurationParameterOptions }> }
|
||||||
| { type: 'foreignKeyColumn'; item: PropertyBased<{ options: ForeignKeyColumnOptions; target: () => object }> };
|
| { type: 'foreignKeyColumn'; item: PropertyBased<{ options: ForeignKeyColumnOptions; target: () => object }> }
|
||||||
|
| { type: 'foreignKeyConstraint'; item: ClassBased<{ options: ForeignKeyConstraintOptions }> };
|
||||||
export type RegisterItemType<T extends RegisterItem['type']> = Extract<RegisterItem, { type: T }>['item'];
|
export type RegisterItemType<T extends RegisterItem['type']> = Extract<RegisterItem, { type: T }>['item'];
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
import { ColumnValue } from 'src/sql-tools/from-code/decorators/column.decorator';
|
import { ColumnValue } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||||
|
import { SchemaBuilder } from 'src/sql-tools/from-code/processors/type';
|
||||||
import {
|
import {
|
||||||
Comparer,
|
Comparer,
|
||||||
DatabaseColumn,
|
DatabaseColumn,
|
||||||
@@ -211,3 +212,22 @@ export const asColumnComment = (tableName: string, columnName: string, comment:
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const asColumnList = (columns: string[]) => columns.map((column) => `"${column}"`).join(', ');
|
export const asColumnList = (columns: string[]) => columns.map((column) => `"${column}"`).join(', ');
|
||||||
|
|
||||||
|
export const asForeignKeyConstraintName = (table: string, columns: string[]) => asKey('FK_', table, [...columns]);
|
||||||
|
|
||||||
|
export const asIndexName = (table: string, columns?: string[], where?: string) => {
|
||||||
|
const items: string[] = [];
|
||||||
|
for (const columnName of columns ?? []) {
|
||||||
|
items.push(columnName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (where) {
|
||||||
|
items.push(where);
|
||||||
|
}
|
||||||
|
|
||||||
|
return asKey('IDX_', table, items);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addWarning = (builder: SchemaBuilder, context: string, message: string) => {
|
||||||
|
builder.warnings.push(`[${context}] ${message}`);
|
||||||
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export * from 'src/sql-tools/from-code/decorators/delete-date-column.decorator';
|
|||||||
export * from 'src/sql-tools/from-code/decorators/extension.decorator';
|
export * from 'src/sql-tools/from-code/decorators/extension.decorator';
|
||||||
export * from 'src/sql-tools/from-code/decorators/extensions.decorator';
|
export * from 'src/sql-tools/from-code/decorators/extensions.decorator';
|
||||||
export * from 'src/sql-tools/from-code/decorators/foreign-key-column.decorator';
|
export * from 'src/sql-tools/from-code/decorators/foreign-key-column.decorator';
|
||||||
|
export * from 'src/sql-tools/from-code/decorators/foreign-key-constraint.decorator';
|
||||||
export * from 'src/sql-tools/from-code/decorators/generated-column.decorator';
|
export * from 'src/sql-tools/from-code/decorators/generated-column.decorator';
|
||||||
export * from 'src/sql-tools/from-code/decorators/index.decorator';
|
export * from 'src/sql-tools/from-code/decorators/index.decorator';
|
||||||
export * from 'src/sql-tools/from-code/decorators/primary-column.decorator';
|
export * from 'src/sql-tools/from-code/decorators/primary-column.decorator';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Kysely } from 'kysely';
|
|||||||
import { DB } from 'src/db';
|
import { DB } from 'src/db';
|
||||||
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||||
import { getKyselyDB } from 'test/utils';
|
import { getKyselyDB, wait } from 'test/utils';
|
||||||
|
|
||||||
let defaultDatabase: Kysely<DB>;
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
@@ -265,5 +265,95 @@ describe(SyncRequestType.AlbumUsersV1, () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should backfill album users when a user shares an album with you', async () => {
|
||||||
|
const { auth, sut, testSync, getRepository } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user1 = mediumFactory.userInsert();
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user1);
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const album1 = mediumFactory.albumInsert({ ownerId: user1.id });
|
||||||
|
const album2 = mediumFactory.albumInsert({ ownerId: user1.id });
|
||||||
|
await albumRepo.create(album1, [], []);
|
||||||
|
await albumRepo.create(album2, [], []);
|
||||||
|
|
||||||
|
// backfill album user
|
||||||
|
await albumUserRepo.create({ albumsId: album1.id, usersId: user1.id, role: AlbumUserRole.EDITOR });
|
||||||
|
await wait(2);
|
||||||
|
// initial album user
|
||||||
|
await albumUserRepo.create({ albumsId: album2.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||||
|
await wait(2);
|
||||||
|
// post checkpoint album user
|
||||||
|
await albumUserRepo.create({ albumsId: album1.id, usersId: user2.id, role: AlbumUserRole.EDITOR });
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||||
|
expect(response).toHaveLength(1);
|
||||||
|
expect(response).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: album2.id,
|
||||||
|
role: AlbumUserRole.EDITOR,
|
||||||
|
userId: auth.user.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ack initial user
|
||||||
|
const acks = response.map(({ ack }) => ack);
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
// get access to the backfill album user
|
||||||
|
await albumUserRepo.create({ albumsId: album1.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||||
|
|
||||||
|
// should backfill the album user
|
||||||
|
const backfillResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||||
|
expect(backfillResponse).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: album1.id,
|
||||||
|
role: AlbumUserRole.EDITOR,
|
||||||
|
userId: user1.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserBackfillV1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.stringContaining(SyncEntityType.AlbumUserBackfillV1),
|
||||||
|
data: {},
|
||||||
|
type: SyncEntityType.SyncAckV1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: album1.id,
|
||||||
|
role: AlbumUserRole.EDITOR,
|
||||||
|
userId: user2.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserV1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: album1.id,
|
||||||
|
role: AlbumUserRole.EDITOR,
|
||||||
|
userId: auth.user.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await sut.setAcks(auth, { acks: [backfillResponse[1].ack, backfillResponse.at(-1).ack] });
|
||||||
|
|
||||||
|
const finalResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||||
|
expect(finalResponse).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
DatabaseConstraintType,
|
||||||
|
DatabaseSchema,
|
||||||
|
ForeignKeyConstraint,
|
||||||
|
PrimaryColumn,
|
||||||
|
Table,
|
||||||
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
export class Table1 {
|
||||||
|
@PrimaryColumn({ type: 'uuid' })
|
||||||
|
id1!: string;
|
||||||
|
|
||||||
|
@PrimaryColumn({ type: 'uuid' })
|
||||||
|
id2!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
@ForeignKeyConstraint({
|
||||||
|
columns: ['parentId1', 'parentId2'],
|
||||||
|
referenceTable: () => Table1,
|
||||||
|
referenceColumns: ['id2', 'id1'],
|
||||||
|
})
|
||||||
|
export class Table2 {
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
parentId1!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
parentId2!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const description = 'should create a foreign key constraint to the target table';
|
||||||
|
export const schema: DatabaseSchema = {
|
||||||
|
name: 'postgres',
|
||||||
|
schemaName: 'public',
|
||||||
|
functions: [],
|
||||||
|
enums: [],
|
||||||
|
extensions: [],
|
||||||
|
parameters: [],
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: 'table1',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'id1',
|
||||||
|
tableName: 'table1',
|
||||||
|
type: 'uuid',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: true,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'id2',
|
||||||
|
tableName: 'table1',
|
||||||
|
type: 'uuid',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: true,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
triggers: [],
|
||||||
|
constraints: [
|
||||||
|
{
|
||||||
|
type: DatabaseConstraintType.PRIMARY_KEY,
|
||||||
|
name: 'PK_e457e8b1301b7bc06ef78188ee4',
|
||||||
|
tableName: 'table1',
|
||||||
|
columnNames: ['id1', 'id2'],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'table2',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'parentId1',
|
||||||
|
tableName: 'table2',
|
||||||
|
type: 'uuid',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: false,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parentId2',
|
||||||
|
tableName: 'table2',
|
||||||
|
type: 'uuid',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: false,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
name: 'IDX_aed36d04470eba20161aa8b1dc',
|
||||||
|
tableName: 'table2',
|
||||||
|
columnNames: ['parentId1', 'parentId2'],
|
||||||
|
unique: false,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
triggers: [],
|
||||||
|
constraints: [
|
||||||
|
{
|
||||||
|
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||||
|
name: 'FK_aed36d04470eba20161aa8b1dc6',
|
||||||
|
tableName: 'table2',
|
||||||
|
columnNames: ['parentId1', 'parentId2'],
|
||||||
|
referenceColumnNames: ['id2', 'id1'],
|
||||||
|
referenceTableName: 'table1',
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
warnings: [],
|
||||||
|
};
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
DatabaseConstraintType,
|
||||||
|
DatabaseSchema,
|
||||||
|
ForeignKeyConstraint,
|
||||||
|
PrimaryColumn,
|
||||||
|
Table,
|
||||||
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
export class Table1 {
|
||||||
|
@PrimaryColumn({ type: 'uuid' })
|
||||||
|
id!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
@ForeignKeyConstraint({ columns: ['parentId2'], referenceTable: () => Table1 })
|
||||||
|
export class Table2 {
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
parentId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const description = 'should warn against missing column in foreign key constraint';
|
||||||
|
export const schema: DatabaseSchema = {
|
||||||
|
name: 'postgres',
|
||||||
|
schemaName: 'public',
|
||||||
|
functions: [],
|
||||||
|
enums: [],
|
||||||
|
extensions: [],
|
||||||
|
parameters: [],
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: 'table1',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
tableName: 'table1',
|
||||||
|
type: 'uuid',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: true,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
triggers: [],
|
||||||
|
constraints: [
|
||||||
|
{
|
||||||
|
type: DatabaseConstraintType.PRIMARY_KEY,
|
||||||
|
name: 'PK_b249cc64cf63b8a22557cdc8537',
|
||||||
|
tableName: 'table1',
|
||||||
|
columnNames: ['id'],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'table2',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'parentId',
|
||||||
|
tableName: 'table2',
|
||||||
|
type: 'uuid',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: false,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
triggers: [],
|
||||||
|
constraints: [],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
warnings: ['[@ForeignKeyConstraint.columns] Unable to find column (Table2.parentId2)'],
|
||||||
|
};
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
DatabaseConstraintType,
|
||||||
|
DatabaseSchema,
|
||||||
|
ForeignKeyConstraint,
|
||||||
|
PrimaryColumn,
|
||||||
|
Table,
|
||||||
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
export class Table1 {
|
||||||
|
@PrimaryColumn({ type: 'uuid' })
|
||||||
|
id!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
@ForeignKeyConstraint({ columns: ['parentId'], referenceTable: () => Table1, referenceColumns: ['foo'] })
|
||||||
|
export class Table2 {
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
parentId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const description = 'should warn against missing reference column in foreign key constraint';
|
||||||
|
export const schema: DatabaseSchema = {
|
||||||
|
name: 'postgres',
|
||||||
|
schemaName: 'public',
|
||||||
|
functions: [],
|
||||||
|
enums: [],
|
||||||
|
extensions: [],
|
||||||
|
parameters: [],
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: 'table1',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
tableName: 'table1',
|
||||||
|
type: 'uuid',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: true,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
triggers: [],
|
||||||
|
constraints: [
|
||||||
|
{
|
||||||
|
type: DatabaseConstraintType.PRIMARY_KEY,
|
||||||
|
name: 'PK_b249cc64cf63b8a22557cdc8537',
|
||||||
|
tableName: 'table1',
|
||||||
|
columnNames: ['id'],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'table2',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'parentId',
|
||||||
|
tableName: 'table2',
|
||||||
|
type: 'uuid',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: false,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
triggers: [],
|
||||||
|
constraints: [],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
warnings: ['[@ForeignKeyConstraint.referenceColumns] Unable to find column (Table1.foo)'],
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Column, DatabaseSchema, ForeignKeyConstraint, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
|
class Foo {}
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
@ForeignKeyConstraint({
|
||||||
|
columns: ['parentId'],
|
||||||
|
referenceTable: () => Foo,
|
||||||
|
})
|
||||||
|
export class Table1 {
|
||||||
|
@Column()
|
||||||
|
parentId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const description = 'should warn against missing reference table';
|
||||||
|
export const schema: DatabaseSchema = {
|
||||||
|
name: 'postgres',
|
||||||
|
schemaName: 'public',
|
||||||
|
functions: [],
|
||||||
|
enums: [],
|
||||||
|
extensions: [],
|
||||||
|
parameters: [],
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: 'table1',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'parentId',
|
||||||
|
tableName: 'table1',
|
||||||
|
type: 'character varying',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: false,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
triggers: [],
|
||||||
|
constraints: [],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
warnings: ['[@ForeignKeyConstraint.referenceTable] Unable to find table (Foo)'],
|
||||||
|
};
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
DatabaseConstraintType,
|
||||||
|
DatabaseSchema,
|
||||||
|
ForeignKeyConstraint,
|
||||||
|
PrimaryColumn,
|
||||||
|
Table,
|
||||||
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
export class Table1 {
|
||||||
|
@PrimaryColumn({ type: 'uuid' })
|
||||||
|
id1!: string;
|
||||||
|
|
||||||
|
@PrimaryColumn({ type: 'uuid' })
|
||||||
|
id2!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
@ForeignKeyConstraint({ columns: ['parentId1', 'parentId2'], referenceTable: () => Table1 })
|
||||||
|
export class Table2 {
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
parentId1!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
parentId2!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const description = 'should create a foreign key constraint to the target table';
|
||||||
|
export const schema: DatabaseSchema = {
|
||||||
|
name: 'postgres',
|
||||||
|
schemaName: 'public',
|
||||||
|
functions: [],
|
||||||
|
enums: [],
|
||||||
|
extensions: [],
|
||||||
|
parameters: [],
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: 'table1',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'id1',
|
||||||
|
tableName: 'table1',
|
||||||
|
type: 'uuid',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: true,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'id2',
|
||||||
|
tableName: 'table1',
|
||||||
|
type: 'uuid',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: true,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
triggers: [],
|
||||||
|
constraints: [
|
||||||
|
{
|
||||||
|
type: DatabaseConstraintType.PRIMARY_KEY,
|
||||||
|
name: 'PK_e457e8b1301b7bc06ef78188ee4',
|
||||||
|
tableName: 'table1',
|
||||||
|
columnNames: ['id1', 'id2'],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'table2',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'parentId1',
|
||||||
|
tableName: 'table2',
|
||||||
|
type: 'uuid',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: false,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parentId2',
|
||||||
|
tableName: 'table2',
|
||||||
|
type: 'uuid',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: false,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
name: 'IDX_aed36d04470eba20161aa8b1dc',
|
||||||
|
tableName: 'table2',
|
||||||
|
columnNames: ['parentId1', 'parentId2'],
|
||||||
|
unique: false,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
triggers: [],
|
||||||
|
constraints: [
|
||||||
|
{
|
||||||
|
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||||
|
name: 'FK_aed36d04470eba20161aa8b1dc6',
|
||||||
|
tableName: 'table2',
|
||||||
|
columnNames: ['parentId1', 'parentId2'],
|
||||||
|
referenceColumnNames: ['id1', 'id2'],
|
||||||
|
referenceTableName: 'table1',
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
warnings: [],
|
||||||
|
};
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
DatabaseConstraintType,
|
||||||
|
DatabaseSchema,
|
||||||
|
ForeignKeyConstraint,
|
||||||
|
PrimaryColumn,
|
||||||
|
Table,
|
||||||
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
export class Table1 {
|
||||||
|
@PrimaryColumn({ type: 'uuid' })
|
||||||
|
id!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
@ForeignKeyConstraint({ columns: ['parentId'], referenceTable: () => Table1, index: false })
|
||||||
|
export class Table2 {
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
parentId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const description = 'should create a foreign key constraint to the target table without an index';
|
||||||
|
export const schema: DatabaseSchema = {
|
||||||
|
name: 'postgres',
|
||||||
|
schemaName: 'public',
|
||||||
|
functions: [],
|
||||||
|
enums: [],
|
||||||
|
extensions: [],
|
||||||
|
parameters: [],
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: 'table1',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
tableName: 'table1',
|
||||||
|
type: 'uuid',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: true,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
triggers: [],
|
||||||
|
constraints: [
|
||||||
|
{
|
||||||
|
type: DatabaseConstraintType.PRIMARY_KEY,
|
||||||
|
name: 'PK_b249cc64cf63b8a22557cdc8537',
|
||||||
|
tableName: 'table1',
|
||||||
|
columnNames: ['id'],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'table2',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'parentId',
|
||||||
|
tableName: 'table2',
|
||||||
|
type: 'uuid',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: false,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
triggers: [],
|
||||||
|
constraints: [
|
||||||
|
{
|
||||||
|
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||||
|
name: 'FK_3fcca5cc563abf256fc346e3ff4',
|
||||||
|
tableName: 'table2',
|
||||||
|
columnNames: ['parentId'],
|
||||||
|
referenceColumnNames: ['id'],
|
||||||
|
referenceTableName: 'table1',
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
warnings: [],
|
||||||
|
};
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { Column, DatabaseConstraintType, DatabaseSchema, ForeignKeyConstraint, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
export class Table1 {
|
||||||
|
@Column()
|
||||||
|
foo!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
@ForeignKeyConstraint({
|
||||||
|
columns: ['bar'],
|
||||||
|
referenceTable: () => Table1,
|
||||||
|
referenceColumns: ['foo'],
|
||||||
|
})
|
||||||
|
export class Table2 {
|
||||||
|
@Column()
|
||||||
|
bar!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const description = 'should create a foreign key constraint to the target table without a primary key';
|
||||||
|
export const schema: DatabaseSchema = {
|
||||||
|
name: 'postgres',
|
||||||
|
schemaName: 'public',
|
||||||
|
functions: [],
|
||||||
|
enums: [],
|
||||||
|
extensions: [],
|
||||||
|
parameters: [],
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: 'table1',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
tableName: 'table1',
|
||||||
|
type: 'character varying',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: false,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
triggers: [],
|
||||||
|
constraints: [],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'table2',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
tableName: 'table2',
|
||||||
|
type: 'character varying',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: false,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
name: 'IDX_7d9c784c98d12365d198d52e4e',
|
||||||
|
tableName: 'table2',
|
||||||
|
columnNames: ['bar'],
|
||||||
|
unique: false,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
triggers: [],
|
||||||
|
constraints: [
|
||||||
|
{
|
||||||
|
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||||
|
name: 'FK_7d9c784c98d12365d198d52e4e6',
|
||||||
|
tableName: 'table2',
|
||||||
|
columnNames: ['bar'],
|
||||||
|
referenceTableName: 'table1',
|
||||||
|
referenceColumnNames: ['foo'],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
warnings: [],
|
||||||
|
};
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
DatabaseConstraintType,
|
||||||
|
DatabaseSchema,
|
||||||
|
ForeignKeyConstraint,
|
||||||
|
PrimaryColumn,
|
||||||
|
Table,
|
||||||
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
export class Table1 {
|
||||||
|
@PrimaryColumn({ type: 'uuid' })
|
||||||
|
id!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Table()
|
||||||
|
@ForeignKeyConstraint({ columns: ['parentId'], referenceTable: () => Table1 })
|
||||||
|
export class Table2 {
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
parentId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const description = 'should create a foreign key constraint to the target table';
|
||||||
|
export const schema: DatabaseSchema = {
|
||||||
|
name: 'postgres',
|
||||||
|
schemaName: 'public',
|
||||||
|
functions: [],
|
||||||
|
enums: [],
|
||||||
|
extensions: [],
|
||||||
|
parameters: [],
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: 'table1',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
tableName: 'table1',
|
||||||
|
type: 'uuid',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: true,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
triggers: [],
|
||||||
|
constraints: [
|
||||||
|
{
|
||||||
|
type: DatabaseConstraintType.PRIMARY_KEY,
|
||||||
|
name: 'PK_b249cc64cf63b8a22557cdc8537',
|
||||||
|
tableName: 'table1',
|
||||||
|
columnNames: ['id'],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'table2',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'parentId',
|
||||||
|
tableName: 'table2',
|
||||||
|
type: 'uuid',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
primary: false,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
name: 'IDX_3fcca5cc563abf256fc346e3ff',
|
||||||
|
tableName: 'table2',
|
||||||
|
columnNames: ['parentId'],
|
||||||
|
unique: false,
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
triggers: [],
|
||||||
|
constraints: [
|
||||||
|
{
|
||||||
|
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||||
|
name: 'FK_3fcca5cc563abf256fc346e3ff4',
|
||||||
|
tableName: 'table2',
|
||||||
|
columnNames: ['parentId'],
|
||||||
|
referenceColumnNames: ['id'],
|
||||||
|
referenceTableName: 'table1',
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
warnings: [],
|
||||||
|
};
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
import MapModal from '$lib/modals/MapModal.svelte';
|
import MapModal from '$lib/modals/MapModal.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
|
||||||
import { navigate } from '$lib/utils/navigation';
|
|
||||||
import { getAlbumInfo, type AlbumResponseDto, type MapMarkerResponseDto } from '@immich/sdk';
|
import { getAlbumInfo, type AlbumResponseDto, type MapMarkerResponseDto } from '@immich/sdk';
|
||||||
import { IconButton } from '@immich/ui';
|
import { IconButton } from '@immich/ui';
|
||||||
import { mdiMapOutline } from '@mdi/js';
|
import { mdiMapOutline } from '@mdi/js';
|
||||||
@@ -17,9 +14,7 @@
|
|||||||
|
|
||||||
let { album }: Props = $props();
|
let { album }: Props = $props();
|
||||||
let abortController: AbortController;
|
let abortController: AbortController;
|
||||||
let { isViewing: showAssetViewer, asset: viewingAsset, setAssetId } = assetViewingStore;
|
let { setAssetId } = assetViewingStore;
|
||||||
let viewingAssets: string[] = $state([]);
|
|
||||||
let viewingAssetCursor = 0;
|
|
||||||
|
|
||||||
let mapMarkers: MapMarkerResponseDto[] = $state([]);
|
let mapMarkers: MapMarkerResponseDto[] = $state([]);
|
||||||
|
|
||||||
@@ -61,37 +56,9 @@
|
|||||||
const assetIds = await modalManager.show(MapModal, { mapMarkers });
|
const assetIds = await modalManager.show(MapModal, { mapMarkers });
|
||||||
|
|
||||||
if (assetIds) {
|
if (assetIds) {
|
||||||
viewingAssets = assetIds;
|
|
||||||
viewingAssetCursor = 0;
|
|
||||||
|
|
||||||
await setAssetId(assetIds[0]);
|
await setAssetId(assetIds[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function navigateNext() {
|
|
||||||
if (viewingAssetCursor < viewingAssets.length - 1) {
|
|
||||||
await setAssetId(viewingAssets[++viewingAssetCursor]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function navigatePrevious() {
|
|
||||||
if (viewingAssetCursor > 0) {
|
|
||||||
await setAssetId(viewingAssets[--viewingAssetCursor]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function navigateRandom() {
|
|
||||||
if (viewingAssets.length <= 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const index = Math.floor(Math.random() * viewingAssets.length);
|
|
||||||
const asset = await setAssetId(viewingAssets[index]);
|
|
||||||
return asset;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -102,22 +69,3 @@
|
|||||||
onclick={openMap}
|
onclick={openMap}
|
||||||
aria-label={$t('map')}
|
aria-label={$t('map')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Portal target="body">
|
|
||||||
{#if $showAssetViewer}
|
|
||||||
{#await import('../../../lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
|
||||||
<AssetViewer
|
|
||||||
asset={$viewingAsset}
|
|
||||||
showNavigation={viewingAssets.length > 1}
|
|
||||||
onNext={navigateNext}
|
|
||||||
onPrevious={navigatePrevious}
|
|
||||||
onRandom={navigateRandom}
|
|
||||||
onClose={() => {
|
|
||||||
assetViewingStore.showAssetViewer(false);
|
|
||||||
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
|
|
||||||
}}
|
|
||||||
isShared={false}
|
|
||||||
/>
|
|
||||||
{/await}
|
|
||||||
{/if}
|
|
||||||
</Portal>
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
{@const isCollapsed = isAlbumGroupCollapsed($albumViewSettings, albumGroup.id)}
|
{@const isCollapsed = isAlbumGroupCollapsed($albumViewSettings, albumGroup.id)}
|
||||||
{@const iconRotation = isCollapsed ? 'rotate-0' : 'rotate-90'}
|
{@const iconRotation = isCollapsed ? 'rotate-0' : 'rotate-90'}
|
||||||
<tbody
|
<tbody
|
||||||
class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg"
|
class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg mt-4"
|
||||||
>
|
>
|
||||||
<tr
|
<tr
|
||||||
class="flex w-full place-items-center p-2 md:ps-5 md:pe-5 md:pt-3 md:pb-3"
|
class="flex w-full place-items-center p-2 md:ps-5 md:pe-5 md:pt-3 md:pb-3"
|
||||||
|
|||||||
@@ -108,6 +108,30 @@
|
|||||||
}
|
}
|
||||||
await modalManager.show(SlideshowSettingsModal);
|
await modalManager.show(SlideshowSettingsModal);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
function exitFullscreenHandler() {
|
||||||
|
const doc = document as Document & {
|
||||||
|
webkitIsFullScreen?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
// eslint-disable-next-line tscompat/tscompat
|
||||||
|
!document.fullscreenElement &&
|
||||||
|
!doc.webkitIsFullScreen
|
||||||
|
) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('fullscreenchange', exitFullscreenHandler);
|
||||||
|
document.addEventListener('webkitfullscreenchange', exitFullscreenHandler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('fullscreenchange', exitFullscreenHandler);
|
||||||
|
document.removeEventListener('webkitfullscreenchange', exitFullscreenHandler);
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document
|
<svelte:document
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
|
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
|
||||||
import Scrubber from '$lib/components/shared-components/scrubber/scrubber.svelte';
|
import Scrubber from '$lib/components/shared-components/scrubber/scrubber.svelte';
|
||||||
import { AppRoute, AssetAction } from '$lib/constants';
|
import { AppRoute, AssetAction } from '$lib/constants';
|
||||||
import { albumMapViewManager } from '$lib/managers/album-view-map.manager.svelte';
|
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
||||||
@@ -911,28 +910,26 @@
|
|||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{#if !albumMapViewManager.isInMapView}
|
<Portal target="body">
|
||||||
<Portal target="body">
|
{#if $showAssetViewer}
|
||||||
{#if $showAssetViewer}
|
{#await import('../asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
||||||
{#await import('../asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
<AssetViewer
|
||||||
<AssetViewer
|
{withStacked}
|
||||||
{withStacked}
|
asset={$viewingAsset}
|
||||||
asset={$viewingAsset}
|
preloadAssets={$preloadAssets}
|
||||||
preloadAssets={$preloadAssets}
|
{isShared}
|
||||||
{isShared}
|
{album}
|
||||||
{album}
|
{person}
|
||||||
{person}
|
preAction={handlePreAction}
|
||||||
preAction={handlePreAction}
|
onAction={handleAction}
|
||||||
onAction={handleAction}
|
onPrevious={handlePrevious}
|
||||||
onPrevious={handlePrevious}
|
onNext={handleNext}
|
||||||
onNext={handleNext}
|
onRandom={handleRandom}
|
||||||
onRandom={handleRandom}
|
onClose={handleClose}
|
||||||
onClose={handleClose}
|
/>
|
||||||
/>
|
{/await}
|
||||||
{/await}
|
{/if}
|
||||||
{/if}
|
</Portal>
|
||||||
</Portal>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#asset-grid {
|
#asset-grid {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
import { isEqual, omit } from 'lodash-es';
|
import { isEqual, omit } from 'lodash-es';
|
||||||
import { DateTime, Duration } from 'luxon';
|
import { DateTime, Duration } from 'luxon';
|
||||||
import maplibregl, { GlobeControl, type GeoJSONSource, type LngLatLike } from 'maplibre-gl';
|
import maplibregl, { GlobeControl, type GeoJSONSource, type LngLatLike } from 'maplibre-gl';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount, untrack } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import {
|
import {
|
||||||
AttributionControl,
|
AttributionControl,
|
||||||
@@ -251,7 +251,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
map?.jumpTo({ center, zoom });
|
if (!center || !zoom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
untrack(() => map?.jumpTo({ center, zoom }));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
class AlbumMapViewManager {
|
|
||||||
#isInMapView = $state(false);
|
|
||||||
|
|
||||||
get isInMapView() {
|
|
||||||
return this.#isInMapView;
|
|
||||||
}
|
|
||||||
|
|
||||||
set isInMapView(isInMapView: boolean) {
|
|
||||||
this.#isInMapView = isInMapView;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const albumMapViewManager = new AlbumMapViewManager();
|
|
||||||
@@ -32,7 +32,7 @@ export async function getAssetWithOffset(
|
|||||||
|
|
||||||
export function findMonthGroupForAsset(timelineManager: TimelineManager, id: string) {
|
export function findMonthGroupForAsset(timelineManager: TimelineManager, id: string) {
|
||||||
for (const month of timelineManager.months) {
|
for (const month of timelineManager.months) {
|
||||||
const asset = month.findAssetById({ id });
|
const asset = month.findAssetById(id);
|
||||||
if (asset) {
|
if (asset) {
|
||||||
return { monthGroup: month, asset };
|
return { monthGroup: month, asset };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { get } from 'svelte/store';
|
|||||||
import { DayGroup } from './day-group.svelte';
|
import { DayGroup } from './day-group.svelte';
|
||||||
import { GroupInsertionCache } from './group-insertion-cache.svelte';
|
import { GroupInsertionCache } from './group-insertion-cache.svelte';
|
||||||
import type { TimelineManager } from './timeline-manager.svelte';
|
import type { TimelineManager } from './timeline-manager.svelte';
|
||||||
import type { AssetDescriptor, AssetOperation, Direction, MoveAsset, TimelineAsset } from './types';
|
import type { AssetOperation, Direction, MoveAsset, TimelineAsset } from './types';
|
||||||
import { ViewerAsset } from './viewer-asset.svelte';
|
import { ViewerAsset } from './viewer-asset.svelte';
|
||||||
|
|
||||||
export class MonthGroup {
|
export class MonthGroup {
|
||||||
@@ -342,8 +342,12 @@ export class MonthGroup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findAssetById(assetDescriptor: AssetDescriptor) {
|
findAssetById(id: string) {
|
||||||
return this.assetsIterator().find((asset) => asset.id === assetDescriptor.id);
|
for (const asset of this.assetsIterator()) {
|
||||||
|
if (asset.id === id) {
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findClosest(target: TimelinePlainDateTime) {
|
findClosest(target: TimelinePlainDateTime) {
|
||||||
|
|||||||
@@ -428,7 +428,7 @@ export class TimelineManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
monthGroup = await this.#loadMonthGroupAtTime(asset.localDateTime, { cancelable: false });
|
monthGroup = await this.#loadMonthGroupAtTime(asset.localDateTime, { cancelable: false });
|
||||||
if (monthGroup?.findAssetById({ id })) {
|
if (monthGroup?.findAssetById(id)) {
|
||||||
return monthGroup;
|
return monthGroup;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,7 +171,7 @@
|
|||||||
{#if sharedLinks.length > 0}
|
{#if sharedLinks.length > 0}
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<Text>{$t('shared_links')}</Text>
|
<Text>{$t('shared_links')}</Text>
|
||||||
<Link href={AppRoute.SHARED_LINKS} class="text-sm">{$t('view_all')}</Link>
|
<Link href={AppRoute.SHARED_LINKS} onclick={() => onClose()} class="text-sm">{$t('view_all')}</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Stack gap={4}>
|
<Stack gap={4}>
|
||||||
|
|||||||
Reference in New Issue
Block a user