mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
Merge branch 'main' into rknn-toolkit-lite2
This commit is contained in:
commit
794da29411
2
.github/workflows/cli.yml
vendored
2
.github/workflows/cli.yml
vendored
@ -88,7 +88,7 @@ jobs:
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.11.0
|
||||
uses: docker/build-push-action@v6.12.0
|
||||
with:
|
||||
file: cli/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@ -177,7 +177,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.11.0
|
||||
uses: docker/build-push-action@v6.12.0
|
||||
with:
|
||||
context: ${{ env.context }}
|
||||
file: ${{ env.file }}
|
||||
@ -268,7 +268,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.11.0
|
||||
uses: docker/build-push-action@v6.12.0
|
||||
with:
|
||||
context: ${{ env.context }}
|
||||
file: ${{ env.file }}
|
||||
|
@ -1 +1 @@
|
||||
22.13.0
|
||||
22.13.1
|
||||
|
10
cli/package-lock.json
generated
10
cli/package-lock.json
generated
@ -24,7 +24,7 @@
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||
"@typescript-eslint/parser": "^8.15.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
@ -59,7 +59,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@ -1397,9 +1397,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
|
||||
"integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
|
||||
"version": "22.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
||||
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -20,7 +20,7 @@
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||
"@typescript-eslint/parser": "^8.15.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
@ -67,6 +67,6 @@
|
||||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.13.0"
|
||||
"node": "22.13.1"
|
||||
}
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
22.13.0
|
||||
22.13.1
|
||||
|
@ -160,6 +160,35 @@ For example, say you have existing transcodes with the policy "Videos higher tha
|
||||
|
||||
No. Our design principle is that the original assets should always be untouched.
|
||||
|
||||
### How can I mount a CIFS/Samba volume within Docker?
|
||||
|
||||
If you aren't able to or prefer not to mount Samba on the host (such as Windows environment), you can mount the volume within Docker.
|
||||
Below is an example in the `docker-compose.yml`.
|
||||
|
||||
Change your username, password, local IP, and share name, and see below where the line `- originals:/usr/src/app/originals`,
|
||||
corrolates to the section where the volume `originals` was created. You can call this whatever you like, and map it to the docker container as you like.
|
||||
For example you could change `originals:` to `Photos:`, and change `- originals:/usr/src/app/originals` to `Photos:/usr/src/app/photos`.
|
||||
|
||||
```diff
|
||||
...
|
||||
services:
|
||||
immich-server:
|
||||
...
|
||||
volumes:
|
||||
# Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
+ - originals:/usr/src/app/originals
|
||||
...
|
||||
volumes:
|
||||
model-cache:
|
||||
+ originals:
|
||||
+ driver_opts:
|
||||
+ type: cifs
|
||||
+ o: 'iocharset=utf8,username=USERNAMEHERE,password=PASSWORDHERE,rw' # change to `ro` if read only desired
|
||||
+ device: '//localipaddress/sharename'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Albums
|
||||
|
@ -68,7 +68,7 @@ After bringing down the containers with `docker compose down` and back up with `
|
||||
:::note
|
||||
To see exactly what metrics are made available, you can additionally add `8081:8081` to the server container's ports and `8082:8082` to the microservices container's ports.
|
||||
Visiting the `/metrics` endpoint for these services will show the same raw data that Prometheus collects.
|
||||
To configure these ports see [`IMMICH_API_METRICS_PORT` & `IMMICH_MICROSERVICES_METRICS_PORT`](../install/environment-variables/#general).
|
||||
To configure these ports see [`IMMICH_API_METRICS_PORT` & `IMMICH_MICROSERVICES_METRICS_PORT`](/docs/install/environment-variables/#general).
|
||||
:::
|
||||
|
||||
### Usage
|
||||
|
@ -49,5 +49,3 @@ The `thumbs/` folder contains both the small thumbnails displayed in the timelin
|
||||
|
||||
The storage metrics of the Immich server will track available storage at `UPLOAD_LOCATION`, so the administrator must set up some sort of monitoring to ensure the storage does not run out of space. The `profile/` folder is much smaller, usually less than 1 MB.
|
||||
:::
|
||||
|
||||
Thanks to [Jrasm91](https://github.com/immich-app/immich/discussions/2110#discussioncomment-5477767) for writing the guide.
|
||||
|
@ -159,10 +159,10 @@ Redis (Sentinel) URL example JSON before encoding:
|
||||
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | HTTP Keep-alive time in seconds | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO) | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL` | Name of the textual CLIP model to be preloaded and kept in cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Name of the visual CLIP model to be preloaded and kept in cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Name of the recognition portion of the facial recognition model to be preloaded and kept in cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Name of the detection portion of the facial recognition model to be preloaded and kept in cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL` | Comma-separated list of (textual) CLIP model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Comma-separated list of (visual) CLIP model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Comma-separated list of (recognition) facial recognition model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Comma-separated list of (detection) facial recognition model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning |
|
||||
| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning |
|
||||
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
|
||||
|
@ -55,6 +55,6 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.13.0"
|
||||
"node": "22.13.1"
|
||||
}
|
||||
}
|
||||
|
@ -73,9 +73,9 @@ function HomepageHeader() {
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p className="font-bold text-2xl md:text-5xl ">Download mobile app</p>
|
||||
<p className="font-bold text-2xl md:text-5xl ">Download the mobile app</p>
|
||||
<p className="text-lg">
|
||||
Download Immich app and start backing up your photos and videos securely to your own server
|
||||
Download the Immich app and start backing up your photos and videos securely to your own server
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1">
|
||||
|
@ -1 +1 @@
|
||||
22.13.0
|
||||
22.13.1
|
||||
|
12
e2e/package-lock.json
generated
12
e2e/package-lock.json
generated
@ -15,7 +15,7 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
@ -64,7 +64,7 @@
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||
"@typescript-eslint/parser": "^8.15.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
@ -99,7 +99,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@ -1658,9 +1658,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
|
||||
"integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
|
||||
"version": "22.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
||||
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -25,7 +25,7 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
@ -53,6 +53,6 @@
|
||||
"vitest": "^2.0.5"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.13.0"
|
||||
"node": "22.13.1"
|
||||
}
|
||||
}
|
||||
|
@ -142,6 +142,10 @@ describe('/albums', () => {
|
||||
...user1Albums[0],
|
||||
assets: [expect.objectContaining({ isFavorite: false })],
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
shared: true,
|
||||
albumUsers: expect.any(Array),
|
||||
});
|
||||
});
|
||||
|
||||
@ -299,6 +303,10 @@ describe('/albums', () => {
|
||||
...user1Albums[0],
|
||||
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
albumUsers: expect.any(Array),
|
||||
shared: true,
|
||||
});
|
||||
});
|
||||
|
||||
@ -330,6 +338,10 @@ describe('/albums', () => {
|
||||
...user1Albums[0],
|
||||
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
albumUsers: expect.any(Array),
|
||||
shared: true,
|
||||
});
|
||||
});
|
||||
|
||||
@ -344,6 +356,10 @@ describe('/albums', () => {
|
||||
assets: [],
|
||||
assetCount: 1,
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
albumUsers: expect.any(Array),
|
||||
shared: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -200,7 +200,7 @@ describe('/people', () => {
|
||||
expect(body).toMatchObject({
|
||||
id: expect.any(String),
|
||||
name: 'New Person',
|
||||
birthDate: '1990-01-01',
|
||||
birthDate: '1990-01-01T00:00:00.000Z',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -244,7 +244,7 @@ describe('/people', () => {
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ birthDate: '1990-01-01' });
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({ birthDate: '1990-01-01' });
|
||||
expect(body).toMatchObject({ birthDate: '1990-01-01T00:00:00.000Z' });
|
||||
});
|
||||
|
||||
it('should clear a date of birth', async () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:f997d3f71b7dcff3f937703c02861437f2b41a94e1ddbd1b5fa357ee99f5cce4 AS builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:adb581d8ed80edd03efd4dcad66db115b9ce8de8522b01720b9f3e6146f0884c AS builder-cpu
|
||||
|
||||
FROM builder-cpu AS builder-openvino
|
||||
|
||||
|
@ -77,29 +77,31 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
|
||||
async def preload_models(preload: PreloadModelData) -> None:
|
||||
log.info(f"Preloading models: clip:{preload.clip} facial_recognition:{preload.facial_recognition}")
|
||||
|
||||
async def load_models(model_string: str, model_type: ModelType, model_task: ModelTask) -> None:
|
||||
for model_name in model_string.split(","):
|
||||
model_name = model_name.strip()
|
||||
model = await model_cache.get(model_name, model_type, model_task)
|
||||
await load(model)
|
||||
|
||||
if preload.clip.textual is not None:
|
||||
model = await model_cache.get(preload.clip.textual, ModelType.TEXTUAL, ModelTask.SEARCH)
|
||||
await load(model)
|
||||
await load_models(preload.clip.textual, ModelType.TEXTUAL, ModelTask.SEARCH)
|
||||
|
||||
if preload.clip.visual is not None:
|
||||
model = await model_cache.get(preload.clip.visual, ModelType.VISUAL, ModelTask.SEARCH)
|
||||
await load(model)
|
||||
await load_models(preload.clip.visual, ModelType.VISUAL, ModelTask.SEARCH)
|
||||
|
||||
if preload.facial_recognition.detection is not None:
|
||||
model = await model_cache.get(
|
||||
await load_models(
|
||||
preload.facial_recognition.detection,
|
||||
ModelType.DETECTION,
|
||||
ModelTask.FACIAL_RECOGNITION,
|
||||
)
|
||||
await load(model)
|
||||
|
||||
if preload.facial_recognition.recognition is not None:
|
||||
model = await model_cache.get(
|
||||
await load_models(
|
||||
preload.facial_recognition.recognition,
|
||||
ModelType.RECOGNITION,
|
||||
ModelTask.FACIAL_RECOGNITION,
|
||||
)
|
||||
await load(model)
|
||||
|
||||
if preload.clip_fallback is not None:
|
||||
log.warning(
|
||||
|
@ -10,7 +10,7 @@ from tokenizers import Encoding, Tokenizer
|
||||
|
||||
from app.config import log
|
||||
from app.models.base import InferenceModel
|
||||
from app.models.transforms import clean_text
|
||||
from app.models.transforms import clean_text, serialize_np_array
|
||||
from app.schemas import ModelSession, ModelTask, ModelType
|
||||
|
||||
|
||||
@ -18,9 +18,9 @@ class BaseCLIPTextualEncoder(InferenceModel):
|
||||
depends = []
|
||||
identity = (ModelType.TEXTUAL, ModelTask.SEARCH)
|
||||
|
||||
def _predict(self, inputs: str, **kwargs: Any) -> NDArray[np.float32]:
|
||||
def _predict(self, inputs: str, **kwargs: Any) -> str:
|
||||
res: NDArray[np.float32] = self.session.run(None, self.tokenize(inputs))[0][0]
|
||||
return res
|
||||
return serialize_np_array(res)
|
||||
|
||||
def _load(self) -> ModelSession:
|
||||
session = super()._load()
|
||||
|
@ -10,7 +10,15 @@ from PIL import Image
|
||||
|
||||
from app.config import log
|
||||
from app.models.base import InferenceModel
|
||||
from app.models.transforms import crop_pil, decode_pil, get_pil_resampling, normalize, resize_pil, to_numpy
|
||||
from app.models.transforms import (
|
||||
crop_pil,
|
||||
decode_pil,
|
||||
get_pil_resampling,
|
||||
normalize,
|
||||
resize_pil,
|
||||
serialize_np_array,
|
||||
to_numpy,
|
||||
)
|
||||
from app.schemas import ModelSession, ModelTask, ModelType
|
||||
|
||||
|
||||
@ -18,10 +26,10 @@ class BaseCLIPVisualEncoder(InferenceModel):
|
||||
depends = []
|
||||
identity = (ModelType.VISUAL, ModelTask.SEARCH)
|
||||
|
||||
def _predict(self, inputs: Image.Image | bytes, **kwargs: Any) -> NDArray[np.float32]:
|
||||
def _predict(self, inputs: Image.Image | bytes, **kwargs: Any) -> str:
|
||||
image = decode_pil(inputs)
|
||||
res: NDArray[np.float32] = self.session.run(None, self.transform(image))[0][0]
|
||||
return res
|
||||
return serialize_np_array(res)
|
||||
|
||||
@abstractmethod
|
||||
def transform(self, image: Image.Image) -> dict[str, NDArray[np.float32]]:
|
||||
|
@ -12,7 +12,7 @@ from PIL import Image
|
||||
|
||||
from app.config import log, settings
|
||||
from app.models.base import InferenceModel
|
||||
from app.models.transforms import decode_cv2
|
||||
from app.models.transforms import decode_cv2, serialize_np_array
|
||||
from app.schemas import FaceDetectionOutput, FacialRecognitionOutput, ModelFormat, ModelSession, ModelTask, ModelType
|
||||
|
||||
|
||||
@ -61,7 +61,7 @@ class FaceRecognizer(InferenceModel):
|
||||
return [
|
||||
{
|
||||
"boundingBox": {"x1": x1, "y1": y1, "x2": x2, "y2": y2},
|
||||
"embedding": embedding,
|
||||
"embedding": serialize_np_array(embedding),
|
||||
"score": score,
|
||||
}
|
||||
for (x1, y1, x2, y2), embedding, score in zip(faces["boxes"], embeddings, faces["scores"])
|
||||
|
@ -4,6 +4,7 @@ from typing import IO
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import orjson
|
||||
from numpy.typing import NDArray
|
||||
from PIL import Image
|
||||
|
||||
@ -69,3 +70,9 @@ def clean_text(text: str, canonicalize: bool = False) -> str:
|
||||
if canonicalize:
|
||||
text = text.translate(_PUNCTUATION_TRANS).lower()
|
||||
return text
|
||||
|
||||
|
||||
# this allows the client to use the array as a string without deserializing only to serialize back to a string
|
||||
# TODO: use this in a less invasive way
|
||||
def serialize_np_array(arr: NDArray[np.float32]) -> str:
|
||||
return orjson.dumps(arr, option=orjson.OPT_SERIALIZE_NUMPY).decode()
|
||||
|
@ -80,7 +80,7 @@ class FaceDetectionOutput(TypedDict):
|
||||
|
||||
class DetectedFace(TypedDict):
|
||||
boundingBox: BoundingBox
|
||||
embedding: npt.NDArray[np.float32]
|
||||
embedding: str
|
||||
score: float
|
||||
|
||||
|
||||
|
@ -10,6 +10,7 @@ from unittest import mock
|
||||
import cv2
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
import orjson
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from fastapi.testclient import TestClient
|
||||
@ -396,11 +397,11 @@ class TestCLIP:
|
||||
mocked.run.return_value = [[self.embedding]]
|
||||
|
||||
clip_encoder = OpenClipVisualEncoder("ViT-B-32__openai", cache_dir="test_cache")
|
||||
embedding = clip_encoder.predict(pil_image)
|
||||
|
||||
assert isinstance(embedding, np.ndarray)
|
||||
assert embedding.shape[0] == clip_model_cfg["embed_dim"]
|
||||
assert embedding.dtype == np.float32
|
||||
embedding_str = clip_encoder.predict(pil_image)
|
||||
assert isinstance(embedding_str, str)
|
||||
embedding = orjson.loads(embedding_str)
|
||||
assert isinstance(embedding, list)
|
||||
assert len(embedding) == clip_model_cfg["embed_dim"]
|
||||
mocked.run.assert_called_once()
|
||||
|
||||
def test_basic_text(
|
||||
@ -418,11 +419,11 @@ class TestCLIP:
|
||||
mocker.patch("app.models.clip.textual.Tokenizer.from_file", autospec=True)
|
||||
|
||||
clip_encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir="test_cache")
|
||||
embedding = clip_encoder.predict("test search query")
|
||||
|
||||
assert isinstance(embedding, np.ndarray)
|
||||
assert embedding.shape[0] == clip_model_cfg["embed_dim"]
|
||||
assert embedding.dtype == np.float32
|
||||
embedding_str = clip_encoder.predict("test search query")
|
||||
assert isinstance(embedding_str, str)
|
||||
embedding = orjson.loads(embedding_str)
|
||||
assert isinstance(embedding, list)
|
||||
assert len(embedding) == clip_model_cfg["embed_dim"]
|
||||
mocked.run.assert_called_once()
|
||||
|
||||
def test_openclip_tokenizer(
|
||||
@ -558,8 +559,11 @@ class TestFaceRecognition:
|
||||
assert isinstance(face.get("boundingBox"), dict)
|
||||
assert set(face["boundingBox"]) == {"x1", "y1", "x2", "y2"}
|
||||
assert all(isinstance(val, np.float32) for val in face["boundingBox"].values())
|
||||
assert isinstance(face.get("embedding"), np.ndarray)
|
||||
assert face["embedding"].shape[0] == 512
|
||||
embedding_str = face.get("embedding")
|
||||
assert isinstance(embedding_str, str)
|
||||
embedding = orjson.loads(embedding_str)
|
||||
assert isinstance(embedding, list)
|
||||
assert len(embedding) == 512
|
||||
assert isinstance(face.get("score", None), np.float32)
|
||||
|
||||
rec_model.get_feat.assert_called_once()
|
||||
@ -930,8 +934,10 @@ class TestPredictionEndpoints:
|
||||
actual = response.json()
|
||||
assert response.status_code == 200
|
||||
assert isinstance(actual, dict)
|
||||
assert isinstance(actual.get("clip", None), list)
|
||||
assert np.allclose(expected, actual["clip"])
|
||||
embedding = actual.get("clip", None)
|
||||
assert isinstance(embedding, str)
|
||||
parsed_embedding = orjson.loads(embedding)
|
||||
assert np.allclose(expected, parsed_embedding)
|
||||
|
||||
def test_clip_text_endpoint(self, responses: dict[str, Any], deployed_app: TestClient) -> None:
|
||||
expected = responses["clip"]["text"]
|
||||
@ -951,8 +957,10 @@ class TestPredictionEndpoints:
|
||||
actual = response.json()
|
||||
assert response.status_code == 200
|
||||
assert isinstance(actual, dict)
|
||||
assert isinstance(actual.get("clip", None), list)
|
||||
assert np.allclose(expected, actual["clip"])
|
||||
embedding = actual.get("clip", None)
|
||||
assert isinstance(embedding, str)
|
||||
parsed_embedding = orjson.loads(embedding)
|
||||
assert np.allclose(expected, parsed_embedding)
|
||||
|
||||
def test_face_endpoint(self, pil_image: Image.Image, responses: dict[str, Any], deployed_app: TestClient) -> None:
|
||||
byte_image = BytesIO()
|
||||
@ -983,5 +991,8 @@ class TestPredictionEndpoints:
|
||||
|
||||
for expected_face, actual_face in zip(responses["facial-recognition"], actual["facial-recognition"]):
|
||||
assert expected_face["boundingBox"] == actual_face["boundingBox"]
|
||||
assert np.allclose(expected_face["embedding"], actual_face["embedding"])
|
||||
embedding = actual_face.get("embedding", None)
|
||||
assert isinstance(embedding, str)
|
||||
parsed_embedding = orjson.loads(embedding)
|
||||
assert np.allclose(expected_face["embedding"], parsed_embedding)
|
||||
assert np.allclose(expected_face["score"], actual_face["score"])
|
||||
|
64
machine-learning/poetry.lock
generated
64
machine-learning/poetry.lock
generated
@ -1625,13 +1625,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "locust"
|
||||
version = "2.32.5"
|
||||
version = "2.32.6"
|
||||
description = "Developer-friendly load testing framework"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "locust-2.32.5-py3-none-any.whl", hash = "sha256:2f49509868ffc2e368be40921c6825f92147c84e997206760a85dab3058f5efb"},
|
||||
{file = "locust-2.32.5.tar.gz", hash = "sha256:ea7bc1e8ce2520e8893c471b4b0a56a4f53b01b4b618adfe8d2c8ab2728b5821"},
|
||||
{file = "locust-2.32.6-py3-none-any.whl", hash = "sha256:d5c0e4f73134415d250087034431cf3ea42ca695d3dee7f10812287cacb6c4ef"},
|
||||
{file = "locust-2.32.6.tar.gz", hash = "sha256:6600cc308398e724764aacc56ccddf6cfcd0127c4c92dedd5c4979dd37ef5b15"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1649,8 +1649,8 @@ psutil = ">=5.9.1"
|
||||
pywin32 = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
pyzmq = ">=25.0.0"
|
||||
requests = [
|
||||
{version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""},
|
||||
{version = ">=2.32.2", markers = "python_full_version > \"3.11.0\""},
|
||||
{version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""},
|
||||
]
|
||||
setuptools = ">=70.0.0"
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
@ -2165,26 +2165,26 @@ sympy = "*"
|
||||
|
||||
[[package]]
|
||||
name = "opencv-python-headless"
|
||||
version = "4.10.0.84"
|
||||
version = "4.11.0.86"
|
||||
description = "Wrapper package for OpenCV python bindings."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "opencv-python-headless-4.10.0.84.tar.gz", hash = "sha256:f2017c6101d7c2ef8d7bc3b414c37ff7f54d64413a1847d89970b6b7069b4e1a"},
|
||||
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a4f4bcb07d8f8a7704d9c8564c224c8b064c63f430e95b61ac0bffaa374d330e"},
|
||||
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:5ae454ebac0eb0a0b932e3406370aaf4212e6a3fdb5038cc86c7aea15a6851da"},
|
||||
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46071015ff9ab40fccd8a163da0ee14ce9846349f06c6c8c0f2870856ffa45db"},
|
||||
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:377d08a7e48a1405b5e84afcbe4798464ce7ee17081c1c23619c8b398ff18295"},
|
||||
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:9092404b65458ed87ce932f613ffbb1106ed2c843577501e5768912360fc50ec"},
|
||||
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:afcf28bd1209dd58810d33defb622b325d3cbe49dcd7a43a902982c33e5fad05"},
|
||||
{file = "opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798"},
|
||||
{file = "opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca"},
|
||||
{file = "opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81"},
|
||||
{file = "opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb"},
|
||||
{file = "opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b"},
|
||||
{file = "opencv_python_headless-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b"},
|
||||
{file = "opencv_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
numpy = [
|
||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||
{version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
|
||||
{version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""},
|
||||
{version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""},
|
||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3137,29 +3137,29 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743"},
|
||||
{file = "ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f"},
|
||||
{file = "ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f"},
|
||||
{file = "ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72"},
|
||||
{file = "ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19"},
|
||||
{file = "ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7"},
|
||||
{file = "ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17"},
|
||||
{file = "ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347"},
|
||||
{file = "ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00"},
|
||||
{file = "ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a"},
|
||||
{file = "ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145"},
|
||||
{file = "ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5"},
|
||||
{file = "ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6"},
|
||||
{file = "ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -14,6 +14,7 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
|
||||
final List<AssetPathEntity> assetPathEntities =
|
||||
await PhotoManager.getAssetPathList(
|
||||
hasAll: true,
|
||||
filterOption: FilterOptionGroup(containsPathModified: true),
|
||||
);
|
||||
return assetPathEntities.map(_toAlbum).toList();
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
22.13.0
|
||||
22.13.1
|
||||
|
8
open-api/typescript-sdk/package-lock.json
generated
8
open-api/typescript-sdk/package-lock.json
generated
@ -12,7 +12,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@ -22,9 +22,9 @@
|
||||
"integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
|
||||
"integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
|
||||
"version": "22.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
||||
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -19,7 +19,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"repository": {
|
||||
@ -28,6 +28,6 @@
|
||||
"directory": "open-api/typescript-sdk"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.13.0"
|
||||
"node": "22.13.1"
|
||||
}
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
22.13.0
|
||||
22.13.1
|
||||
|
65
server/package-lock.json
generated
65
server/package-lock.json
generated
@ -86,7 +86,7 @@
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/picomatch": "^3.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
@ -104,6 +104,7 @@
|
||||
"globals": "^15.9.0",
|
||||
"kysely-codegen": "^0.16.3",
|
||||
"mock-fs": "^5.2.0",
|
||||
"node-addon-api": "^8.3.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.0.2",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
@ -5128,9 +5129,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
|
||||
"integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
|
||||
"version": "22.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
||||
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
@ -6436,6 +6437,12 @@
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt/node_modules/node-addon-api": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
|
||||
@ -11128,10 +11135,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
|
||||
"license": "MIT"
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.0.tgz",
|
||||
"integrity": "sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-emoji": {
|
||||
"version": "1.11.0",
|
||||
@ -16205,44 +16216,6 @@
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/zip-stream/node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/zip-stream/node_modules/readable-stream": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
|
||||
"integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,7 +112,7 @@
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/picomatch": "^3.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
@ -130,6 +130,7 @@
|
||||
"globals": "^15.9.0",
|
||||
"kysely-codegen": "^0.16.3",
|
||||
"mock-fs": "^5.2.0",
|
||||
"node-addon-api": "^8.3.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.0.2",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
@ -144,6 +145,6 @@
|
||||
"vitest": "^2.0.5"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.13.0"
|
||||
"node": "22.13.1"
|
||||
}
|
||||
}
|
||||
|
@ -5,13 +5,13 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IConfigRepository } from 'src/types';
|
||||
import { getAssetFiles } from 'src/utils/asset.util';
|
||||
import { getConfig } from 'src/utils/config';
|
||||
|
||||
|
@ -100,6 +100,7 @@ export const DummyValue = {
|
||||
DATE: new Date(),
|
||||
TIME_BUCKET: '2024-01-01T00:00:00.000Z',
|
||||
BOOLEAN: true,
|
||||
VECTOR: '[1, 2, 3]',
|
||||
};
|
||||
|
||||
export const GENERATE_SQL_KEY = 'generate-sql-key';
|
||||
|
@ -29,7 +29,7 @@ export class AddUsersDto {
|
||||
albumUsers!: AlbumUserAddDto[];
|
||||
}
|
||||
|
||||
class AlbumUserCreateDto {
|
||||
export class AlbumUserCreateDto {
|
||||
@ValidateUUID()
|
||||
userId!: string;
|
||||
|
||||
|
@ -248,7 +248,7 @@ export function hasPeopleCte(db: Kysely<DB>, personIds: string[]) {
|
||||
.select('assetId')
|
||||
.where('personId', '=', anyUuid(personIds!))
|
||||
.groupBy('assetId')
|
||||
.having((eb) => eb.fn.count('personId'), '>=', personIds.length),
|
||||
.having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -11,10 +11,6 @@ export class FaceSearchEntity {
|
||||
faceId!: string;
|
||||
|
||||
@Index('face_index', { synchronize: false })
|
||||
@Column({
|
||||
type: 'float4',
|
||||
array: true,
|
||||
transformer: { from: JSON.parse, to: (v) => `[${v}]` },
|
||||
})
|
||||
embedding!: number[];
|
||||
@Column({ type: 'float4', array: true })
|
||||
embedding!: string;
|
||||
}
|
||||
|
@ -11,6 +11,6 @@ export class SmartSearchEntity {
|
||||
assetId!: string;
|
||||
|
||||
@Index('clip_index', { synchronize: false })
|
||||
@Column({ type: 'float4', array: true, transformer: { from: JSON.parse, to: (v) => v } })
|
||||
embedding!: number[];
|
||||
@Column({ type: 'float4', array: true })
|
||||
embedding!: string;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { StorageFolder, SystemMetadataKey } from 'src/enum';
|
||||
import { Column, DeepPartial, Entity, PrimaryColumn } from 'typeorm';
|
||||
import { DeepPartial } from 'src/types';
|
||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('system_metadata')
|
||||
export class SystemMetadataEntity<T extends keyof SystemMetadata = SystemMetadataKey> {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { UserAvatarColor, UserMetadataKey } from 'src/enum';
|
||||
import { DeepPartial } from 'src/types';
|
||||
import { HumanReadableSize } from 'src/utils/bytes';
|
||||
import { Column, DeepPartial, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('user_metadata')
|
||||
export class UserMetadataEntity<T extends keyof UserMetadata = UserMetadataKey> {
|
||||
|
@ -1,53 +0,0 @@
|
||||
import { AlbumUserRole } from 'src/enum';
|
||||
|
||||
export const IAccessRepository = 'IAccessRepository';
|
||||
|
||||
export interface IAccessRepository {
|
||||
activity: {
|
||||
checkOwnerAccess(userId: string, activityIds: Set<string>): Promise<Set<string>>;
|
||||
checkAlbumOwnerAccess(userId: string, activityIds: Set<string>): Promise<Set<string>>;
|
||||
checkCreateAccess(userId: string, albumIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
asset: {
|
||||
checkOwnerAccess(userId: string, assetIds: Set<string>): Promise<Set<string>>;
|
||||
checkAlbumAccess(userId: string, assetIds: Set<string>): Promise<Set<string>>;
|
||||
checkPartnerAccess(userId: string, assetIds: Set<string>): Promise<Set<string>>;
|
||||
checkSharedLinkAccess(sharedLinkId: string, assetIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
authDevice: {
|
||||
checkOwnerAccess(userId: string, deviceIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
album: {
|
||||
checkOwnerAccess(userId: string, albumIds: Set<string>): Promise<Set<string>>;
|
||||
checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole): Promise<Set<string>>;
|
||||
checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
timeline: {
|
||||
checkPartnerAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
memory: {
|
||||
checkOwnerAccess(userId: string, memoryIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
person: {
|
||||
checkFaceOwnerAccess(userId: string, assetFaceId: Set<string>): Promise<Set<string>>;
|
||||
checkOwnerAccess(userId: string, personIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
partner: {
|
||||
checkUpdateAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
stack: {
|
||||
checkOwnerAccess(userId: string, stackIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
tag: {
|
||||
checkOwnerAccess(userId: string, tagIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
import { Insertable, Updateable } from 'kysely';
|
||||
import { Albums } from 'src/db';
|
||||
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { IBulkAsset } from 'src/utils/asset.util';
|
||||
|
||||
@ -15,7 +18,7 @@ export interface AlbumInfoOptions {
|
||||
}
|
||||
|
||||
export interface IAlbumRepository extends IBulkAsset {
|
||||
getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null>;
|
||||
getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | undefined>;
|
||||
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
|
||||
removeAsset(assetId: string): Promise<void>;
|
||||
getMetadataForIds(ids: string[]): Promise<AlbumAssetCount[]>;
|
||||
@ -25,8 +28,8 @@ export interface IAlbumRepository extends IBulkAsset {
|
||||
restoreAll(userId: string): Promise<void>;
|
||||
softDeleteAll(userId: string): Promise<void>;
|
||||
deleteAll(userId: string): Promise<void>;
|
||||
create(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
|
||||
update(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
|
||||
create(album: Insertable<Albums>, assetIds: string[], albumUsers: AlbumUserCreateDto[]): Promise<AlbumEntity>;
|
||||
update(id: string, album: Updateable<Albums>): Promise<AlbumEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
updateThumbnails(): Promise<number | undefined>;
|
||||
}
|
||||
|
@ -1,19 +0,0 @@
|
||||
import { Insertable } from 'kysely';
|
||||
import { ApiKeys } from 'src/db';
|
||||
import { APIKeyEntity } from 'src/entities/api-key.entity';
|
||||
import { AuthApiKey } from 'src/types';
|
||||
|
||||
export const IKeyRepository = 'IKeyRepository';
|
||||
|
||||
export interface IKeyRepository {
|
||||
create(dto: Insertable<ApiKeys>): Promise<APIKeyEntity>;
|
||||
update(userId: string, id: string, dto: Partial<APIKeyEntity>): Promise<APIKeyEntity>;
|
||||
delete(userId: string, id: string): Promise<void>;
|
||||
/**
|
||||
* Includes the hashed `key` for verification
|
||||
* @param id
|
||||
*/
|
||||
getKey(hashedToken: string): Promise<AuthApiKey | undefined>;
|
||||
getById(userId: string, id: string): Promise<APIKeyEntity | null>;
|
||||
getByUserId(userId: string): Promise<APIKeyEntity[]>;
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { DatabaseAction, EntityType } from 'src/enum';
|
||||
|
||||
export const IAuditRepository = 'IAuditRepository';
|
||||
|
||||
export interface AuditSearch {
|
||||
action?: DatabaseAction;
|
||||
entityType?: EntityType;
|
||||
userIds: string[];
|
||||
}
|
||||
|
||||
export interface IAuditRepository {
|
||||
getAfter(since: Date, options: AuditSearch): Promise<string[]>;
|
||||
removeBefore(before: Date): Promise<void>;
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
import { RegisterQueueOptions } from '@nestjs/bullmq';
|
||||
import { QueueOptions } from 'bullmq';
|
||||
import { RedisOptions } from 'ioredis';
|
||||
import { KyselyConfig } from 'kysely';
|
||||
import { ClsModuleOptions } from 'nestjs-cls';
|
||||
import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
|
||||
import { ImmichEnvironment, ImmichTelemetry, ImmichWorker, LogLevel } from 'src/enum';
|
||||
import { DatabaseConnectionParams, VectorExtension } from 'src/interfaces/database.interface';
|
||||
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js';
|
||||
|
||||
export const IConfigRepository = 'IConfigRepository';
|
||||
|
||||
export interface EnvData {
|
||||
host?: string;
|
||||
port: number;
|
||||
environment: ImmichEnvironment;
|
||||
configFile?: string;
|
||||
logLevel?: LogLevel;
|
||||
|
||||
buildMetadata: {
|
||||
build?: string;
|
||||
buildUrl?: string;
|
||||
buildImage?: string;
|
||||
buildImageUrl?: string;
|
||||
repository?: string;
|
||||
repositoryUrl?: string;
|
||||
sourceRef?: string;
|
||||
sourceCommit?: string;
|
||||
sourceUrl?: string;
|
||||
thirdPartySourceUrl?: string;
|
||||
thirdPartyBugFeatureUrl?: string;
|
||||
thirdPartyDocumentationUrl?: string;
|
||||
thirdPartySupportUrl?: string;
|
||||
};
|
||||
|
||||
bull: {
|
||||
config: QueueOptions;
|
||||
queues: RegisterQueueOptions[];
|
||||
};
|
||||
|
||||
cls: {
|
||||
config: ClsModuleOptions;
|
||||
};
|
||||
|
||||
database: {
|
||||
config: { typeorm: PostgresConnectionOptions & DatabaseConnectionParams; kysely: KyselyConfig };
|
||||
skipMigrations: boolean;
|
||||
vectorExtension: VectorExtension;
|
||||
};
|
||||
|
||||
licensePublicKey: {
|
||||
client: string;
|
||||
server: string;
|
||||
};
|
||||
|
||||
network: {
|
||||
trustedProxies: string[];
|
||||
};
|
||||
|
||||
otel: OpenTelemetryModuleOptions;
|
||||
|
||||
resourcePaths: {
|
||||
lockFile: string;
|
||||
geodata: {
|
||||
dateFile: string;
|
||||
admin1: string;
|
||||
admin2: string;
|
||||
cities500: string;
|
||||
naturalEarthCountriesPath: string;
|
||||
};
|
||||
web: {
|
||||
root: string;
|
||||
indexHtml: string;
|
||||
};
|
||||
};
|
||||
|
||||
redis: RedisOptions;
|
||||
|
||||
telemetry: {
|
||||
apiPort: number;
|
||||
microservicesPort: number;
|
||||
metrics: Set<ImmichTelemetry>;
|
||||
};
|
||||
|
||||
storage: {
|
||||
ignoreMountCheckErrors: boolean;
|
||||
};
|
||||
|
||||
workers: ImmichWorker[];
|
||||
|
||||
noColor: boolean;
|
||||
nodeVersion?: string;
|
||||
}
|
||||
|
||||
export interface IConfigRepository {
|
||||
getEnv(): EnvData;
|
||||
getWorker(): ImmichWorker | undefined;
|
||||
}
|
@ -28,10 +28,10 @@ export type FaceDetectionOptions = ModelOptions & { minScore: number };
|
||||
|
||||
type VisualResponse = { imageHeight: number; imageWidth: number };
|
||||
export type ClipVisualRequest = { [ModelTask.SEARCH]: { [ModelType.VISUAL]: ModelOptions } };
|
||||
export type ClipVisualResponse = { [ModelTask.SEARCH]: number[] } & VisualResponse;
|
||||
export type ClipVisualResponse = { [ModelTask.SEARCH]: string } & VisualResponse;
|
||||
|
||||
export type ClipTextualRequest = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: ModelOptions } };
|
||||
export type ClipTextualResponse = { [ModelTask.SEARCH]: number[] };
|
||||
export type ClipTextualResponse = { [ModelTask.SEARCH]: string };
|
||||
|
||||
export type FacialRecognitionRequest = {
|
||||
[ModelTask.FACIAL_RECOGNITION]: {
|
||||
@ -42,7 +42,7 @@ export type FacialRecognitionRequest = {
|
||||
|
||||
export interface Face {
|
||||
boundingBox: BoundingBox;
|
||||
embedding: number[];
|
||||
embedding: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ export type DetectedFaces = { faces: Face[] } & VisualResponse;
|
||||
export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | FacialRecognitionRequest;
|
||||
|
||||
export interface IMachineLearningRepository {
|
||||
encodeImage(urls: string[], imagePath: string, config: ModelOptions): Promise<number[]>;
|
||||
encodeText(urls: string[], text: string, config: ModelOptions): Promise<number[]>;
|
||||
encodeImage(urls: string[], imagePath: string, config: ModelOptions): Promise<string>;
|
||||
encodeText(urls: string[], text: string, config: ModelOptions): Promise<string>;
|
||||
detectFaces(urls: string[], imagePath: string, config: FaceDetectionOptions): Promise<DetectedFaces>;
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { Insertable, Updateable } from 'kysely';
|
||||
import { AssetFaces, FaceSearch, Person } from 'src/db';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { FaceSearchEntity } from 'src/entities/face-search.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { SourceType } from 'src/enum';
|
||||
import { Paginated, PaginationOptions } from 'src/utils/pagination';
|
||||
import { FindManyOptions, FindOptionsRelations, FindOptionsSelect } from 'typeorm';
|
||||
import { FindOptionsRelations } from 'typeorm';
|
||||
|
||||
export const IPersonRepository = 'IPersonRepository';
|
||||
|
||||
@ -48,29 +49,31 @@ export interface DeleteFacesOptions {
|
||||
|
||||
export type UnassignFacesOptions = DeleteFacesOptions;
|
||||
|
||||
export type SelectFaceOptions = Partial<{ [K in keyof AssetFaceEntity]: boolean }>;
|
||||
|
||||
export interface IPersonRepository {
|
||||
getAll(pagination: PaginationOptions, options?: FindManyOptions<PersonEntity>): Paginated<PersonEntity>;
|
||||
getAll(options?: Partial<PersonEntity>): AsyncIterableIterator<PersonEntity>;
|
||||
getAllForUser(pagination: PaginationOptions, userId: string, options: PersonSearchOptions): Paginated<PersonEntity>;
|
||||
getAllWithoutFaces(): Promise<PersonEntity[]>;
|
||||
getById(personId: string): Promise<PersonEntity | null>;
|
||||
getByName(userId: string, personName: string, options: PersonNameSearchOptions): Promise<PersonEntity[]>;
|
||||
getDistinctNames(userId: string, options: PersonNameSearchOptions): Promise<PersonNameResponse[]>;
|
||||
|
||||
create(person: Partial<PersonEntity>): Promise<PersonEntity>;
|
||||
createAll(people: Partial<PersonEntity>[]): Promise<string[]>;
|
||||
create(person: Insertable<Person>): Promise<PersonEntity>;
|
||||
createAll(people: Insertable<Person>[]): Promise<string[]>;
|
||||
delete(entities: PersonEntity[]): Promise<void>;
|
||||
deleteFaces(options: DeleteFacesOptions): Promise<void>;
|
||||
refreshFaces(
|
||||
facesToAdd: Partial<AssetFaceEntity>[],
|
||||
facesToAdd: Insertable<AssetFaces>[],
|
||||
faceIdsToRemove: string[],
|
||||
embeddingsToAdd?: FaceSearchEntity[],
|
||||
embeddingsToAdd?: Insertable<FaceSearch>[],
|
||||
): Promise<void>;
|
||||
getAllFaces(pagination: PaginationOptions, options?: FindManyOptions<AssetFaceEntity>): Paginated<AssetFaceEntity>;
|
||||
getAllFaces(options?: Partial<AssetFaceEntity>): AsyncIterableIterator<AssetFaceEntity>;
|
||||
getFaceById(id: string): Promise<AssetFaceEntity>;
|
||||
getFaceByIdWithAssets(
|
||||
id: string,
|
||||
relations?: FindOptionsRelations<AssetFaceEntity>,
|
||||
select?: FindOptionsSelect<AssetFaceEntity>,
|
||||
select?: SelectFaceOptions,
|
||||
): Promise<AssetFaceEntity | null>;
|
||||
getFaces(assetId: string): Promise<AssetFaceEntity[]>;
|
||||
getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]>;
|
||||
@ -80,7 +83,7 @@ export interface IPersonRepository {
|
||||
getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
|
||||
reassignFaces(data: UpdateFacesData): Promise<number>;
|
||||
unassignFaces(options: UnassignFacesOptions): Promise<void>;
|
||||
update(person: Partial<PersonEntity>): Promise<PersonEntity>;
|
||||
updateAll(people: Partial<PersonEntity>[]): Promise<void>;
|
||||
update(person: Updateable<Person> & { id: string }): Promise<PersonEntity>;
|
||||
updateAll(people: Insertable<Person>[]): Promise<void>;
|
||||
getLatestFaceDate(): Promise<string | undefined>;
|
||||
}
|
||||
|
@ -104,7 +104,7 @@ export interface SearchExifOptions {
|
||||
}
|
||||
|
||||
export interface SearchEmbeddingOptions {
|
||||
embedding: number[];
|
||||
embedding: string;
|
||||
userIds: string[];
|
||||
}
|
||||
|
||||
@ -152,7 +152,7 @@ export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
|
||||
|
||||
export interface AssetDuplicateSearch {
|
||||
assetId: string;
|
||||
embedding: number[];
|
||||
embedding: string;
|
||||
maxDistance: number;
|
||||
type: AssetType;
|
||||
userIds: string[];
|
||||
@ -192,7 +192,7 @@ export interface ISearchRepository {
|
||||
searchDuplicates(options: AssetDuplicateSearch): Promise<AssetDuplicateResult[]>;
|
||||
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
|
||||
searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]>;
|
||||
upsert(assetId: string, embedding: number[]): Promise<void>;
|
||||
upsert(assetId: string, embedding: string): Promise<void>;
|
||||
searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>;
|
||||
getAssetsByCity(userIds: string[]): Promise<AssetEntity[]>;
|
||||
deleteAllSearchEmbeddings(): Promise<void>;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Updateable } from 'kysely';
|
||||
import { StackEntity } from 'src/entities/stack.entity';
|
||||
|
||||
export const IStackRepository = 'IStackRepository';
|
||||
@ -10,8 +11,8 @@ export interface StackSearch {
|
||||
export interface IStackRepository {
|
||||
search(query: StackSearch): Promise<StackEntity[]>;
|
||||
create(stack: { ownerId: string; assetIds: string[] }): Promise<StackEntity>;
|
||||
update(stack: Pick<StackEntity, 'id'> & Partial<StackEntity>): Promise<StackEntity>;
|
||||
update(id: string, entity: Updateable<StackEntity>): Promise<StackEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
deleteAll(ids: string[]): Promise<void>;
|
||||
getById(id: string): Promise<StackEntity | null>;
|
||||
getById(id: string): Promise<StackEntity | undefined>;
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
|
||||
export const IViewRepository = 'IViewRepository';
|
||||
|
||||
export interface IViewRepository {
|
||||
getAssetsByOriginalPath(userId: string, partialPath: string): Promise<AssetEntity[]>;
|
||||
getUniqueOriginalPaths(userId: string): Promise<string[]>;
|
||||
}
|
@ -3,7 +3,7 @@ import { IoAdapter } from '@nestjs/platform-socket.io';
|
||||
import { createAdapter } from '@socket.io/redis-adapter';
|
||||
import { Redis } from 'ioredis';
|
||||
import { ServerOptions } from 'socket.io';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
|
||||
export class WebSocketAdapter extends IoAdapter {
|
||||
constructor(private app: INestApplicationContext) {
|
||||
@ -11,7 +11,7 @@ export class WebSocketAdapter extends IoAdapter {
|
||||
}
|
||||
|
||||
createIOServer(port: number, options?: ServerOptions): any {
|
||||
const { redis } = this.app.get<IConfigRepository>(IConfigRepository).getEnv();
|
||||
const { redis } = this.app.get(ConfigRepository).getEnv();
|
||||
const server = super.createIOServer(port, options);
|
||||
const pubClient = new Redis(redis);
|
||||
const subClient = pubClient.duplicate();
|
||||
|
@ -1,460 +1,490 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- AlbumRepository.getById
|
||||
SELECT DISTINCT
|
||||
"distinctAlias"."AlbumEntity_id" AS "ids_AlbumEntity_id"
|
||||
FROM
|
||||
select
|
||||
"albums".*,
|
||||
(
|
||||
SELECT
|
||||
"AlbumEntity"."id" AS "AlbumEntity_id",
|
||||
"AlbumEntity"."ownerId" AS "AlbumEntity_ownerId",
|
||||
"AlbumEntity"."albumName" AS "AlbumEntity_albumName",
|
||||
"AlbumEntity"."description" AS "AlbumEntity_description",
|
||||
"AlbumEntity"."createdAt" AS "AlbumEntity_createdAt",
|
||||
"AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt",
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."userId" AS "AlbumEntity__AlbumEntity_sharedLinks_userId",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."key" AS "AlbumEntity__AlbumEntity_sharedLinks_key",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."type" AS "AlbumEntity__AlbumEntity_sharedLinks_type",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."createdAt" AS "AlbumEntity__AlbumEntity_sharedLinks_createdAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."expiresAt" AS "AlbumEntity__AlbumEntity_sharedLinks_expiresAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."allowUpload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowUpload",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."allowDownload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowDownload",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."showExif" AS "AlbumEntity__AlbumEntity_sharedLinks_showExif",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId"
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
|
||||
AND (
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId"
|
||||
AND (
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id"
|
||||
WHERE
|
||||
((("AlbumEntity"."id" = $1)))
|
||||
AND ("AlbumEntity"."deletedAt" IS NULL)
|
||||
) "distinctAlias"
|
||||
ORDER BY
|
||||
"AlbumEntity_id" ASC
|
||||
LIMIT
|
||||
1
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "albums"."ownerId"
|
||||
) as obj
|
||||
) as "owner",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"album_users".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "album_users"."usersId"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"albums_shared_users_users" as "album_users"
|
||||
where
|
||||
"album_users"."albumsId" = "albums"."id"
|
||||
) as agg
|
||||
) as "albumUsers",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"shared_links"
|
||||
where
|
||||
"shared_links"."albumId" = "albums"."id"
|
||||
) as agg
|
||||
) as "sharedLinks"
|
||||
from
|
||||
"albums"
|
||||
where
|
||||
"albums"."id" = $1
|
||||
and "albums"."deletedAt" is null
|
||||
|
||||
-- AlbumRepository.getByAssetId
|
||||
SELECT
|
||||
"AlbumEntity"."id" AS "AlbumEntity_id",
|
||||
"AlbumEntity"."ownerId" AS "AlbumEntity_ownerId",
|
||||
"AlbumEntity"."albumName" AS "AlbumEntity_albumName",
|
||||
"AlbumEntity"."description" AS "AlbumEntity_description",
|
||||
"AlbumEntity"."createdAt" AS "AlbumEntity_createdAt",
|
||||
"AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt",
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt"
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
|
||||
AND (
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId"
|
||||
AND (
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "albums_assets_assets" "AlbumEntity_AlbumEntity__AlbumEntity_assets" ON "AlbumEntity_AlbumEntity__AlbumEntity_assets"."albumsId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "assets" "AlbumEntity__AlbumEntity_assets" ON "AlbumEntity__AlbumEntity_assets"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_assets"."assetsId"
|
||||
AND (
|
||||
"AlbumEntity__AlbumEntity_assets"."deletedAt" IS NULL
|
||||
)
|
||||
WHERE
|
||||
select
|
||||
"albums".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "albums"."ownerId"
|
||||
) as obj
|
||||
) as "owner",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"album_users".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "album_users"."usersId"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"albums_shared_users_users" as "album_users"
|
||||
where
|
||||
"album_users"."albumsId" = "albums"."id"
|
||||
) as agg
|
||||
) as "albumUsers"
|
||||
from
|
||||
"albums"
|
||||
left join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id"
|
||||
left join "albums_shared_users_users" as "album_users" on "album_users"."albumsId" = "albums"."id"
|
||||
where
|
||||
(
|
||||
(
|
||||
(
|
||||
(
|
||||
("AlbumEntity"."ownerId" = $1)
|
||||
AND ((("AlbumEntity__AlbumEntity_assets"."id" = $2)))
|
||||
)
|
||||
)
|
||||
OR (
|
||||
(
|
||||
(
|
||||
(
|
||||
(
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" = $3
|
||||
)
|
||||
)
|
||||
)
|
||||
AND ((("AlbumEntity__AlbumEntity_assets"."id" = $4)))
|
||||
)
|
||||
)
|
||||
"albums"."ownerId" = $1
|
||||
and "album_assets"."assetsId" = $2
|
||||
)
|
||||
or (
|
||||
"album_users"."usersId" = $3
|
||||
and "album_assets"."assetsId" = $4
|
||||
)
|
||||
)
|
||||
AND ("AlbumEntity"."deletedAt" IS NULL)
|
||||
ORDER BY
|
||||
"AlbumEntity"."createdAt" DESC
|
||||
and "albums"."deletedAt" is null
|
||||
order by
|
||||
"albums"."createdAt" desc,
|
||||
"albums"."createdAt" desc
|
||||
|
||||
-- AlbumRepository.getMetadataForIds
|
||||
SELECT
|
||||
"album"."id" AS "album_id",
|
||||
MIN("assets"."fileCreatedAt") AS "start_date",
|
||||
MAX("assets"."fileCreatedAt") AS "end_date",
|
||||
COUNT("assets"."id") AS "asset_count"
|
||||
FROM
|
||||
"albums" "album"
|
||||
LEFT JOIN "albums_assets_assets" "album_assets" ON "album_assets"."albumsId" = "album"."id"
|
||||
LEFT JOIN "assets" "assets" ON "assets"."id" = "album_assets"."assetsId"
|
||||
AND "assets"."deletedAt" IS NULL
|
||||
WHERE
|
||||
("album"."id" IN ($1))
|
||||
AND ("album"."deletedAt" IS NULL)
|
||||
GROUP BY
|
||||
"album"."id"
|
||||
select
|
||||
"albums"."id",
|
||||
min("assets"."fileCreatedAt") as "startDate",
|
||||
max("assets"."fileCreatedAt") as "endDate",
|
||||
count("assets"."id") as "assetCount"
|
||||
from
|
||||
"albums"
|
||||
left join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id"
|
||||
left join "assets" on "assets"."id" = "album_assets"."assetsId"
|
||||
where
|
||||
"albums"."id" in ($1)
|
||||
group by
|
||||
"albums"."id"
|
||||
|
||||
-- AlbumRepository.getOwned
|
||||
SELECT
|
||||
"AlbumEntity"."id" AS "AlbumEntity_id",
|
||||
"AlbumEntity"."ownerId" AS "AlbumEntity_ownerId",
|
||||
"AlbumEntity"."albumName" AS "AlbumEntity_albumName",
|
||||
"AlbumEntity"."description" AS "AlbumEntity_description",
|
||||
"AlbumEntity"."createdAt" AS "AlbumEntity_createdAt",
|
||||
"AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt",
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."userId" AS "AlbumEntity__AlbumEntity_sharedLinks_userId",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."key" AS "AlbumEntity__AlbumEntity_sharedLinks_key",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."type" AS "AlbumEntity__AlbumEntity_sharedLinks_type",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."createdAt" AS "AlbumEntity__AlbumEntity_sharedLinks_createdAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."expiresAt" AS "AlbumEntity__AlbumEntity_sharedLinks_expiresAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."allowUpload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowUpload",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."allowDownload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowDownload",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."showExif" AS "AlbumEntity__AlbumEntity_sharedLinks_showExif",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt"
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId"
|
||||
AND (
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
|
||||
AND (
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
|
||||
)
|
||||
WHERE
|
||||
((("AlbumEntity"."ownerId" = $1)))
|
||||
AND ("AlbumEntity"."deletedAt" IS NULL)
|
||||
ORDER BY
|
||||
"AlbumEntity"."createdAt" DESC
|
||||
select
|
||||
"albums".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "albums"."ownerId"
|
||||
) as obj
|
||||
) as "owner",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"album_users".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "album_users"."usersId"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"albums_shared_users_users" as "album_users"
|
||||
where
|
||||
"album_users"."albumsId" = "albums"."id"
|
||||
) as agg
|
||||
) as "albumUsers",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"shared_links"
|
||||
where
|
||||
"shared_links"."albumId" = "albums"."id"
|
||||
) as agg
|
||||
) as "sharedLinks"
|
||||
from
|
||||
"albums"
|
||||
where
|
||||
"albums"."ownerId" = $1
|
||||
and "albums"."deletedAt" is null
|
||||
order by
|
||||
"albums"."createdAt" desc
|
||||
|
||||
-- AlbumRepository.getShared
|
||||
SELECT
|
||||
"AlbumEntity"."id" AS "AlbumEntity_id",
|
||||
"AlbumEntity"."ownerId" AS "AlbumEntity_ownerId",
|
||||
"AlbumEntity"."albumName" AS "AlbumEntity_albumName",
|
||||
"AlbumEntity"."description" AS "AlbumEntity_description",
|
||||
"AlbumEntity"."createdAt" AS "AlbumEntity_createdAt",
|
||||
"AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt",
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."userId" AS "AlbumEntity__AlbumEntity_sharedLinks_userId",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."key" AS "AlbumEntity__AlbumEntity_sharedLinks_key",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."type" AS "AlbumEntity__AlbumEntity_sharedLinks_type",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."createdAt" AS "AlbumEntity__AlbumEntity_sharedLinks_createdAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."expiresAt" AS "AlbumEntity__AlbumEntity_sharedLinks_expiresAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."allowUpload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowUpload",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."allowDownload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowDownload",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."showExif" AS "AlbumEntity__AlbumEntity_sharedLinks_showExif",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt"
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId"
|
||||
AND (
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
|
||||
AND (
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
|
||||
)
|
||||
WHERE
|
||||
select distinct
|
||||
on ("albums"."createdAt") "albums".*,
|
||||
(
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
(
|
||||
select
|
||||
"album_users".*,
|
||||
(
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" = $1
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
OR (
|
||||
(
|
||||
(
|
||||
(
|
||||
(
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."userId" = $2
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
OR (
|
||||
(
|
||||
("AlbumEntity"."ownerId" = $3)
|
||||
AND (
|
||||
(
|
||||
(
|
||||
NOT (
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" IS NULL
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "album_users"."usersId"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"albums_shared_users_users" as "album_users"
|
||||
where
|
||||
"album_users"."albumsId" = "albums"."id"
|
||||
) as agg
|
||||
) as "albumUsers",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "albums"."ownerId"
|
||||
) as obj
|
||||
) as "owner",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"shared_links"
|
||||
where
|
||||
"shared_links"."albumId" = "albums"."id"
|
||||
) as agg
|
||||
) as "sharedLinks"
|
||||
from
|
||||
"albums"
|
||||
left join "albums_shared_users_users" as "shared_albums" on "shared_albums"."albumsId" = "albums"."id"
|
||||
left join "shared_links" on "shared_links"."albumId" = "albums"."id"
|
||||
where
|
||||
(
|
||||
"shared_albums"."usersId" = $1
|
||||
or "shared_links"."userId" = $2
|
||||
or (
|
||||
"albums"."ownerId" = $3
|
||||
and "shared_albums"."usersId" is not null
|
||||
)
|
||||
)
|
||||
AND ("AlbumEntity"."deletedAt" IS NULL)
|
||||
ORDER BY
|
||||
"AlbumEntity"."createdAt" DESC
|
||||
and "albums"."deletedAt" is null
|
||||
order by
|
||||
"albums"."createdAt" desc
|
||||
|
||||
-- AlbumRepository.getNotShared
|
||||
SELECT
|
||||
"AlbumEntity"."id" AS "AlbumEntity_id",
|
||||
"AlbumEntity"."ownerId" AS "AlbumEntity_ownerId",
|
||||
"AlbumEntity"."albumName" AS "AlbumEntity_albumName",
|
||||
"AlbumEntity"."description" AS "AlbumEntity_description",
|
||||
"AlbumEntity"."createdAt" AS "AlbumEntity_createdAt",
|
||||
"AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt",
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."userId" AS "AlbumEntity__AlbumEntity_sharedLinks_userId",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."key" AS "AlbumEntity__AlbumEntity_sharedLinks_key",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."type" AS "AlbumEntity__AlbumEntity_sharedLinks_type",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."createdAt" AS "AlbumEntity__AlbumEntity_sharedLinks_createdAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."expiresAt" AS "AlbumEntity__AlbumEntity_sharedLinks_expiresAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."allowUpload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowUpload",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."allowDownload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowDownload",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."showExif" AS "AlbumEntity__AlbumEntity_sharedLinks_showExif",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt"
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
|
||||
AND (
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
|
||||
)
|
||||
WHERE
|
||||
select distinct
|
||||
on ("albums"."createdAt") "albums".*,
|
||||
(
|
||||
(
|
||||
("AlbumEntity"."ownerId" = $1)
|
||||
AND (
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"album_users".*,
|
||||
(
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" IS NULL
|
||||
)
|
||||
)
|
||||
)
|
||||
AND (
|
||||
(
|
||||
(
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."id" IS NULL
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
AND ("AlbumEntity"."deletedAt" IS NULL)
|
||||
ORDER BY
|
||||
"AlbumEntity"."createdAt" DESC
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "album_users"."usersId"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"albums_shared_users_users" as "album_users"
|
||||
where
|
||||
"album_users"."albumsId" = "albums"."id"
|
||||
) as agg
|
||||
) as "albumUsers",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "albums"."ownerId"
|
||||
) as obj
|
||||
) as "owner",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"shared_links"
|
||||
where
|
||||
"shared_links"."albumId" = "albums"."id"
|
||||
) as agg
|
||||
) as "sharedLinks"
|
||||
from
|
||||
"albums"
|
||||
left join "albums_shared_users_users" as "shared_albums" on "shared_albums"."albumsId" = "albums"."id"
|
||||
left join "shared_links" on "shared_links"."albumId" = "albums"."id"
|
||||
where
|
||||
"albums"."ownerId" = $1
|
||||
and "shared_albums"."usersId" is null
|
||||
and "shared_links"."userId" is null
|
||||
and "albums"."deletedAt" is null
|
||||
order by
|
||||
"albums"."createdAt" desc
|
||||
|
||||
-- AlbumRepository.getAssetIds
|
||||
SELECT
|
||||
"albums_assets"."assetsId" AS "assetId"
|
||||
FROM
|
||||
"albums_assets_assets" "albums_assets"
|
||||
WHERE
|
||||
"albums_assets"."albumsId" = $1
|
||||
AND "albums_assets"."assetsId" IN ($2)
|
||||
select
|
||||
*
|
||||
from
|
||||
"albums_assets_assets"
|
||||
where
|
||||
"albums_assets_assets"."albumsId" = $1
|
||||
and "albums_assets_assets"."assetsId" in ($2)
|
||||
|
@ -1,342 +1,252 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- PersonRepository.reassignFaces
|
||||
UPDATE "asset_faces"
|
||||
SET
|
||||
update "asset_faces"
|
||||
set
|
||||
"personId" = $1
|
||||
WHERE
|
||||
"personId" = $2
|
||||
where
|
||||
"asset_faces"."personId" = $2
|
||||
|
||||
-- PersonRepository.getAllForUser
|
||||
SELECT
|
||||
"person"."id" AS "person_id",
|
||||
"person"."createdAt" AS "person_createdAt",
|
||||
"person"."updatedAt" AS "person_updatedAt",
|
||||
"person"."ownerId" AS "person_ownerId",
|
||||
"person"."name" AS "person_name",
|
||||
"person"."birthDate" AS "person_birthDate",
|
||||
"person"."thumbnailPath" AS "person_thumbnailPath",
|
||||
"person"."faceAssetId" AS "person_faceAssetId",
|
||||
"person"."isHidden" AS "person_isHidden"
|
||||
FROM
|
||||
"person" "person"
|
||||
INNER JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
|
||||
INNER JOIN "assets" "asset" ON "asset"."id" = "face"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"person"."ownerId" = $1
|
||||
AND "asset"."isArchived" = false
|
||||
AND "person"."isHidden" = false
|
||||
GROUP BY
|
||||
"person"."id"
|
||||
HAVING
|
||||
"person"."name" != ''
|
||||
OR COUNT("face"."assetId") >= $2
|
||||
ORDER BY
|
||||
"person"."isHidden" ASC,
|
||||
NULLIF("person"."name", '') IS NULL ASC,
|
||||
COUNT("face"."assetId") DESC,
|
||||
NULLIF("person"."name", '') ASC NULLS LAST,
|
||||
"person"."createdAt" ASC
|
||||
LIMIT
|
||||
11
|
||||
OFFSET
|
||||
10
|
||||
-- PersonRepository.unassignFaces
|
||||
update "asset_faces"
|
||||
set
|
||||
"personId" = $1
|
||||
where
|
||||
"asset_faces"."sourceType" = $2
|
||||
VACUUM
|
||||
ANALYZE asset_faces,
|
||||
face_search,
|
||||
person
|
||||
REINDEX TABLE asset_faces
|
||||
REINDEX TABLE person
|
||||
|
||||
-- PersonRepository.delete
|
||||
delete from "person"
|
||||
where
|
||||
"person"."id" in ($1)
|
||||
|
||||
-- PersonRepository.deleteFaces
|
||||
delete from "asset_faces"
|
||||
where
|
||||
"asset_faces"."sourceType" = $1
|
||||
VACUUM
|
||||
ANALYZE asset_faces,
|
||||
face_search,
|
||||
person
|
||||
REINDEX TABLE asset_faces
|
||||
REINDEX TABLE person
|
||||
|
||||
-- PersonRepository.getAllWithoutFaces
|
||||
SELECT
|
||||
"person"."id" AS "person_id",
|
||||
"person"."createdAt" AS "person_createdAt",
|
||||
"person"."updatedAt" AS "person_updatedAt",
|
||||
"person"."ownerId" AS "person_ownerId",
|
||||
"person"."name" AS "person_name",
|
||||
"person"."birthDate" AS "person_birthDate",
|
||||
"person"."thumbnailPath" AS "person_thumbnailPath",
|
||||
"person"."faceAssetId" AS "person_faceAssetId",
|
||||
"person"."isHidden" AS "person_isHidden"
|
||||
FROM
|
||||
"person" "person"
|
||||
LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
|
||||
GROUP BY
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"person"
|
||||
left join "asset_faces" on "asset_faces"."personId" = "person"."id"
|
||||
group by
|
||||
"person"."id"
|
||||
HAVING
|
||||
COUNT("face"."assetId") = 0
|
||||
having
|
||||
count("asset_faces"."assetId") = $1
|
||||
|
||||
-- PersonRepository.getFaces
|
||||
SELECT
|
||||
"AssetFaceEntity"."id" AS "AssetFaceEntity_id",
|
||||
"AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
|
||||
"AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
|
||||
"AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
|
||||
"AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
|
||||
"AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
|
||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||
"AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_person_ownerId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."name" AS "AssetFaceEntity__AssetFaceEntity_person_name",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden"
|
||||
FROM
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
|
||||
WHERE
|
||||
(("AssetFaceEntity"."assetId" = $1))
|
||||
ORDER BY
|
||||
"AssetFaceEntity"."boundingBoxX1" ASC
|
||||
select
|
||||
"asset_faces".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"person"."id" = "asset_faces"."personId"
|
||||
) as obj
|
||||
) as "person"
|
||||
from
|
||||
"asset_faces"
|
||||
where
|
||||
"asset_faces"."assetId" = $1
|
||||
order by
|
||||
"asset_faces"."boundingBoxX1" asc
|
||||
|
||||
-- PersonRepository.getFaceById
|
||||
SELECT DISTINCT
|
||||
"distinctAlias"."AssetFaceEntity_id" AS "ids_AssetFaceEntity_id"
|
||||
FROM
|
||||
select
|
||||
"asset_faces".*,
|
||||
(
|
||||
SELECT
|
||||
"AssetFaceEntity"."id" AS "AssetFaceEntity_id",
|
||||
"AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
|
||||
"AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
|
||||
"AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
|
||||
"AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
|
||||
"AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
|
||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||
"AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_person_ownerId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."name" AS "AssetFaceEntity__AssetFaceEntity_person_name",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden"
|
||||
FROM
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
|
||||
WHERE
|
||||
(("AssetFaceEntity"."id" = $1))
|
||||
) "distinctAlias"
|
||||
ORDER BY
|
||||
"AssetFaceEntity_id" ASC
|
||||
LIMIT
|
||||
1
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"person"."id" = "asset_faces"."personId"
|
||||
) as obj
|
||||
) as "person"
|
||||
from
|
||||
"asset_faces"
|
||||
where
|
||||
"asset_faces"."id" = $1
|
||||
|
||||
-- PersonRepository.getFaceByIdWithAssets
|
||||
SELECT DISTINCT
|
||||
"distinctAlias"."AssetFaceEntity_id" AS "ids_AssetFaceEntity_id"
|
||||
FROM
|
||||
select
|
||||
"asset_faces".*,
|
||||
(
|
||||
SELECT
|
||||
"AssetFaceEntity"."id" AS "AssetFaceEntity_id",
|
||||
"AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
|
||||
"AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
|
||||
"AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
|
||||
"AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
|
||||
"AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
|
||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||
"AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_person_ownerId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."name" AS "AssetFaceEntity__AssetFaceEntity_person_name",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."status" AS "AssetFaceEntity__AssetFaceEntity_asset_status",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_updatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_deletedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."fileCreatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileCreatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."localDateTime" AS "AssetFaceEntity__AssetFaceEntity_asset_localDateTime",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."fileModifiedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileModifiedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isFavorite" AS "AssetFaceEntity__AssetFaceEntity_asset_isFavorite",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isArchived" AS "AssetFaceEntity__AssetFaceEntity_asset_isArchived",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isExternal" AS "AssetFaceEntity__AssetFaceEntity_asset_isExternal",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isOffline" AS "AssetFaceEntity__AssetFaceEntity_asset_isOffline",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."checksum" AS "AssetFaceEntity__AssetFaceEntity_asset_checksum",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."duration" AS "AssetFaceEntity__AssetFaceEntity_asset_duration",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isVisible" AS "AssetFaceEntity__AssetFaceEntity_asset_isVisible",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."livePhotoVideoId" AS "AssetFaceEntity__AssetFaceEntity_asset_livePhotoVideoId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."originalFileName" AS "AssetFaceEntity__AssetFaceEntity_asset_originalFileName",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."sidecarPath" AS "AssetFaceEntity__AssetFaceEntity_asset_sidecarPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."stackId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."duplicateId" AS "AssetFaceEntity__AssetFaceEntity_asset_duplicateId"
|
||||
FROM
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
|
||||
LEFT JOIN "assets" "AssetFaceEntity__AssetFaceEntity_asset" ON "AssetFaceEntity__AssetFaceEntity_asset"."id" = "AssetFaceEntity"."assetId"
|
||||
AND (
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" IS NULL
|
||||
)
|
||||
WHERE
|
||||
(("AssetFaceEntity"."id" = $1))
|
||||
) "distinctAlias"
|
||||
ORDER BY
|
||||
"AssetFaceEntity_id" ASC
|
||||
LIMIT
|
||||
1
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"person"."id" = "asset_faces"."personId"
|
||||
) as obj
|
||||
) as "person",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"assets".*
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"assets"."id" = "asset_faces"."assetId"
|
||||
) as obj
|
||||
) as "asset"
|
||||
from
|
||||
"asset_faces"
|
||||
where
|
||||
"asset_faces"."id" = $1
|
||||
|
||||
-- PersonRepository.reassignFace
|
||||
UPDATE "asset_faces"
|
||||
SET
|
||||
update "asset_faces"
|
||||
set
|
||||
"personId" = $1
|
||||
WHERE
|
||||
"id" = $2
|
||||
where
|
||||
"asset_faces"."id" = $2
|
||||
|
||||
-- PersonRepository.getByName
|
||||
SELECT
|
||||
"person"."id" AS "person_id",
|
||||
"person"."createdAt" AS "person_createdAt",
|
||||
"person"."updatedAt" AS "person_updatedAt",
|
||||
"person"."ownerId" AS "person_ownerId",
|
||||
"person"."name" AS "person_name",
|
||||
"person"."birthDate" AS "person_birthDate",
|
||||
"person"."thumbnailPath" AS "person_thumbnailPath",
|
||||
"person"."faceAssetId" AS "person_faceAssetId",
|
||||
"person"."isHidden" AS "person_isHidden"
|
||||
FROM
|
||||
"person" "person"
|
||||
WHERE
|
||||
"person"."ownerId" = $1
|
||||
AND (
|
||||
LOWER("person"."name") LIKE $2
|
||||
OR LOWER("person"."name") LIKE $3
|
||||
)
|
||||
LIMIT
|
||||
1000
|
||||
|
||||
-- PersonRepository.getDistinctNames
|
||||
SELECT DISTINCT
|
||||
ON (lower("person"."name")) "person"."id" AS "person_id",
|
||||
"person"."name" AS "person_name"
|
||||
FROM
|
||||
"person" "person"
|
||||
WHERE
|
||||
"person"."ownerId" = $1
|
||||
AND "person"."name" != ''
|
||||
|
||||
-- PersonRepository.getStatistics
|
||||
SELECT
|
||||
COUNT(DISTINCT ("asset"."id")) AS "count"
|
||||
FROM
|
||||
"asset_faces" "face"
|
||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "face"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"face"."personId" = $1
|
||||
AND "asset"."isArchived" = false
|
||||
AND "asset"."deletedAt" IS NULL
|
||||
AND "asset"."livePhotoVideoId" IS NULL
|
||||
|
||||
-- PersonRepository.getNumberOfPeople
|
||||
SELECT
|
||||
COUNT(DISTINCT ("person"."id")) AS "total",
|
||||
COUNT(DISTINCT ("person"."id")) FILTER (
|
||||
WHERE
|
||||
"person"."isHidden" = true
|
||||
) AS "hidden"
|
||||
FROM
|
||||
"person" "person"
|
||||
INNER JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
|
||||
INNER JOIN "assets" "asset" ON "asset"."id" = "face"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"person"."ownerId" = $1
|
||||
AND "asset"."isArchived" = false
|
||||
|
||||
-- PersonRepository.getFacesByIds
|
||||
SELECT
|
||||
"AssetFaceEntity"."id" AS "AssetFaceEntity_id",
|
||||
"AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
|
||||
"AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
|
||||
"AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
|
||||
"AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
|
||||
"AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
|
||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||
"AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."status" AS "AssetFaceEntity__AssetFaceEntity_asset_status",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_updatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_deletedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."fileCreatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileCreatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."localDateTime" AS "AssetFaceEntity__AssetFaceEntity_asset_localDateTime",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."fileModifiedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileModifiedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isFavorite" AS "AssetFaceEntity__AssetFaceEntity_asset_isFavorite",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isArchived" AS "AssetFaceEntity__AssetFaceEntity_asset_isArchived",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isExternal" AS "AssetFaceEntity__AssetFaceEntity_asset_isExternal",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isOffline" AS "AssetFaceEntity__AssetFaceEntity_asset_isOffline",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."checksum" AS "AssetFaceEntity__AssetFaceEntity_asset_checksum",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."duration" AS "AssetFaceEntity__AssetFaceEntity_asset_duration",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isVisible" AS "AssetFaceEntity__AssetFaceEntity_asset_isVisible",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."livePhotoVideoId" AS "AssetFaceEntity__AssetFaceEntity_asset_livePhotoVideoId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."originalFileName" AS "AssetFaceEntity__AssetFaceEntity_asset_originalFileName",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."sidecarPath" AS "AssetFaceEntity__AssetFaceEntity_asset_sidecarPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."stackId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."duplicateId" AS "AssetFaceEntity__AssetFaceEntity_asset_duplicateId"
|
||||
FROM
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
LEFT JOIN "assets" "AssetFaceEntity__AssetFaceEntity_asset" ON "AssetFaceEntity__AssetFaceEntity_asset"."id" = "AssetFaceEntity"."assetId"
|
||||
WHERE
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"person"
|
||||
where
|
||||
(
|
||||
(
|
||||
(
|
||||
("AssetFaceEntity"."assetId" = $1)
|
||||
AND ("AssetFaceEntity"."personId" = $2)
|
||||
)
|
||||
"person"."ownerId" = $1
|
||||
and (
|
||||
lower("person"."name") like $2
|
||||
or lower("person"."name") like $3
|
||||
)
|
||||
)
|
||||
limit
|
||||
$4
|
||||
|
||||
-- PersonRepository.getDistinctNames
|
||||
select distinct
|
||||
on (lower("person"."name")) "person"."id",
|
||||
"person"."name"
|
||||
from
|
||||
"person"
|
||||
where
|
||||
(
|
||||
"person"."ownerId" = $1
|
||||
and "person"."name" != $2
|
||||
)
|
||||
|
||||
-- PersonRepository.getStatistics
|
||||
select
|
||||
count(distinct ("assets"."id")) as "count"
|
||||
from
|
||||
"asset_faces"
|
||||
left join "assets" on "assets"."id" = "asset_faces"."assetId"
|
||||
and "asset_faces"."personId" = $1
|
||||
and "assets"."isArchived" = $2
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."livePhotoVideoId" is null
|
||||
|
||||
-- PersonRepository.getNumberOfPeople
|
||||
select
|
||||
count(distinct ("person"."id")) as "total",
|
||||
count(distinct ("person"."id")) filter (
|
||||
where
|
||||
"person"."isHidden" = $1
|
||||
) as "hidden"
|
||||
from
|
||||
"person"
|
||||
inner join "asset_faces" on "asset_faces"."personId" = "person"."id"
|
||||
inner join "assets" on "assets"."id" = "asset_faces"."assetId"
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."isArchived" = $2
|
||||
where
|
||||
"person"."ownerId" = $3
|
||||
|
||||
-- PersonRepository.refreshFaces
|
||||
with
|
||||
"added_embeddings" as (
|
||||
insert into
|
||||
"face_search" ("faceId", "embedding")
|
||||
values
|
||||
($1, $2)
|
||||
)
|
||||
select
|
||||
from
|
||||
(
|
||||
select
|
||||
1
|
||||
) as "dummy"
|
||||
|
||||
-- PersonRepository.getFacesByIds
|
||||
select
|
||||
"asset_faces".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"assets".*
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"assets"."id" = "asset_faces"."assetId"
|
||||
) as obj
|
||||
) as "asset",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"person"."id" = "asset_faces"."personId"
|
||||
) as obj
|
||||
) as "person"
|
||||
from
|
||||
"asset_faces"
|
||||
where
|
||||
"asset_faces"."assetId" in ($1)
|
||||
and "asset_faces"."personId" in ($2)
|
||||
|
||||
-- PersonRepository.getRandomFace
|
||||
SELECT
|
||||
"AssetFaceEntity"."id" AS "AssetFaceEntity_id",
|
||||
"AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
|
||||
"AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
|
||||
"AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
|
||||
"AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
|
||||
"AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
|
||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||
"AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType"
|
||||
FROM
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
WHERE
|
||||
(("AssetFaceEntity"."personId" = $1))
|
||||
LIMIT
|
||||
1
|
||||
select
|
||||
"asset_faces".*
|
||||
from
|
||||
"asset_faces"
|
||||
where
|
||||
"asset_faces"."personId" = $1
|
||||
|
||||
-- PersonRepository.getLatestFaceDate
|
||||
SELECT
|
||||
MAX("jobStatus"."facesRecognizedAt")::text AS "latestDate"
|
||||
FROM
|
||||
"asset_job_status" "jobStatus"
|
||||
select
|
||||
max("asset_job_status"."facesRecognizedAt")::text as "latestDate"
|
||||
from
|
||||
"asset_job_status"
|
||||
|
@ -76,7 +76,7 @@ where
|
||||
and "assets"."isArchived" = $5
|
||||
and "assets"."deletedAt" is null
|
||||
order by
|
||||
smart_search.embedding <= > $6::vector
|
||||
smart_search.embedding <= > $6
|
||||
limit
|
||||
$7
|
||||
offset
|
||||
@ -88,7 +88,7 @@ with
|
||||
select
|
||||
"assets"."id" as "assetId",
|
||||
"assets"."duplicateId",
|
||||
smart_search.embedding <= > $1::vector as "distance"
|
||||
smart_search.embedding <= > $1 as "distance"
|
||||
from
|
||||
"assets"
|
||||
inner join "smart_search" on "assets"."id" = "smart_search"."assetId"
|
||||
@ -99,7 +99,7 @@ with
|
||||
and "assets"."type" = $4
|
||||
and "assets"."id" != $5::uuid
|
||||
order by
|
||||
smart_search.embedding <= > $6::vector
|
||||
smart_search.embedding <= > $6
|
||||
limit
|
||||
$7
|
||||
)
|
||||
@ -116,7 +116,7 @@ with
|
||||
select
|
||||
"asset_faces"."id",
|
||||
"asset_faces"."personId",
|
||||
face_search.embedding <= > $1::vector as "distance"
|
||||
face_search.embedding <= > $1 as "distance"
|
||||
from
|
||||
"asset_faces"
|
||||
inner join "assets" on "assets"."id" = "asset_faces"."assetId"
|
||||
@ -125,7 +125,7 @@ with
|
||||
"assets"."ownerId" = any ($2::uuid [])
|
||||
and "assets"."deletedAt" is null
|
||||
order by
|
||||
face_search.embedding <= > $3::vector
|
||||
face_search.embedding <= > $3
|
||||
limit
|
||||
$4
|
||||
)
|
||||
|
@ -1,257 +1,95 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- StackRepository.search
|
||||
SELECT
|
||||
"StackEntity"."id" AS "StackEntity_id",
|
||||
"StackEntity"."ownerId" AS "StackEntity_ownerId",
|
||||
"StackEntity"."primaryAssetId" AS "StackEntity_primaryAssetId",
|
||||
"StackEntity__StackEntity_assets"."id" AS "StackEntity__StackEntity_assets_id",
|
||||
"StackEntity__StackEntity_assets"."deviceAssetId" AS "StackEntity__StackEntity_assets_deviceAssetId",
|
||||
"StackEntity__StackEntity_assets"."ownerId" AS "StackEntity__StackEntity_assets_ownerId",
|
||||
"StackEntity__StackEntity_assets"."libraryId" AS "StackEntity__StackEntity_assets_libraryId",
|
||||
"StackEntity__StackEntity_assets"."deviceId" AS "StackEntity__StackEntity_assets_deviceId",
|
||||
"StackEntity__StackEntity_assets"."type" AS "StackEntity__StackEntity_assets_type",
|
||||
"StackEntity__StackEntity_assets"."status" AS "StackEntity__StackEntity_assets_status",
|
||||
"StackEntity__StackEntity_assets"."originalPath" AS "StackEntity__StackEntity_assets_originalPath",
|
||||
"StackEntity__StackEntity_assets"."thumbhash" AS "StackEntity__StackEntity_assets_thumbhash",
|
||||
"StackEntity__StackEntity_assets"."encodedVideoPath" AS "StackEntity__StackEntity_assets_encodedVideoPath",
|
||||
"StackEntity__StackEntity_assets"."createdAt" AS "StackEntity__StackEntity_assets_createdAt",
|
||||
"StackEntity__StackEntity_assets"."updatedAt" AS "StackEntity__StackEntity_assets_updatedAt",
|
||||
"StackEntity__StackEntity_assets"."deletedAt" AS "StackEntity__StackEntity_assets_deletedAt",
|
||||
"StackEntity__StackEntity_assets"."fileCreatedAt" AS "StackEntity__StackEntity_assets_fileCreatedAt",
|
||||
"StackEntity__StackEntity_assets"."localDateTime" AS "StackEntity__StackEntity_assets_localDateTime",
|
||||
"StackEntity__StackEntity_assets"."fileModifiedAt" AS "StackEntity__StackEntity_assets_fileModifiedAt",
|
||||
"StackEntity__StackEntity_assets"."isFavorite" AS "StackEntity__StackEntity_assets_isFavorite",
|
||||
"StackEntity__StackEntity_assets"."isArchived" AS "StackEntity__StackEntity_assets_isArchived",
|
||||
"StackEntity__StackEntity_assets"."isExternal" AS "StackEntity__StackEntity_assets_isExternal",
|
||||
"StackEntity__StackEntity_assets"."isOffline" AS "StackEntity__StackEntity_assets_isOffline",
|
||||
"StackEntity__StackEntity_assets"."checksum" AS "StackEntity__StackEntity_assets_checksum",
|
||||
"StackEntity__StackEntity_assets"."duration" AS "StackEntity__StackEntity_assets_duration",
|
||||
"StackEntity__StackEntity_assets"."isVisible" AS "StackEntity__StackEntity_assets_isVisible",
|
||||
"StackEntity__StackEntity_assets"."livePhotoVideoId" AS "StackEntity__StackEntity_assets_livePhotoVideoId",
|
||||
"StackEntity__StackEntity_assets"."originalFileName" AS "StackEntity__StackEntity_assets_originalFileName",
|
||||
"StackEntity__StackEntity_assets"."sidecarPath" AS "StackEntity__StackEntity_assets_sidecarPath",
|
||||
"StackEntity__StackEntity_assets"."stackId" AS "StackEntity__StackEntity_assets_stackId",
|
||||
"StackEntity__StackEntity_assets"."duplicateId" AS "StackEntity__StackEntity_assets_duplicateId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_assetId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."description" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_description",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageWidth" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageWidth",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageHeight" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageHeight",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fileSizeInByte" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fileSizeInByte",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."orientation" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_orientation",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."dateTimeOriginal" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_dateTimeOriginal",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."modifyDate" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_modifyDate",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."timeZone" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_timeZone",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."latitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_latitude",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."longitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_longitude",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."projectionType" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_projectionType",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."city" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_city",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."livePhotoCID" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_livePhotoCID",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."autoStackId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_autoStackId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."state" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_state",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."country" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_country",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."make" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_make",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."model" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_model",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."lensModel" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_lensModel",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fNumber" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fNumber",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."focalLength" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_focalLength",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."iso" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_iso",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exposureTime" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exposureTime",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."profileDescription" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_profileDescription",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."colorspace" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_colorspace",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."bitsPerSample" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_bitsPerSample",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."rating" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_rating",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fps" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fps"
|
||||
FROM
|
||||
"asset_stack" "StackEntity"
|
||||
LEFT JOIN "assets" "StackEntity__StackEntity_assets" ON "StackEntity__StackEntity_assets"."stackId" = "StackEntity"."id"
|
||||
AND (
|
||||
"StackEntity__StackEntity_assets"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "exif" "01db479afeb88793eed8e0d1dde6ccfccf1698b9" ON "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" = "StackEntity__StackEntity_assets"."id"
|
||||
WHERE
|
||||
(("StackEntity"."ownerId" = $1))
|
||||
select
|
||||
"asset_stack".*,
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"assets"."deletedAt" is null
|
||||
and "assets"."stackId" = "asset_stack"."id"
|
||||
) as agg
|
||||
) as "assets"
|
||||
from
|
||||
"asset_stack"
|
||||
where
|
||||
"asset_stack"."ownerId" = $1
|
||||
|
||||
-- StackRepository.delete
|
||||
SELECT DISTINCT
|
||||
"distinctAlias"."StackEntity_id" AS "ids_StackEntity_id",
|
||||
"distinctAlias"."StackEntity__StackEntity_assets_fileCreatedAt"
|
||||
FROM
|
||||
select
|
||||
*,
|
||||
(
|
||||
SELECT
|
||||
"StackEntity"."id" AS "StackEntity_id",
|
||||
"StackEntity"."ownerId" AS "StackEntity_ownerId",
|
||||
"StackEntity"."primaryAssetId" AS "StackEntity_primaryAssetId",
|
||||
"StackEntity__StackEntity_assets"."id" AS "StackEntity__StackEntity_assets_id",
|
||||
"StackEntity__StackEntity_assets"."deviceAssetId" AS "StackEntity__StackEntity_assets_deviceAssetId",
|
||||
"StackEntity__StackEntity_assets"."ownerId" AS "StackEntity__StackEntity_assets_ownerId",
|
||||
"StackEntity__StackEntity_assets"."libraryId" AS "StackEntity__StackEntity_assets_libraryId",
|
||||
"StackEntity__StackEntity_assets"."deviceId" AS "StackEntity__StackEntity_assets_deviceId",
|
||||
"StackEntity__StackEntity_assets"."type" AS "StackEntity__StackEntity_assets_type",
|
||||
"StackEntity__StackEntity_assets"."status" AS "StackEntity__StackEntity_assets_status",
|
||||
"StackEntity__StackEntity_assets"."originalPath" AS "StackEntity__StackEntity_assets_originalPath",
|
||||
"StackEntity__StackEntity_assets"."thumbhash" AS "StackEntity__StackEntity_assets_thumbhash",
|
||||
"StackEntity__StackEntity_assets"."encodedVideoPath" AS "StackEntity__StackEntity_assets_encodedVideoPath",
|
||||
"StackEntity__StackEntity_assets"."createdAt" AS "StackEntity__StackEntity_assets_createdAt",
|
||||
"StackEntity__StackEntity_assets"."updatedAt" AS "StackEntity__StackEntity_assets_updatedAt",
|
||||
"StackEntity__StackEntity_assets"."deletedAt" AS "StackEntity__StackEntity_assets_deletedAt",
|
||||
"StackEntity__StackEntity_assets"."fileCreatedAt" AS "StackEntity__StackEntity_assets_fileCreatedAt",
|
||||
"StackEntity__StackEntity_assets"."localDateTime" AS "StackEntity__StackEntity_assets_localDateTime",
|
||||
"StackEntity__StackEntity_assets"."fileModifiedAt" AS "StackEntity__StackEntity_assets_fileModifiedAt",
|
||||
"StackEntity__StackEntity_assets"."isFavorite" AS "StackEntity__StackEntity_assets_isFavorite",
|
||||
"StackEntity__StackEntity_assets"."isArchived" AS "StackEntity__StackEntity_assets_isArchived",
|
||||
"StackEntity__StackEntity_assets"."isExternal" AS "StackEntity__StackEntity_assets_isExternal",
|
||||
"StackEntity__StackEntity_assets"."isOffline" AS "StackEntity__StackEntity_assets_isOffline",
|
||||
"StackEntity__StackEntity_assets"."checksum" AS "StackEntity__StackEntity_assets_checksum",
|
||||
"StackEntity__StackEntity_assets"."duration" AS "StackEntity__StackEntity_assets_duration",
|
||||
"StackEntity__StackEntity_assets"."isVisible" AS "StackEntity__StackEntity_assets_isVisible",
|
||||
"StackEntity__StackEntity_assets"."livePhotoVideoId" AS "StackEntity__StackEntity_assets_livePhotoVideoId",
|
||||
"StackEntity__StackEntity_assets"."originalFileName" AS "StackEntity__StackEntity_assets_originalFileName",
|
||||
"StackEntity__StackEntity_assets"."sidecarPath" AS "StackEntity__StackEntity_assets_sidecarPath",
|
||||
"StackEntity__StackEntity_assets"."stackId" AS "StackEntity__StackEntity_assets_stackId",
|
||||
"StackEntity__StackEntity_assets"."duplicateId" AS "StackEntity__StackEntity_assets_duplicateId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_assetId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."description" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_description",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageWidth" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageWidth",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageHeight" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageHeight",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fileSizeInByte" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fileSizeInByte",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."orientation" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_orientation",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."dateTimeOriginal" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_dateTimeOriginal",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."modifyDate" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_modifyDate",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."timeZone" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_timeZone",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."latitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_latitude",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."longitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_longitude",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."projectionType" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_projectionType",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."city" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_city",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."livePhotoCID" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_livePhotoCID",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."autoStackId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_autoStackId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."state" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_state",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."country" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_country",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."make" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_make",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."model" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_model",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."lensModel" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_lensModel",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fNumber" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fNumber",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."focalLength" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_focalLength",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."iso" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_iso",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exposureTime" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exposureTime",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."profileDescription" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_profileDescription",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."colorspace" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_colorspace",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."bitsPerSample" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_bitsPerSample",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."rating" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_rating",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fps" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fps",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."id" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_id",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."value" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_value",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."createdAt" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_createdAt",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."updatedAt" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_updatedAt",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."color" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_color",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."parentId" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_parentId",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."userId" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_userId"
|
||||
FROM
|
||||
"asset_stack" "StackEntity"
|
||||
LEFT JOIN "assets" "StackEntity__StackEntity_assets" ON "StackEntity__StackEntity_assets"."stackId" = "StackEntity"."id"
|
||||
AND (
|
||||
"StackEntity__StackEntity_assets"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "exif" "01db479afeb88793eed8e0d1dde6ccfccf1698b9" ON "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" = "StackEntity__StackEntity_assets"."id"
|
||||
LEFT JOIN "tag_asset" "4f1c9474d4596aede2814ee2eb938eecf7a93b95" ON "4f1c9474d4596aede2814ee2eb938eecf7a93b95"."assetsId" = "StackEntity__StackEntity_assets"."id"
|
||||
LEFT JOIN "tags" "3e3064f11b97177a1e1ce3c77ecf32850343aba1" ON "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."id" = "4f1c9474d4596aede2814ee2eb938eecf7a93b95"."tagsId"
|
||||
WHERE
|
||||
(("StackEntity"."id" = $1))
|
||||
) "distinctAlias"
|
||||
ORDER BY
|
||||
"distinctAlias"."StackEntity__StackEntity_assets_fileCreatedAt" ASC,
|
||||
"StackEntity_id" ASC
|
||||
LIMIT
|
||||
1
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*,
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"tags".*
|
||||
from
|
||||
"tags"
|
||||
inner join "tag_asset" on "tags"."id" = "tag_asset"."tagsId"
|
||||
where
|
||||
"tag_asset"."assetsId" = "assets"."id"
|
||||
) as agg
|
||||
) as "tags"
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"assets"."deletedAt" is null
|
||||
and "assets"."stackId" = "asset_stack"."id"
|
||||
) as agg
|
||||
) as "assets"
|
||||
from
|
||||
"asset_stack"
|
||||
where
|
||||
"id" = $1::uuid
|
||||
|
||||
-- StackRepository.getById
|
||||
SELECT DISTINCT
|
||||
"distinctAlias"."StackEntity_id" AS "ids_StackEntity_id",
|
||||
"distinctAlias"."StackEntity__StackEntity_assets_fileCreatedAt"
|
||||
FROM
|
||||
select
|
||||
*,
|
||||
(
|
||||
SELECT
|
||||
"StackEntity"."id" AS "StackEntity_id",
|
||||
"StackEntity"."ownerId" AS "StackEntity_ownerId",
|
||||
"StackEntity"."primaryAssetId" AS "StackEntity_primaryAssetId",
|
||||
"StackEntity__StackEntity_assets"."id" AS "StackEntity__StackEntity_assets_id",
|
||||
"StackEntity__StackEntity_assets"."deviceAssetId" AS "StackEntity__StackEntity_assets_deviceAssetId",
|
||||
"StackEntity__StackEntity_assets"."ownerId" AS "StackEntity__StackEntity_assets_ownerId",
|
||||
"StackEntity__StackEntity_assets"."libraryId" AS "StackEntity__StackEntity_assets_libraryId",
|
||||
"StackEntity__StackEntity_assets"."deviceId" AS "StackEntity__StackEntity_assets_deviceId",
|
||||
"StackEntity__StackEntity_assets"."type" AS "StackEntity__StackEntity_assets_type",
|
||||
"StackEntity__StackEntity_assets"."status" AS "StackEntity__StackEntity_assets_status",
|
||||
"StackEntity__StackEntity_assets"."originalPath" AS "StackEntity__StackEntity_assets_originalPath",
|
||||
"StackEntity__StackEntity_assets"."thumbhash" AS "StackEntity__StackEntity_assets_thumbhash",
|
||||
"StackEntity__StackEntity_assets"."encodedVideoPath" AS "StackEntity__StackEntity_assets_encodedVideoPath",
|
||||
"StackEntity__StackEntity_assets"."createdAt" AS "StackEntity__StackEntity_assets_createdAt",
|
||||
"StackEntity__StackEntity_assets"."updatedAt" AS "StackEntity__StackEntity_assets_updatedAt",
|
||||
"StackEntity__StackEntity_assets"."deletedAt" AS "StackEntity__StackEntity_assets_deletedAt",
|
||||
"StackEntity__StackEntity_assets"."fileCreatedAt" AS "StackEntity__StackEntity_assets_fileCreatedAt",
|
||||
"StackEntity__StackEntity_assets"."localDateTime" AS "StackEntity__StackEntity_assets_localDateTime",
|
||||
"StackEntity__StackEntity_assets"."fileModifiedAt" AS "StackEntity__StackEntity_assets_fileModifiedAt",
|
||||
"StackEntity__StackEntity_assets"."isFavorite" AS "StackEntity__StackEntity_assets_isFavorite",
|
||||
"StackEntity__StackEntity_assets"."isArchived" AS "StackEntity__StackEntity_assets_isArchived",
|
||||
"StackEntity__StackEntity_assets"."isExternal" AS "StackEntity__StackEntity_assets_isExternal",
|
||||
"StackEntity__StackEntity_assets"."isOffline" AS "StackEntity__StackEntity_assets_isOffline",
|
||||
"StackEntity__StackEntity_assets"."checksum" AS "StackEntity__StackEntity_assets_checksum",
|
||||
"StackEntity__StackEntity_assets"."duration" AS "StackEntity__StackEntity_assets_duration",
|
||||
"StackEntity__StackEntity_assets"."isVisible" AS "StackEntity__StackEntity_assets_isVisible",
|
||||
"StackEntity__StackEntity_assets"."livePhotoVideoId" AS "StackEntity__StackEntity_assets_livePhotoVideoId",
|
||||
"StackEntity__StackEntity_assets"."originalFileName" AS "StackEntity__StackEntity_assets_originalFileName",
|
||||
"StackEntity__StackEntity_assets"."sidecarPath" AS "StackEntity__StackEntity_assets_sidecarPath",
|
||||
"StackEntity__StackEntity_assets"."stackId" AS "StackEntity__StackEntity_assets_stackId",
|
||||
"StackEntity__StackEntity_assets"."duplicateId" AS "StackEntity__StackEntity_assets_duplicateId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_assetId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."description" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_description",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageWidth" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageWidth",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageHeight" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageHeight",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fileSizeInByte" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fileSizeInByte",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."orientation" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_orientation",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."dateTimeOriginal" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_dateTimeOriginal",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."modifyDate" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_modifyDate",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."timeZone" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_timeZone",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."latitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_latitude",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."longitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_longitude",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."projectionType" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_projectionType",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."city" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_city",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."livePhotoCID" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_livePhotoCID",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."autoStackId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_autoStackId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."state" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_state",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."country" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_country",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."make" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_make",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."model" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_model",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."lensModel" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_lensModel",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fNumber" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fNumber",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."focalLength" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_focalLength",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."iso" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_iso",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exposureTime" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exposureTime",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."profileDescription" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_profileDescription",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."colorspace" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_colorspace",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."bitsPerSample" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_bitsPerSample",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."rating" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_rating",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fps" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fps",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."id" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_id",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."value" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_value",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."createdAt" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_createdAt",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."updatedAt" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_updatedAt",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."color" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_color",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."parentId" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_parentId",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."userId" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_userId"
|
||||
FROM
|
||||
"asset_stack" "StackEntity"
|
||||
LEFT JOIN "assets" "StackEntity__StackEntity_assets" ON "StackEntity__StackEntity_assets"."stackId" = "StackEntity"."id"
|
||||
AND (
|
||||
"StackEntity__StackEntity_assets"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "exif" "01db479afeb88793eed8e0d1dde6ccfccf1698b9" ON "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" = "StackEntity__StackEntity_assets"."id"
|
||||
LEFT JOIN "tag_asset" "4f1c9474d4596aede2814ee2eb938eecf7a93b95" ON "4f1c9474d4596aede2814ee2eb938eecf7a93b95"."assetsId" = "StackEntity__StackEntity_assets"."id"
|
||||
LEFT JOIN "tags" "3e3064f11b97177a1e1ce3c77ecf32850343aba1" ON "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."id" = "4f1c9474d4596aede2814ee2eb938eecf7a93b95"."tagsId"
|
||||
WHERE
|
||||
(("StackEntity"."id" = $1))
|
||||
) "distinctAlias"
|
||||
ORDER BY
|
||||
"distinctAlias"."StackEntity__StackEntity_assets_fileCreatedAt" ASC,
|
||||
"StackEntity_id" ASC
|
||||
LIMIT
|
||||
1
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*,
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"tags".*
|
||||
from
|
||||
"tags"
|
||||
inner join "tag_asset" on "tags"."id" = "tag_asset"."tagsId"
|
||||
where
|
||||
"tag_asset"."assetsId" = "assets"."id"
|
||||
) as agg
|
||||
) as "tags"
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"assets"."deletedAt" is null
|
||||
and "assets"."stackId" = "asset_stack"."id"
|
||||
) as agg
|
||||
) as "assets"
|
||||
from
|
||||
"asset_stack"
|
||||
where
|
||||
"id" = $1::uuid
|
||||
|
@ -1,33 +1,18 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Kysely, sql } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||
|
||||
import { AlbumUserRole } from 'src/enum';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
|
||||
type IActivityAccess = IAccessRepository['activity'];
|
||||
type IAlbumAccess = IAccessRepository['album'];
|
||||
type IAssetAccess = IAccessRepository['asset'];
|
||||
type IAuthDeviceAccess = IAccessRepository['authDevice'];
|
||||
type IMemoryAccess = IAccessRepository['memory'];
|
||||
type IPersonAccess = IAccessRepository['person'];
|
||||
type IPartnerAccess = IAccessRepository['partner'];
|
||||
type IStackAccess = IAccessRepository['stack'];
|
||||
type ITagAccess = IAccessRepository['tag'];
|
||||
type ITimelineAccess = IAccessRepository['timeline'];
|
||||
|
||||
@Injectable()
|
||||
class ActivityAccess implements IActivityAccess {
|
||||
class ActivityAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, activityIds: Set<string>): Promise<Set<string>> {
|
||||
async checkOwnerAccess(userId: string, activityIds: Set<string>) {
|
||||
if (activityIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -36,17 +21,14 @@ class ActivityAccess implements IActivityAccess {
|
||||
.where('activity.id', 'in', [...activityIds])
|
||||
.where('activity.userId', '=', userId)
|
||||
.execute()
|
||||
.then((activities) => {
|
||||
console.log('activities', activities);
|
||||
return new Set(activities.map((activity) => activity.id));
|
||||
});
|
||||
.then((activities) => new Set(activities.map((activity) => activity.id)));
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkAlbumOwnerAccess(userId: string, activityIds: Set<string>): Promise<Set<string>> {
|
||||
async checkAlbumOwnerAccess(userId: string, activityIds: Set<string>) {
|
||||
if (activityIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -61,9 +43,9 @@ class ActivityAccess implements IActivityAccess {
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkCreateAccess(userId: string, albumIds: Set<string>): Promise<Set<string>> {
|
||||
async checkCreateAccess(userId: string, albumIds: Set<string>) {
|
||||
if (albumIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -80,14 +62,14 @@ class ActivityAccess implements IActivityAccess {
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumAccess implements IAlbumAccess {
|
||||
class AlbumAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, albumIds: Set<string>): Promise<Set<string>> {
|
||||
async checkOwnerAccess(userId: string, albumIds: Set<string>) {
|
||||
if (albumIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -102,9 +84,9 @@ class AlbumAccess implements IAlbumAccess {
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole): Promise<Set<string>> {
|
||||
async checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole) {
|
||||
if (albumIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
const accessRole =
|
||||
@ -125,9 +107,9 @@ class AlbumAccess implements IAlbumAccess {
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>> {
|
||||
async checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>) {
|
||||
if (albumIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -142,14 +124,14 @@ class AlbumAccess implements IAlbumAccess {
|
||||
}
|
||||
}
|
||||
|
||||
class AssetAccess implements IAssetAccess {
|
||||
class AssetAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkAlbumAccess(userId: string, assetIds: Set<string>): Promise<Set<string>> {
|
||||
async checkAlbumAccess(userId: string, assetIds: Set<string>) {
|
||||
if (assetIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -185,9 +167,9 @@ class AssetAccess implements IAssetAccess {
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, assetIds: Set<string>): Promise<Set<string>> {
|
||||
async checkOwnerAccess(userId: string, assetIds: Set<string>) {
|
||||
if (assetIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -201,9 +183,9 @@ class AssetAccess implements IAssetAccess {
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkPartnerAccess(userId: string, assetIds: Set<string>): Promise<Set<string>> {
|
||||
async checkPartnerAccess(userId: string, assetIds: Set<string>) {
|
||||
if (assetIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -224,9 +206,9 @@ class AssetAccess implements IAssetAccess {
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkSharedLinkAccess(sharedLinkId: string, assetIds: Set<string>): Promise<Set<string>> {
|
||||
async checkSharedLinkAccess(sharedLinkId: string, assetIds: Set<string>) {
|
||||
if (assetIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -276,14 +258,14 @@ class AssetAccess implements IAssetAccess {
|
||||
}
|
||||
}
|
||||
|
||||
class AuthDeviceAccess implements IAuthDeviceAccess {
|
||||
class AuthDeviceAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, deviceIds: Set<string>): Promise<Set<string>> {
|
||||
async checkOwnerAccess(userId: string, deviceIds: Set<string>) {
|
||||
if (deviceIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -296,14 +278,14 @@ class AuthDeviceAccess implements IAuthDeviceAccess {
|
||||
}
|
||||
}
|
||||
|
||||
class StackAccess implements IStackAccess {
|
||||
class StackAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, stackIds: Set<string>): Promise<Set<string>> {
|
||||
async checkOwnerAccess(userId: string, stackIds: Set<string>) {
|
||||
if (stackIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -316,14 +298,14 @@ class StackAccess implements IStackAccess {
|
||||
}
|
||||
}
|
||||
|
||||
class TimelineAccess implements ITimelineAccess {
|
||||
class TimelineAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkPartnerAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>> {
|
||||
async checkPartnerAccess(userId: string, partnerIds: Set<string>) {
|
||||
if (partnerIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -336,14 +318,14 @@ class TimelineAccess implements ITimelineAccess {
|
||||
}
|
||||
}
|
||||
|
||||
class MemoryAccess implements IMemoryAccess {
|
||||
class MemoryAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, memoryIds: Set<string>): Promise<Set<string>> {
|
||||
async checkOwnerAccess(userId: string, memoryIds: Set<string>) {
|
||||
if (memoryIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -357,14 +339,14 @@ class MemoryAccess implements IMemoryAccess {
|
||||
}
|
||||
}
|
||||
|
||||
class PersonAccess implements IPersonAccess {
|
||||
class PersonAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, personIds: Set<string>): Promise<Set<string>> {
|
||||
async checkOwnerAccess(userId: string, personIds: Set<string>) {
|
||||
if (personIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -378,9 +360,9 @@ class PersonAccess implements IPersonAccess {
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkFaceOwnerAccess(userId: string, assetFaceIds: Set<string>): Promise<Set<string>> {
|
||||
async checkFaceOwnerAccess(userId: string, assetFaceIds: Set<string>) {
|
||||
if (assetFaceIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -396,14 +378,14 @@ class PersonAccess implements IPersonAccess {
|
||||
}
|
||||
}
|
||||
|
||||
class PartnerAccess implements IPartnerAccess {
|
||||
class PartnerAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkUpdateAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>> {
|
||||
async checkUpdateAccess(userId: string, partnerIds: Set<string>) {
|
||||
if (partnerIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -416,14 +398,14 @@ class PartnerAccess implements IPartnerAccess {
|
||||
}
|
||||
}
|
||||
|
||||
class TagAccess implements ITagAccess {
|
||||
class TagAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, tagIds: Set<string>): Promise<Set<string>> {
|
||||
async checkOwnerAccess(userId: string, tagIds: Set<string>) {
|
||||
if (tagIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
@ -436,17 +418,17 @@ class TagAccess implements ITagAccess {
|
||||
}
|
||||
}
|
||||
|
||||
export class AccessRepository implements IAccessRepository {
|
||||
activity: IActivityAccess;
|
||||
album: IAlbumAccess;
|
||||
asset: IAssetAccess;
|
||||
authDevice: IAuthDeviceAccess;
|
||||
memory: IMemoryAccess;
|
||||
person: IPersonAccess;
|
||||
partner: IPartnerAccess;
|
||||
stack: IStackAccess;
|
||||
tag: ITagAccess;
|
||||
timeline: ITimelineAccess;
|
||||
export class AccessRepository {
|
||||
activity: ActivityAccess;
|
||||
album: AlbumAccess;
|
||||
asset: AssetAccess;
|
||||
authDevice: AuthDeviceAccess;
|
||||
memory: MemoryAccess;
|
||||
person: PersonAccess;
|
||||
partner: PartnerAccess;
|
||||
stack: StackAccess;
|
||||
tag: TagAccess;
|
||||
timeline: TimelineAccess;
|
||||
|
||||
constructor(@InjectKysely() db: Kysely<DB>) {
|
||||
this.activity = new ActivityAccess(db);
|
||||
|
@ -1,72 +1,116 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { ExpressionBuilder, Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { Albums, DB } from 'src/db';
|
||||
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import {
|
||||
DataSource,
|
||||
EntityManager,
|
||||
FindOptionsOrder,
|
||||
FindOptionsRelations,
|
||||
In,
|
||||
IsNull,
|
||||
Not,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
const withoutDeletedUsers = <T extends AlbumEntity | null>(album: T) => {
|
||||
if (album) {
|
||||
album.albumUsers = album.albumUsers.filter((albumUser) => albumUser.user && !albumUser.user.deletedAt);
|
||||
}
|
||||
return album;
|
||||
const userColumns = [
|
||||
'id',
|
||||
'email',
|
||||
'createdAt',
|
||||
'profileImagePath',
|
||||
'isAdmin',
|
||||
'shouldChangePassword',
|
||||
'deletedAt',
|
||||
'oauthId',
|
||||
'updatedAt',
|
||||
'storageLabel',
|
||||
'name',
|
||||
'quotaSizeInBytes',
|
||||
'quotaUsageInBytes',
|
||||
'status',
|
||||
'profileChangedAt',
|
||||
] as const;
|
||||
|
||||
const withOwner = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||
return jsonObjectFrom(eb.selectFrom('users').select(userColumns).whereRef('users.id', '=', 'albums.ownerId')).as(
|
||||
'owner',
|
||||
);
|
||||
};
|
||||
|
||||
const withAlbumUsers = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||
return jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('albums_shared_users_users as album_users')
|
||||
.selectAll('album_users')
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(eb.selectFrom('users').select(userColumns).whereRef('users.id', '=', 'album_users.usersId')).as(
|
||||
'user',
|
||||
),
|
||||
)
|
||||
.whereRef('album_users.albumsId', '=', 'albums.id'),
|
||||
).as('albumUsers');
|
||||
};
|
||||
|
||||
const withSharedLink = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||
return jsonArrayFrom(eb.selectFrom('shared_links').selectAll().whereRef('shared_links.albumId', '=', 'albums.id')).as(
|
||||
'sharedLinks',
|
||||
);
|
||||
};
|
||||
|
||||
const withAssets = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||
return eb
|
||||
.selectFrom((eb) =>
|
||||
eb
|
||||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||
.select((eb) => eb.fn.toJson('exif').as('exifInfo'))
|
||||
.innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')
|
||||
.whereRef('albums_assets_assets.albumsId', '=', 'albums.id')
|
||||
.orderBy('assets.fileCreatedAt', 'desc')
|
||||
.as('asset'),
|
||||
)
|
||||
.select((eb) => eb.fn.jsonAgg('asset').as('assets'))
|
||||
.as('assets');
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AlbumRepository implements IAlbumRepository {
|
||||
constructor(
|
||||
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
||||
@InjectRepository(AlbumEntity) private repository: Repository<AlbumEntity>,
|
||||
@InjectDataSource() private dataSource: DataSource,
|
||||
@InjectKysely() private db: Kysely<DB>,
|
||||
) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, {}] })
|
||||
async getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null> {
|
||||
const relations: FindOptionsRelations<AlbumEntity> = {
|
||||
owner: true,
|
||||
albumUsers: { user: true },
|
||||
assets: false,
|
||||
sharedLinks: true,
|
||||
};
|
||||
|
||||
const order: FindOptionsOrder<AlbumEntity> = {};
|
||||
|
||||
if (options.withAssets) {
|
||||
relations.assets = {
|
||||
exifInfo: true,
|
||||
};
|
||||
|
||||
order.assets = {
|
||||
fileCreatedAt: 'DESC',
|
||||
};
|
||||
}
|
||||
|
||||
const album = await this.repository.findOne({ where: { id }, relations, order });
|
||||
return withoutDeletedUsers(album);
|
||||
async getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | undefined> {
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
.where('albums.id', '=', id)
|
||||
.where('albums.deletedAt', 'is', null)
|
||||
.select(withOwner)
|
||||
.select(withAlbumUsers)
|
||||
.select(withSharedLink)
|
||||
.$if(options.withAssets, (eb) => eb.select(withAssets))
|
||||
.executeTakeFirst() as Promise<AlbumEntity | undefined>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
async getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]> {
|
||||
const albums = await this.repository.find({
|
||||
where: [
|
||||
{ ownerId, assets: { id: assetId } },
|
||||
{ albumUsers: { userId: ownerId }, assets: { id: assetId } },
|
||||
],
|
||||
relations: { owner: true, albumUsers: { user: true } },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return albums.map((album) => withoutDeletedUsers(album));
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
.leftJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id')
|
||||
.leftJoin('albums_shared_users_users as album_users', 'album_users.albumsId', 'albums.id')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb.and([eb('albums.ownerId', '=', ownerId), eb('album_assets.assetsId', '=', assetId)]),
|
||||
eb.and([eb('album_users.usersId', '=', ownerId), eb('album_assets.assetsId', '=', assetId)]),
|
||||
]),
|
||||
)
|
||||
.where('albums.deletedAt', 'is', null)
|
||||
.orderBy('albums.createdAt', 'desc')
|
||||
.select(withOwner)
|
||||
.select(withAlbumUsers)
|
||||
.orderBy('albums.createdAt', 'desc')
|
||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||
@ -77,36 +121,38 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Only possible with query builder because of GROUP BY.
|
||||
const albumMetadatas = await this.repository
|
||||
.createQueryBuilder('album')
|
||||
.select('album.id')
|
||||
.addSelect('MIN(assets.fileCreatedAt)', 'start_date')
|
||||
.addSelect('MAX(assets.fileCreatedAt)', 'end_date')
|
||||
.addSelect('COUNT(assets.id)', 'asset_count')
|
||||
.leftJoin('albums_assets_assets', 'album_assets', 'album_assets.albumsId = album.id')
|
||||
.leftJoin('assets', 'assets', 'assets.id = album_assets.assetsId')
|
||||
.where('album.id IN (:...ids)', { ids })
|
||||
.groupBy('album.id')
|
||||
.getRawMany();
|
||||
const metadatas = await this.db
|
||||
.selectFrom('albums')
|
||||
.leftJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id')
|
||||
.leftJoin('assets', 'assets.id', 'album_assets.assetsId')
|
||||
.select('albums.id')
|
||||
.select((eb) => eb.fn.min('assets.fileCreatedAt').as('startDate'))
|
||||
.select((eb) => eb.fn.max('assets.fileCreatedAt').as('endDate'))
|
||||
.select((eb) => eb.fn.count('assets.id').as('assetCount'))
|
||||
.where('albums.id', 'in', ids)
|
||||
.groupBy('albums.id')
|
||||
.execute();
|
||||
|
||||
return albumMetadatas.map<AlbumAssetCount>((metadatas) => ({
|
||||
albumId: metadatas['album_id'],
|
||||
assetCount: Number(metadatas['asset_count']),
|
||||
startDate: metadatas['end_date'] ? new Date(metadatas['start_date']) : undefined,
|
||||
endDate: metadatas['end_date'] ? new Date(metadatas['end_date']) : undefined,
|
||||
return metadatas.map((metadatas) => ({
|
||||
albumId: metadatas.id,
|
||||
assetCount: Number(metadatas.assetCount),
|
||||
startDate: metadatas.startDate ? new Date(metadatas.startDate) : undefined,
|
||||
endDate: metadatas.endDate ? new Date(metadatas.endDate) : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getOwned(ownerId: string): Promise<AlbumEntity[]> {
|
||||
const albums = await this.repository.find({
|
||||
relations: { albumUsers: { user: true }, sharedLinks: true, owner: true },
|
||||
where: { ownerId },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return albums.map((album) => withoutDeletedUsers(album));
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
.select(withOwner)
|
||||
.select(withAlbumUsers)
|
||||
.select(withSharedLink)
|
||||
.where('albums.ownerId', '=', ownerId)
|
||||
.where('albums.deletedAt', 'is', null)
|
||||
.orderBy('albums.createdAt', 'desc')
|
||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -114,17 +160,25 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
*/
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getShared(ownerId: string): Promise<AlbumEntity[]> {
|
||||
const albums = await this.repository.find({
|
||||
relations: { albumUsers: { user: true }, sharedLinks: true, owner: true },
|
||||
where: [
|
||||
{ albumUsers: { userId: ownerId } },
|
||||
{ sharedLinks: { userId: ownerId } },
|
||||
{ ownerId, albumUsers: { user: Not(IsNull()) } },
|
||||
],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return albums.map((album) => withoutDeletedUsers(album));
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
.distinctOn('albums.createdAt')
|
||||
.leftJoin('albums_shared_users_users as shared_albums', 'shared_albums.albumsId', 'albums.id')
|
||||
.leftJoin('shared_links', 'shared_links.albumId', 'albums.id')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('shared_albums.usersId', '=', ownerId),
|
||||
eb('shared_links.userId', '=', ownerId),
|
||||
eb.and([eb('albums.ownerId', '=', ownerId), eb('shared_albums.usersId', 'is not', null)]),
|
||||
]),
|
||||
)
|
||||
.where('albums.deletedAt', 'is', null)
|
||||
.select(withAlbumUsers)
|
||||
.select(withOwner)
|
||||
.select(withSharedLink)
|
||||
.orderBy('albums.createdAt', 'desc')
|
||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -132,35 +186,37 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
*/
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getNotShared(ownerId: string): Promise<AlbumEntity[]> {
|
||||
const albums = await this.repository.find({
|
||||
relations: { albumUsers: true, sharedLinks: true, owner: true },
|
||||
where: { ownerId, albumUsers: { user: IsNull() }, sharedLinks: { id: IsNull() } },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return albums.map((album) => withoutDeletedUsers(album));
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
.distinctOn('albums.createdAt')
|
||||
.leftJoin('albums_shared_users_users as shared_albums', 'shared_albums.albumsId', 'albums.id')
|
||||
.leftJoin('shared_links', 'shared_links.albumId', 'albums.id')
|
||||
.where('albums.ownerId', '=', ownerId)
|
||||
.where('shared_albums.usersId', 'is', null)
|
||||
.where('shared_links.userId', 'is', null)
|
||||
.where('albums.deletedAt', 'is', null)
|
||||
.select(withAlbumUsers)
|
||||
.select(withOwner)
|
||||
.select(withSharedLink)
|
||||
.orderBy('albums.createdAt', 'desc')
|
||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
||||
}
|
||||
|
||||
async restoreAll(userId: string): Promise<void> {
|
||||
await this.repository.restore({ ownerId: userId });
|
||||
await this.db.updateTable('albums').set({ deletedAt: null }).where('ownerId', '=', userId).execute();
|
||||
}
|
||||
|
||||
async softDeleteAll(userId: string): Promise<void> {
|
||||
await this.repository.softDelete({ ownerId: userId });
|
||||
await this.db.updateTable('albums').set({ deletedAt: new Date() }).where('ownerId', '=', userId).execute();
|
||||
}
|
||||
|
||||
async deleteAll(userId: string): Promise<void> {
|
||||
await this.repository.delete({ ownerId: userId });
|
||||
await this.db.deleteFrom('albums').where('ownerId', '=', userId).execute();
|
||||
}
|
||||
|
||||
async removeAsset(assetId: string): Promise<void> {
|
||||
// Using dataSource, because there is no direct access to albums_assets_assets.
|
||||
await this.dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from('albums_assets_assets')
|
||||
.where('"albums_assets_assets"."assetsId" = :assetId', { assetId })
|
||||
.execute();
|
||||
await this.db.deleteFrom('albums_assets_assets').where('albums_assets_assets.assetsId', '=', assetId).execute();
|
||||
}
|
||||
|
||||
@Chunked({ paramIndex: 1 })
|
||||
@ -169,14 +225,10 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from('albums_assets_assets')
|
||||
.where({
|
||||
albumsId: albumId,
|
||||
assetsId: In(assetIds),
|
||||
})
|
||||
await this.db
|
||||
.deleteFrom('albums_assets_assets')
|
||||
.where('albums_assets_assets.albumsId', '=', albumId)
|
||||
.where('albums_assets_assets.assetsId', 'in', assetIds)
|
||||
.execute();
|
||||
}
|
||||
|
||||
@ -194,73 +246,80 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
const results = await this.dataSource
|
||||
.createQueryBuilder()
|
||||
.select('albums_assets.assetsId', 'assetId')
|
||||
.from('albums_assets_assets', 'albums_assets')
|
||||
.where('"albums_assets"."albumsId" = :albumId', { albumId })
|
||||
.andWhere('"albums_assets"."assetsId" IN (:...assetIds)', { assetIds })
|
||||
.getRawMany<{ assetId: string }>();
|
||||
|
||||
return new Set(results.map(({ assetId }) => assetId));
|
||||
return this.db
|
||||
.selectFrom('albums_assets_assets')
|
||||
.selectAll()
|
||||
.where('albums_assets_assets.albumsId', '=', albumId)
|
||||
.where('albums_assets_assets.assetsId', 'in', assetIds)
|
||||
.execute()
|
||||
.then((results) => new Set(results.map(({ assetsId }) => assetsId)));
|
||||
}
|
||||
|
||||
async addAssetIds(albumId: string, assetIds: string[]): Promise<void> {
|
||||
await this.addAssets(this.dataSource.manager, albumId, assetIds);
|
||||
await this.addAssets(this.db, albumId, assetIds);
|
||||
}
|
||||
|
||||
create(album: Partial<AlbumEntity>): Promise<AlbumEntity> {
|
||||
return this.dataSource.transaction<AlbumEntity>(async (manager) => {
|
||||
const { id } = await manager.save(AlbumEntity, { ...album, assets: [] });
|
||||
const assetIds = (album.assets || []).map((asset) => asset.id);
|
||||
await this.addAssets(manager, id, assetIds);
|
||||
return manager.findOneOrFail(AlbumEntity, {
|
||||
where: { id },
|
||||
relations: {
|
||||
owner: true,
|
||||
albumUsers: { user: true },
|
||||
sharedLinks: true,
|
||||
assets: true,
|
||||
},
|
||||
});
|
||||
create(album: Insertable<Albums>, assetIds: string[], albumUsers: AlbumUserCreateDto[]): Promise<AlbumEntity> {
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
const newAlbum = await tx.insertInto('albums').values(album).returning('albums.id').executeTakeFirst();
|
||||
|
||||
if (!newAlbum) {
|
||||
throw new Error('Failed to create album');
|
||||
}
|
||||
|
||||
if (assetIds.length > 0) {
|
||||
await this.addAssets(tx, newAlbum.id, assetIds);
|
||||
}
|
||||
|
||||
if (albumUsers.length > 0) {
|
||||
await tx
|
||||
.insertInto('albums_shared_users_users')
|
||||
.values(
|
||||
albumUsers.map((albumUser) => ({ albumsId: newAlbum.id, usersId: albumUser.userId, role: albumUser.role })),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
return tx
|
||||
.selectFrom('albums')
|
||||
.selectAll()
|
||||
.where('id', '=', newAlbum.id)
|
||||
.select(withOwner)
|
||||
.select(withSharedLink)
|
||||
.select(withAssets)
|
||||
.select(withAlbumUsers)
|
||||
.executeTakeFirst() as unknown as Promise<AlbumEntity>;
|
||||
});
|
||||
}
|
||||
|
||||
update(album: Partial<AlbumEntity>): Promise<AlbumEntity> {
|
||||
return this.save(album);
|
||||
update(id: string, album: Updateable<Albums>): Promise<AlbumEntity> {
|
||||
return this.db
|
||||
.updateTable('albums')
|
||||
.set({ ...album, updatedAt: new Date() })
|
||||
.where('id', '=', id)
|
||||
.returningAll('albums')
|
||||
.returning(withOwner)
|
||||
.returning(withSharedLink)
|
||||
.returning(withAlbumUsers)
|
||||
.executeTakeFirst() as unknown as Promise<AlbumEntity>;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.repository.delete({ id });
|
||||
await this.db.deleteFrom('albums').where('id', '=', id).execute();
|
||||
}
|
||||
|
||||
@Chunked({ paramIndex: 2, chunkSize: 30_000 })
|
||||
private async addAssets(manager: EntityManager, albumId: string, assetIds: string[]): Promise<void> {
|
||||
private async addAssets(db: Kysely<DB>, albumId: string, assetIds: string[]): Promise<void> {
|
||||
if (assetIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into('albums_assets_assets', ['albumsId', 'assetsId'])
|
||||
await db
|
||||
.insertInto('albums_assets_assets')
|
||||
.values(assetIds.map((assetId) => ({ albumsId: albumId, assetsId: assetId })))
|
||||
.execute();
|
||||
}
|
||||
|
||||
private async save(album: Partial<AlbumEntity>) {
|
||||
const { id } = await this.repository.save(album);
|
||||
return this.repository.findOneOrFail({
|
||||
where: { id },
|
||||
relations: {
|
||||
owner: true,
|
||||
albumUsers: { user: true },
|
||||
sharedLinks: true,
|
||||
assets: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure all thumbnails for albums are updated by:
|
||||
* - Removing thumbnails from albums without assets
|
||||
@ -272,28 +331,44 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
async updateThumbnails(): Promise<number | undefined> {
|
||||
// Subquery for getting a new thumbnail.
|
||||
|
||||
const builder = this.dataSource
|
||||
.createQueryBuilder('albums_assets_assets', 'album_assets')
|
||||
.innerJoin('assets', 'assets', '"album_assets"."assetsId" = "assets"."id"')
|
||||
.where('"album_assets"."albumsId" = "albums"."id"');
|
||||
const result = await this.db
|
||||
.updateTable('albums')
|
||||
.set((eb) => ({
|
||||
albumThumbnailAssetId: this.updateThumbnailBuilder(eb)
|
||||
.select('album_assets.assetsId')
|
||||
.orderBy('assets.fileCreatedAt', 'desc')
|
||||
.limit(1),
|
||||
updatedAt: new Date(),
|
||||
}))
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb.and([
|
||||
eb('albumThumbnailAssetId', 'is', null),
|
||||
eb.exists(this.updateThumbnailBuilder(eb).select(sql`1`.as('1'))), // Has assets
|
||||
]),
|
||||
eb.and([
|
||||
eb('albumThumbnailAssetId', 'is not', null),
|
||||
eb.not(
|
||||
eb.exists(
|
||||
this.updateThumbnailBuilder(eb)
|
||||
.select(sql`1`.as('1'))
|
||||
.whereRef('albums.albumThumbnailAssetId', '=', 'album_assets.assetsId'), // Has invalid assets
|
||||
),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
)
|
||||
.execute();
|
||||
|
||||
const newThumbnail = builder
|
||||
.clone()
|
||||
.select('"album_assets"."assetsId"')
|
||||
.orderBy('"assets"."fileCreatedAt"', 'DESC')
|
||||
.limit(1);
|
||||
const hasAssets = builder.clone().select('1');
|
||||
const hasInvalidAsset = hasAssets.clone().andWhere('"albums"."albumThumbnailAssetId" = "album_assets"."assetsId"');
|
||||
return Number(result[0].numUpdatedRows);
|
||||
}
|
||||
|
||||
const updateAlbums = this.repository
|
||||
.createQueryBuilder('albums')
|
||||
.update(AlbumEntity)
|
||||
.set({ albumThumbnailAssetId: () => `(${newThumbnail.getQuery()})` })
|
||||
.where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${hasAssets.getQuery()})`)
|
||||
.orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${hasInvalidAsset.getQuery()})`);
|
||||
|
||||
const result = await updateAlbums.execute();
|
||||
|
||||
return result.affected;
|
||||
private updateThumbnailBuilder(eb: ExpressionBuilder<DB, 'albums'>) {
|
||||
return eb
|
||||
.selectFrom('albums_assets_assets as album_assets')
|
||||
.innerJoin('assets', (join) =>
|
||||
join.onRef('album_assets.assetsId', '=', 'assets.id').on('assets.deletedAt', 'is', null),
|
||||
)
|
||||
.whereRef('album_assets.albumsId', '=', 'albums.id');
|
||||
}
|
||||
}
|
||||
|
@ -1,50 +1,36 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Insertable, Kysely, Updateable } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { ApiKeys, DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { APIKeyEntity } from 'src/entities/api-key.entity';
|
||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||
import { AuthApiKey } from 'src/types';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
const columns = ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'] as const;
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyRepository implements IKeyRepository {
|
||||
constructor(
|
||||
@InjectRepository(APIKeyEntity) private repository: Repository<APIKeyEntity>,
|
||||
@InjectKysely() private db: Kysely<DB>,
|
||||
) {}
|
||||
export class ApiKeyRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
async create(dto: Insertable<ApiKeys>): Promise<APIKeyEntity> {
|
||||
const { id, name, createdAt, updatedAt, permissions } = await this.db
|
||||
.insertInto('api_keys')
|
||||
.values(dto)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return { id, name, createdAt, updatedAt, permissions } as APIKeyEntity;
|
||||
create(dto: Insertable<ApiKeys>) {
|
||||
return this.db.insertInto('api_keys').values(dto).returningAll().executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
async update(userId: string, id: string, dto: Updateable<ApiKeys>): Promise<APIKeyEntity> {
|
||||
async update(userId: string, id: string, dto: Updateable<ApiKeys>) {
|
||||
return this.db
|
||||
.updateTable('api_keys')
|
||||
.set(dto)
|
||||
.where('api_keys.userId', '=', userId)
|
||||
.where('id', '=', asUuid(id))
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow() as unknown as Promise<APIKeyEntity>;
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
async delete(userId: string, id: string): Promise<void> {
|
||||
async delete(userId: string, id: string) {
|
||||
await this.db.deleteFrom('api_keys').where('userId', '=', userId).where('id', '=', asUuid(id)).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
getKey(hashedToken: string): Promise<AuthApiKey | undefined> {
|
||||
getKey(hashedToken: string) {
|
||||
return this.db
|
||||
.selectFrom('api_keys')
|
||||
.innerJoinLateral(
|
||||
@ -72,26 +58,26 @@ export class ApiKeyRepository implements IKeyRepository {
|
||||
eb.fn.toJson('user').as('user'),
|
||||
])
|
||||
.where('api_keys.key', '=', hashedToken)
|
||||
.executeTakeFirst() as Promise<AuthApiKey | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
getById(userId: string, id: string): Promise<APIKeyEntity | null> {
|
||||
getById(userId: string, id: string) {
|
||||
return this.db
|
||||
.selectFrom('api_keys')
|
||||
.select(columns)
|
||||
.where('id', '=', asUuid(id))
|
||||
.where('userId', '=', userId)
|
||||
.executeTakeFirst() as unknown as Promise<APIKeyEntity | null>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getByUserId(userId: string): Promise<APIKeyEntity[]> {
|
||||
getByUserId(userId: string) {
|
||||
return this.db
|
||||
.selectFrom('api_keys')
|
||||
.select(columns)
|
||||
.where('userId', '=', userId)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.execute() as unknown as Promise<APIKeyEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
@ -4,10 +4,15 @@ import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { DatabaseAction, EntityType } from 'src/enum';
|
||||
import { AuditSearch, IAuditRepository } from 'src/interfaces/audit.interface';
|
||||
|
||||
export interface AuditSearch {
|
||||
action?: DatabaseAction;
|
||||
entityType?: EntityType;
|
||||
userIds: string[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuditRepository implements IAuditRepository {
|
||||
export class AuditRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({
|
||||
|
@ -1,19 +1,106 @@
|
||||
import { RegisterQueueOptions } from '@nestjs/bullmq';
|
||||
import { Inject, Injectable, Optional } from '@nestjs/common';
|
||||
import { QueueOptions } from 'bullmq';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validateSync } from 'class-validator';
|
||||
import { Request, Response } from 'express';
|
||||
import { RedisOptions } from 'ioredis';
|
||||
import { KyselyConfig } from 'kysely';
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import { CLS_ID } from 'nestjs-cls';
|
||||
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
|
||||
import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
|
||||
import { join, resolve } from 'node:path';
|
||||
import postgres, { Notice } from 'postgres';
|
||||
import { citiesFile, excludePaths, IWorker } from 'src/constants';
|
||||
import { Telemetry } from 'src/decorators';
|
||||
import { EnvDto } from 'src/dtos/env.dto';
|
||||
import { ImmichEnvironment, ImmichHeader, ImmichTelemetry, ImmichWorker } from 'src/enum';
|
||||
import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
||||
import { ImmichEnvironment, ImmichHeader, ImmichTelemetry, ImmichWorker, LogLevel } from 'src/enum';
|
||||
import { DatabaseConnectionParams, DatabaseExtension, VectorExtension } from 'src/interfaces/database.interface';
|
||||
import { QueueName } from 'src/interfaces/job.interface';
|
||||
import { setDifference } from 'src/utils/set';
|
||||
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js';
|
||||
|
||||
export interface EnvData {
|
||||
host?: string;
|
||||
port: number;
|
||||
environment: ImmichEnvironment;
|
||||
configFile?: string;
|
||||
logLevel?: LogLevel;
|
||||
|
||||
buildMetadata: {
|
||||
build?: string;
|
||||
buildUrl?: string;
|
||||
buildImage?: string;
|
||||
buildImageUrl?: string;
|
||||
repository?: string;
|
||||
repositoryUrl?: string;
|
||||
sourceRef?: string;
|
||||
sourceCommit?: string;
|
||||
sourceUrl?: string;
|
||||
thirdPartySourceUrl?: string;
|
||||
thirdPartyBugFeatureUrl?: string;
|
||||
thirdPartyDocumentationUrl?: string;
|
||||
thirdPartySupportUrl?: string;
|
||||
};
|
||||
|
||||
bull: {
|
||||
config: QueueOptions;
|
||||
queues: RegisterQueueOptions[];
|
||||
};
|
||||
|
||||
cls: {
|
||||
config: ClsModuleOptions;
|
||||
};
|
||||
|
||||
database: {
|
||||
config: { typeorm: PostgresConnectionOptions & DatabaseConnectionParams; kysely: KyselyConfig };
|
||||
skipMigrations: boolean;
|
||||
vectorExtension: VectorExtension;
|
||||
};
|
||||
|
||||
licensePublicKey: {
|
||||
client: string;
|
||||
server: string;
|
||||
};
|
||||
|
||||
network: {
|
||||
trustedProxies: string[];
|
||||
};
|
||||
|
||||
otel: OpenTelemetryModuleOptions;
|
||||
|
||||
resourcePaths: {
|
||||
lockFile: string;
|
||||
geodata: {
|
||||
dateFile: string;
|
||||
admin1: string;
|
||||
admin2: string;
|
||||
cities500: string;
|
||||
naturalEarthCountriesPath: string;
|
||||
};
|
||||
web: {
|
||||
root: string;
|
||||
indexHtml: string;
|
||||
};
|
||||
};
|
||||
|
||||
redis: RedisOptions;
|
||||
|
||||
telemetry: {
|
||||
apiPort: number;
|
||||
microservicesPort: number;
|
||||
metrics: Set<ImmichTelemetry>;
|
||||
};
|
||||
|
||||
storage: {
|
||||
ignoreMountCheckErrors: boolean;
|
||||
};
|
||||
|
||||
workers: ImmichWorker[];
|
||||
|
||||
noColor: boolean;
|
||||
nodeVersion?: string;
|
||||
}
|
||||
|
||||
const productionKeys = {
|
||||
client:
|
||||
@ -269,10 +356,10 @@ let cached: EnvData | undefined;
|
||||
|
||||
@Injectable()
|
||||
@Telemetry({ enabled: false })
|
||||
export class ConfigRepository implements IConfigRepository {
|
||||
export class ConfigRepository {
|
||||
constructor(@Inject(IWorker) @Optional() private worker?: ImmichWorker) {}
|
||||
|
||||
getEnv(): EnvData {
|
||||
getEnv() {
|
||||
if (!cached) {
|
||||
cached = getEnv();
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import { InjectKysely } from 'nestjs-kysely';
|
||||
import semver from 'semver';
|
||||
import { POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants';
|
||||
import { DB } from 'src/db';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import {
|
||||
DatabaseExtension,
|
||||
DatabaseLock,
|
||||
@ -18,6 +17,7 @@ import {
|
||||
VectorUpdateResult,
|
||||
} from 'src/interfaces/database.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { UPSERT_COLUMNS } from 'src/utils/database';
|
||||
import { isValidInteger } from 'src/validation';
|
||||
import { DataSource, EntityManager, EntityMetadata, QueryRunner } from 'typeorm';
|
||||
@ -31,7 +31,7 @@ export class DatabaseRepository implements IDatabaseRepository {
|
||||
@InjectKysely() private db: Kysely<DB>,
|
||||
@InjectDataSource() private dataSource: DataSource,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
configRepository: ConfigRepository,
|
||||
) {
|
||||
this.vectorExtension = configRepository.getEnv().database.vectorExtension;
|
||||
this.logger.setContext(DatabaseRepository.name);
|
||||
|
@ -12,7 +12,6 @@ import _ from 'lodash';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import { EventConfig } from 'src/decorators';
|
||||
import { ImmichWorker, MetadataKey } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import {
|
||||
ArgsOf,
|
||||
ClientEventMap,
|
||||
@ -52,7 +51,7 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
@Inject(IConfigRepository) private configRepository: ConfigRepository,
|
||||
private configRepository: ConfigRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(EventRepository.name);
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IAuditRepository } from 'src/interfaces/audit.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICronRepository } from 'src/interfaces/cron.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
@ -35,7 +31,6 @@ import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
|
||||
import { ITrashRepository } from 'src/interfaces/trash.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
|
||||
import { IViewRepository } from 'src/interfaces/view.interface';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||
@ -78,22 +73,23 @@ import { ViewRepository } from 'src/repositories/view-repository';
|
||||
|
||||
export const repositories = [
|
||||
//
|
||||
AccessRepository,
|
||||
ActivityRepository,
|
||||
AuditRepository,
|
||||
ApiKeyRepository,
|
||||
ConfigRepository,
|
||||
ViewRepository,
|
||||
];
|
||||
|
||||
export const providers = [
|
||||
{ provide: IAccessRepository, useClass: AccessRepository },
|
||||
{ provide: IAlbumRepository, useClass: AlbumRepository },
|
||||
{ provide: IAlbumUserRepository, useClass: AlbumUserRepository },
|
||||
{ provide: IAssetRepository, useClass: AssetRepository },
|
||||
{ provide: IAuditRepository, useClass: AuditRepository },
|
||||
{ provide: IConfigRepository, useClass: ConfigRepository },
|
||||
{ provide: ICronRepository, useClass: CronRepository },
|
||||
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
||||
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
|
||||
{ provide: IEventRepository, useClass: EventRepository },
|
||||
{ provide: IJobRepository, useClass: JobRepository },
|
||||
{ provide: IKeyRepository, useClass: ApiKeyRepository },
|
||||
{ provide: ILibraryRepository, useClass: LibraryRepository },
|
||||
{ provide: ILoggerRepository, useClass: LoggerRepository },
|
||||
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository },
|
||||
@ -119,5 +115,4 @@ export const providers = [
|
||||
{ provide: ITrashRepository, useClass: TrashRepository },
|
||||
{ provide: IUserRepository, useClass: UserRepository },
|
||||
{ provide: IVersionHistoryRepository, useClass: VersionHistoryRepository },
|
||||
{ provide: IViewRepository, useClass: ViewRepository },
|
||||
];
|
||||
|
@ -1,13 +1,11 @@
|
||||
import { getQueueToken } from '@nestjs/bullmq';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ModuleRef, Reflector } from '@nestjs/core';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { JobsOptions, Queue, Worker } from 'bullmq';
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { JobConfig } from 'src/decorators';
|
||||
import { MetadataKey } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import {
|
||||
IEntityJob,
|
||||
@ -22,6 +20,7 @@ import {
|
||||
QueueStatus,
|
||||
} from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { getKeyByValue, getMethodNames, ImmichStartupError } from 'src/utils/misc';
|
||||
|
||||
type JobMapItem = {
|
||||
@ -38,8 +37,7 @@ export class JobRepository implements IJobRepository {
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
private schedulerRegistry: SchedulerRegistry,
|
||||
@Inject(IConfigRepository) private configRepository: IConfigRepository,
|
||||
private configRepository: ConfigRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { ImmichWorker } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { LoggerRepository } from 'src/repositories/logger.repository';
|
||||
import { IConfigRepository } from 'src/types';
|
||||
import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { ConsoleLogger, Inject, Injectable, Scope } from '@nestjs/common';
|
||||
import { ConsoleLogger, Injectable, Scope } from '@nestjs/common';
|
||||
import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { Telemetry } from 'src/decorators';
|
||||
import { LogLevel } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
|
||||
const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL];
|
||||
|
||||
@ -25,7 +25,7 @@ export class LoggerRepository extends ConsoleLogger implements ILoggerRepository
|
||||
|
||||
constructor(
|
||||
private cls: ClsService,
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
configRepository: ConfigRepository,
|
||||
) {
|
||||
super(LoggerRepository.name);
|
||||
|
||||
|
@ -11,7 +11,6 @@ import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db';
|
||||
import { AssetEntity, withExif } from 'src/entities/asset.entity';
|
||||
import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity';
|
||||
import { LogLevel, SystemMetadataKey } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import {
|
||||
GeoPoint,
|
||||
@ -21,6 +20,7 @@ import {
|
||||
ReverseGeocodeResult,
|
||||
} from 'src/interfaces/map.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
|
||||
interface MapDB extends DB {
|
||||
geodata_places_tmp: GeodataPlaces;
|
||||
@ -30,7 +30,7 @@ interface MapDB extends DB {
|
||||
@Injectable()
|
||||
export class MapRepository implements IMapRepository {
|
||||
constructor(
|
||||
@Inject(IConfigRepository) private configRepository: IConfigRepository,
|
||||
private configRepository: ConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) private metadataRepository: ISystemMetadataRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
@InjectKysely() private db: Kysely<MapDB>,
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
import { ExpressionBuilder, Insertable, Kysely, SelectExpression, sql } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import _ from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { AssetFaces, DB, FaceSearch, Person } from 'src/db';
|
||||
import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { FaceSearchEntity } from 'src/entities/face-search.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { PaginationMode, SourceType } from 'src/enum';
|
||||
import { SourceType } from 'src/enum';
|
||||
import {
|
||||
AssetFaceId,
|
||||
DeleteFacesOptions,
|
||||
@ -17,332 +17,418 @@ import {
|
||||
PersonNameSearchOptions,
|
||||
PersonSearchOptions,
|
||||
PersonStatistics,
|
||||
SelectFaceOptions,
|
||||
UnassignFacesOptions,
|
||||
UpdateFacesData,
|
||||
} from 'src/interfaces/person.interface';
|
||||
import { Paginated, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
|
||||
import { DataSource, FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm';
|
||||
import { mapUpsertColumns } from 'src/utils/database';
|
||||
import { Paginated, PaginationOptions } from 'src/utils/pagination';
|
||||
import { FindOptionsRelations } from 'typeorm';
|
||||
|
||||
const withPerson = (eb: ExpressionBuilder<DB, 'asset_faces'>) => {
|
||||
return jsonObjectFrom(
|
||||
eb.selectFrom('person').selectAll('person').whereRef('person.id', '=', 'asset_faces.personId'),
|
||||
).as('person');
|
||||
};
|
||||
|
||||
const withAsset = (eb: ExpressionBuilder<DB, 'asset_faces'>) => {
|
||||
return jsonObjectFrom(
|
||||
eb.selectFrom('assets').selectAll('assets').whereRef('assets.id', '=', 'asset_faces.assetId'),
|
||||
).as('asset');
|
||||
};
|
||||
|
||||
const withFaceSearch = (eb: ExpressionBuilder<DB, 'asset_faces'>) => {
|
||||
return jsonObjectFrom(
|
||||
eb.selectFrom('face_search').selectAll('face_search').whereRef('face_search.faceId', '=', 'asset_faces.id'),
|
||||
).as('faceSearch');
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PersonRepository implements IPersonRepository {
|
||||
constructor(
|
||||
@InjectDataSource() private dataSource: DataSource,
|
||||
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
||||
@InjectRepository(PersonEntity) private personRepository: Repository<PersonEntity>,
|
||||
@InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository<AssetFaceEntity>,
|
||||
@InjectRepository(FaceSearchEntity) private faceSearchRepository: Repository<FaceSearchEntity>,
|
||||
@InjectRepository(AssetJobStatusEntity) private jobStatusRepository: Repository<AssetJobStatusEntity>,
|
||||
) {}
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] })
|
||||
async reassignFaces({ oldPersonId, faceIds, newPersonId }: UpdateFacesData): Promise<number> {
|
||||
const result = await this.assetFaceRepository
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
const result = await this.db
|
||||
.updateTable('asset_faces')
|
||||
.set({ personId: newPersonId })
|
||||
.where(_.omitBy({ personId: oldPersonId, id: faceIds ? In(faceIds) : undefined }, _.isUndefined))
|
||||
.execute();
|
||||
.$if(!!oldPersonId, (qb) => qb.where('asset_faces.personId', '=', oldPersonId!))
|
||||
.$if(!!faceIds, (qb) => qb.where('asset_faces.id', 'in', faceIds!))
|
||||
.executeTakeFirst();
|
||||
|
||||
return result.affected ?? 0;
|
||||
return Number(result.numChangedRows) ?? 0;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ sourceType: SourceType.EXIF }] })
|
||||
async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> {
|
||||
await this.assetFaceRepository
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
await this.db
|
||||
.updateTable('asset_faces')
|
||||
.set({ personId: null })
|
||||
.where({ sourceType })
|
||||
.where('asset_faces.sourceType', '=', sourceType)
|
||||
.execute();
|
||||
|
||||
await this.vacuum({ reindexVectors: false });
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[{ id: DummyValue.UUID }]] })
|
||||
async delete(entities: PersonEntity[]): Promise<void> {
|
||||
await this.personRepository.remove(entities);
|
||||
if (entities.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.db
|
||||
.deleteFrom('person')
|
||||
.where(
|
||||
'person.id',
|
||||
'in',
|
||||
entities.map(({ id }) => id),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ sourceType: SourceType.EXIF }] })
|
||||
async deleteFaces({ sourceType }: DeleteFacesOptions): Promise<void> {
|
||||
await this.assetFaceRepository
|
||||
.createQueryBuilder('asset_faces')
|
||||
.delete()
|
||||
.andWhere('sourceType = :sourceType', { sourceType })
|
||||
.execute();
|
||||
await this.db.deleteFrom('asset_faces').where('asset_faces.sourceType', '=', sourceType).execute();
|
||||
|
||||
await this.vacuum({ reindexVectors: sourceType === SourceType.MACHINE_LEARNING });
|
||||
}
|
||||
|
||||
getAllFaces(
|
||||
pagination: PaginationOptions,
|
||||
options: FindManyOptions<AssetFaceEntity> = {},
|
||||
): Paginated<AssetFaceEntity> {
|
||||
return paginate(this.assetFaceRepository, pagination, options);
|
||||
getAllFaces(options: Partial<AssetFaceEntity> = {}): AsyncIterableIterator<AssetFaceEntity> {
|
||||
return this.db
|
||||
.selectFrom('asset_faces')
|
||||
.selectAll('asset_faces')
|
||||
.$if(options.personId === null, (qb) => qb.where('asset_faces.personId', 'is', null))
|
||||
.$if(!!options.personId, (qb) => qb.where('asset_faces.personId', '=', options.personId!))
|
||||
.$if(!!options.sourceType, (qb) => qb.where('asset_faces.sourceType', '=', options.sourceType!))
|
||||
.$if(!!options.assetId, (qb) => qb.where('asset_faces.assetId', '=', options.assetId!))
|
||||
.$if(!!options.assetId, (qb) => qb.where('asset_faces.assetId', '=', options.assetId!))
|
||||
.stream() as AsyncIterableIterator<AssetFaceEntity>;
|
||||
}
|
||||
|
||||
getAll(pagination: PaginationOptions, options: FindManyOptions<PersonEntity> = {}): Paginated<PersonEntity> {
|
||||
return paginate(this.personRepository, pagination, options);
|
||||
getAll(options: Partial<PersonEntity> = {}): AsyncIterableIterator<PersonEntity> {
|
||||
return this.db
|
||||
.selectFrom('person')
|
||||
.selectAll('person')
|
||||
.$if(!!options.ownerId, (qb) => qb.where('person.ownerId', '=', options.ownerId!))
|
||||
.$if(!!options.thumbnailPath, (qb) => qb.where('person.thumbnailPath', '=', options.thumbnailPath!))
|
||||
.$if(options.faceAssetId === null, (qb) => qb.where('person.faceAssetId', 'is', null))
|
||||
.$if(!!options.faceAssetId, (qb) => qb.where('person.faceAssetId', '=', options.faceAssetId!))
|
||||
.$if(options.isHidden !== undefined, (qb) => qb.where('person.isHidden', '=', options.isHidden!))
|
||||
.stream() as AsyncIterableIterator<PersonEntity>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ take: 10, skip: 10 }, DummyValue.UUID] })
|
||||
async getAllForUser(
|
||||
pagination: PaginationOptions,
|
||||
userId: string,
|
||||
options?: PersonSearchOptions,
|
||||
): Paginated<PersonEntity> {
|
||||
const queryBuilder = this.personRepository
|
||||
.createQueryBuilder('person')
|
||||
.innerJoin('person.faces', 'face')
|
||||
.where('person.ownerId = :userId', { userId })
|
||||
.innerJoin('face.asset', 'asset')
|
||||
.andWhere('asset.isArchived = false')
|
||||
.orderBy('person.isHidden', 'ASC')
|
||||
.addOrderBy("NULLIF(person.name, '') IS NULL", 'ASC')
|
||||
.addOrderBy('COUNT(face.assetId)', 'DESC')
|
||||
.addOrderBy("NULLIF(person.name, '')", 'ASC', 'NULLS LAST')
|
||||
.addOrderBy('person.createdAt')
|
||||
.having("person.name != '' OR COUNT(face.assetId) >= :faces", { faces: options?.minimumFaceCount || 1 })
|
||||
.groupBy('person.id');
|
||||
if (options?.closestFaceAssetId) {
|
||||
const innerQueryBuilder = this.faceSearchRepository
|
||||
.createQueryBuilder('face_search')
|
||||
.select('embedding', 'embedding')
|
||||
.where('"face_search"."faceId" = "person"."faceAssetId"');
|
||||
const faceSelectQueryBuilder = this.faceSearchRepository
|
||||
.createQueryBuilder('face_search')
|
||||
.select('embedding', 'embedding')
|
||||
.where('"face_search"."faceId" = :faceId', { faceId: options.closestFaceAssetId });
|
||||
queryBuilder
|
||||
.orderBy('(' + innerQueryBuilder.getQuery() + ') <=> (' + faceSelectQueryBuilder.getQuery() + ')')
|
||||
.setParameters(faceSelectQueryBuilder.getParameters());
|
||||
const items = (await this.db
|
||||
.selectFrom('person')
|
||||
.selectAll('person')
|
||||
.innerJoin('asset_faces', 'asset_faces.personId', 'person.id')
|
||||
.innerJoin('assets', (join) =>
|
||||
join
|
||||
.onRef('asset_faces.assetId', '=', 'assets.id')
|
||||
.on('assets.isArchived', '=', false)
|
||||
.on('assets.deletedAt', 'is', null),
|
||||
)
|
||||
.where('person.ownerId', '=', userId)
|
||||
.orderBy('person.isHidden', 'asc')
|
||||
.orderBy(sql`NULLIF(person.name, '') is null`, 'asc')
|
||||
.orderBy((eb) => eb.fn.count('asset_faces.assetId'), 'desc')
|
||||
.orderBy(sql`NULLIF(person.name, '')`, sql`asc nulls last`)
|
||||
.orderBy('person.createdAt')
|
||||
.having((eb) =>
|
||||
eb.or([
|
||||
eb('person.name', '!=', ''),
|
||||
eb((innerEb) => innerEb.fn.count('asset_faces.assetId'), '>=', options?.minimumFaceCount || 1),
|
||||
]),
|
||||
)
|
||||
.groupBy('person.id')
|
||||
.$if(!!options?.closestFaceAssetId, (qb) =>
|
||||
qb.orderBy((eb) =>
|
||||
eb(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('face_search')
|
||||
.select('face_search.embedding')
|
||||
.whereRef('face_search.faceId', '=', 'person.faceAssetId'),
|
||||
'<=>',
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('face_search')
|
||||
.select('face_search.embedding')
|
||||
.where('face_search.faceId', '=', options!.closestFaceAssetId!),
|
||||
),
|
||||
),
|
||||
)
|
||||
.$if(!options?.withHidden, (qb) => qb.where('person.isHidden', '=', false))
|
||||
.offset(pagination.skip ?? 0)
|
||||
.limit(pagination.take + 1)
|
||||
.execute()) as PersonEntity[];
|
||||
|
||||
if (items.length > pagination.take) {
|
||||
return { items: items.slice(0, -1), hasNextPage: true };
|
||||
}
|
||||
if (!options?.withHidden) {
|
||||
queryBuilder.andWhere('person.isHidden = false');
|
||||
}
|
||||
return paginatedBuilder(queryBuilder, {
|
||||
mode: PaginationMode.LIMIT_OFFSET,
|
||||
...pagination,
|
||||
});
|
||||
|
||||
return { items, hasNextPage: false };
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
getAllWithoutFaces(): Promise<PersonEntity[]> {
|
||||
return this.personRepository
|
||||
.createQueryBuilder('person')
|
||||
.leftJoin('person.faces', 'face')
|
||||
.having('COUNT(face.assetId) = 0')
|
||||
return this.db
|
||||
.selectFrom('person')
|
||||
.selectAll('person')
|
||||
.leftJoin('asset_faces', 'asset_faces.personId', 'person.id')
|
||||
.having((eb) => eb.fn.count('asset_faces.assetId'), '=', 0)
|
||||
.groupBy('person.id')
|
||||
.withDeleted()
|
||||
.getMany();
|
||||
.execute() as Promise<PersonEntity[]>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFaces(assetId: string): Promise<AssetFaceEntity[]> {
|
||||
return this.assetFaceRepository.find({
|
||||
where: { assetId },
|
||||
relations: {
|
||||
person: true,
|
||||
},
|
||||
order: {
|
||||
boundingBoxX1: 'ASC',
|
||||
},
|
||||
});
|
||||
return this.db
|
||||
.selectFrom('asset_faces')
|
||||
.selectAll('asset_faces')
|
||||
.select(withPerson)
|
||||
.where('asset_faces.assetId', '=', assetId)
|
||||
.orderBy('asset_faces.boundingBoxX1', 'asc')
|
||||
.execute() as Promise<AssetFaceEntity[]>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFaceById(id: string): Promise<AssetFaceEntity> {
|
||||
// TODO return null instead of find or fail
|
||||
return this.assetFaceRepository.findOneOrFail({
|
||||
where: { id },
|
||||
relations: {
|
||||
person: true,
|
||||
},
|
||||
});
|
||||
return this.db
|
||||
.selectFrom('asset_faces')
|
||||
.selectAll('asset_faces')
|
||||
.select(withPerson)
|
||||
.where('asset_faces.id', '=', id)
|
||||
.executeTakeFirstOrThrow() as Promise<AssetFaceEntity>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFaceByIdWithAssets(
|
||||
id: string,
|
||||
relations: FindOptionsRelations<AssetFaceEntity>,
|
||||
select: FindOptionsSelect<AssetFaceEntity>,
|
||||
relations?: FindOptionsRelations<AssetFaceEntity>,
|
||||
select?: SelectFaceOptions,
|
||||
): Promise<AssetFaceEntity | null> {
|
||||
return this.assetFaceRepository.findOne(
|
||||
_.omitBy(
|
||||
{
|
||||
where: { id },
|
||||
relations: {
|
||||
...relations,
|
||||
person: true,
|
||||
asset: true,
|
||||
},
|
||||
select,
|
||||
},
|
||||
_.isUndefined,
|
||||
),
|
||||
);
|
||||
return (this.db
|
||||
.selectFrom('asset_faces')
|
||||
.$if(!!select, (qb) =>
|
||||
qb.select(
|
||||
Object.keys(
|
||||
_.omitBy({ ...select!, faceSearch: undefined, asset: undefined }, _.isUndefined),
|
||||
) as SelectExpression<DB, 'asset_faces'>[],
|
||||
),
|
||||
)
|
||||
.$if(!select, (qb) => qb.selectAll('asset_faces'))
|
||||
.select(withPerson)
|
||||
.select(withAsset)
|
||||
.$if(!!relations?.faceSearch, (qb) => qb.select(withFaceSearch))
|
||||
.where('asset_faces.id', '=', id)
|
||||
.executeTakeFirst() ?? null) as Promise<AssetFaceEntity | null>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
async reassignFace(assetFaceId: string, newPersonId: string): Promise<number> {
|
||||
const result = await this.assetFaceRepository
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
const result = await this.db
|
||||
.updateTable('asset_faces')
|
||||
.set({ personId: newPersonId })
|
||||
.where({ id: assetFaceId })
|
||||
.execute();
|
||||
.where('asset_faces.id', '=', assetFaceId)
|
||||
.executeTakeFirst();
|
||||
|
||||
return result.affected ?? 0;
|
||||
return Number(result.numChangedRows) ?? 0;
|
||||
}
|
||||
|
||||
getById(personId: string): Promise<PersonEntity | null> {
|
||||
return this.personRepository.findOne({ where: { id: personId } });
|
||||
return (this.db //
|
||||
.selectFrom('person')
|
||||
.selectAll('person')
|
||||
.where('person.id', '=', personId)
|
||||
.executeTakeFirst() ?? null) as Promise<PersonEntity | null>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, { withHidden: true }] })
|
||||
getByName(userId: string, personName: string, { withHidden }: PersonNameSearchOptions): Promise<PersonEntity[]> {
|
||||
const queryBuilder = this.personRepository
|
||||
.createQueryBuilder('person')
|
||||
.where(
|
||||
'person.ownerId = :userId AND (LOWER(person.name) LIKE :nameStart OR LOWER(person.name) LIKE :nameAnywhere)',
|
||||
{ userId, nameStart: `${personName.toLowerCase()}%`, nameAnywhere: `% ${personName.toLowerCase()}%` },
|
||||
return this.db
|
||||
.selectFrom('person')
|
||||
.selectAll('person')
|
||||
.where((eb) =>
|
||||
eb.and([
|
||||
eb('person.ownerId', '=', userId),
|
||||
eb.or([
|
||||
eb(eb.fn('lower', ['person.name']), 'like', `${personName.toLowerCase()}%`),
|
||||
eb(eb.fn('lower', ['person.name']), 'like', `% ${personName.toLowerCase()}%`),
|
||||
]),
|
||||
]),
|
||||
)
|
||||
.limit(1000);
|
||||
|
||||
if (!withHidden) {
|
||||
queryBuilder.andWhere('person.isHidden = false');
|
||||
}
|
||||
return queryBuilder.getMany();
|
||||
.limit(1000)
|
||||
.$if(!withHidden, (qb) => qb.where('person.isHidden', '=', false))
|
||||
.execute() as Promise<PersonEntity[]>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, { withHidden: true }] })
|
||||
getDistinctNames(userId: string, { withHidden }: PersonNameSearchOptions): Promise<PersonNameResponse[]> {
|
||||
const queryBuilder = this.personRepository
|
||||
.createQueryBuilder('person')
|
||||
return this.db
|
||||
.selectFrom('person')
|
||||
.select(['person.id', 'person.name'])
|
||||
.distinctOn(['lower(person.name)'])
|
||||
.where(`person.ownerId = :userId AND person.name != ''`, { userId });
|
||||
|
||||
if (!withHidden) {
|
||||
queryBuilder.andWhere('person.isHidden = false');
|
||||
}
|
||||
|
||||
return queryBuilder.getMany();
|
||||
.distinctOn((eb) => eb.fn('lower', ['person.name']))
|
||||
.where((eb) => eb.and([eb('person.ownerId', '=', userId), eb('person.name', '!=', '')]))
|
||||
.$if(!withHidden, (qb) => qb.where('person.isHidden', '=', false))
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getStatistics(personId: string): Promise<PersonStatistics> {
|
||||
const items = await this.assetFaceRepository
|
||||
.createQueryBuilder('face')
|
||||
.leftJoin('face.asset', 'asset')
|
||||
.where('face.personId = :personId', { personId })
|
||||
.andWhere('asset.isArchived = false')
|
||||
.andWhere('asset.deletedAt IS NULL')
|
||||
.andWhere('asset.livePhotoVideoId IS NULL')
|
||||
.select('COUNT(DISTINCT(asset.id))', 'count')
|
||||
.getRawOne();
|
||||
const result = await this.db
|
||||
.selectFrom('asset_faces')
|
||||
.leftJoin('assets', (join) =>
|
||||
join
|
||||
.onRef('assets.id', '=', 'asset_faces.assetId')
|
||||
.on('asset_faces.personId', '=', personId)
|
||||
.on('assets.isArchived', '=', false)
|
||||
.on('assets.deletedAt', 'is', null)
|
||||
.on('assets.livePhotoVideoId', 'is', null),
|
||||
)
|
||||
.select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count'))
|
||||
.executeTakeFirst();
|
||||
|
||||
return {
|
||||
assets: items.count ?? 0,
|
||||
assets: result ? Number(result.count) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getNumberOfPeople(userId: string): Promise<PeopleStatistics> {
|
||||
const items = await this.personRepository
|
||||
.createQueryBuilder('person')
|
||||
.innerJoin('person.faces', 'face')
|
||||
.where('person.ownerId = :userId', { userId })
|
||||
.innerJoin('face.asset', 'asset')
|
||||
.andWhere('asset.isArchived = false')
|
||||
.select('COUNT(DISTINCT(person.id))', 'total')
|
||||
.addSelect('COUNT(DISTINCT(person.id)) FILTER (WHERE person.isHidden = true)', 'hidden')
|
||||
.getRawOne();
|
||||
const items = await this.db
|
||||
.selectFrom('person')
|
||||
.innerJoin('asset_faces', 'asset_faces.personId', 'person.id')
|
||||
.where('person.ownerId', '=', userId)
|
||||
.innerJoin('assets', (join) =>
|
||||
join
|
||||
.onRef('assets.id', '=', 'asset_faces.assetId')
|
||||
.on('assets.deletedAt', 'is', null)
|
||||
.on('assets.isArchived', '=', false),
|
||||
)
|
||||
.select((eb) => eb.fn.count(eb.fn('distinct', ['person.id'])).as('total'))
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
.count(eb.fn('distinct', ['person.id']))
|
||||
.filterWhere('person.isHidden', '=', true)
|
||||
.as('hidden'),
|
||||
)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (items == undefined) {
|
||||
return { total: 0, hidden: 0 };
|
||||
}
|
||||
|
||||
const result: PeopleStatistics = {
|
||||
total: items.total ?? 0,
|
||||
hidden: items.hidden ?? 0,
|
||||
return {
|
||||
total: Number(items.total),
|
||||
hidden: Number(items.hidden),
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
create(person: Partial<PersonEntity>): Promise<PersonEntity> {
|
||||
return this.save(person);
|
||||
create(person: Insertable<Person>): Promise<PersonEntity> {
|
||||
return this.db.insertInto('person').values(person).returningAll().executeTakeFirst() as Promise<PersonEntity>;
|
||||
}
|
||||
|
||||
async createAll(people: Partial<PersonEntity>[]): Promise<string[]> {
|
||||
const results = await this.personRepository.save(people);
|
||||
return results.map((person) => person.id);
|
||||
async createAll(people: Insertable<Person>[]): Promise<string[]> {
|
||||
const results = await this.db.insertInto('person').values(people).returningAll().execute();
|
||||
return results.map(({ id }) => id);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[], [], [{ faceId: DummyValue.UUID, embedding: DummyValue.VECTOR }]] })
|
||||
async refreshFaces(
|
||||
facesToAdd: Partial<AssetFaceEntity>[],
|
||||
facesToAdd: (Insertable<AssetFaces> & { assetId: string })[],
|
||||
faceIdsToRemove: string[],
|
||||
embeddingsToAdd?: FaceSearchEntity[],
|
||||
embeddingsToAdd?: Insertable<FaceSearch>[],
|
||||
): Promise<void> {
|
||||
const query = this.faceSearchRepository.createQueryBuilder().select('1').fromDummy();
|
||||
let query = this.db;
|
||||
if (facesToAdd.length > 0) {
|
||||
const insertCte = this.assetFaceRepository.createQueryBuilder().insert().values(facesToAdd);
|
||||
query.addCommonTableExpression(insertCte, 'added');
|
||||
(query as any) = query.with('added', (db) => db.insertInto('asset_faces').values(facesToAdd));
|
||||
}
|
||||
|
||||
if (faceIdsToRemove.length > 0) {
|
||||
const deleteCte = this.assetFaceRepository
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.where('id = any(:faceIdsToRemove)', { faceIdsToRemove });
|
||||
query.addCommonTableExpression(deleteCte, 'deleted');
|
||||
(query as any) = query.with('removed', (db) =>
|
||||
db.deleteFrom('asset_faces').where('asset_faces.id', '=', (eb) => eb.fn.any(eb.val(faceIdsToRemove))),
|
||||
);
|
||||
}
|
||||
|
||||
if (embeddingsToAdd?.length) {
|
||||
const embeddingCte = this.faceSearchRepository.createQueryBuilder().insert().values(embeddingsToAdd).orIgnore();
|
||||
query.addCommonTableExpression(embeddingCte, 'embeddings');
|
||||
query.getQuery(); // typeorm mixes up parameters without this
|
||||
(query as any) = query.with('added_embeddings', (db) => db.insertInto('face_search').values(embeddingsToAdd));
|
||||
}
|
||||
|
||||
await query.execute();
|
||||
await query.selectFrom(sql`(select 1)`.as('dummy')).execute();
|
||||
}
|
||||
|
||||
async update(person: Partial<PersonEntity>): Promise<PersonEntity> {
|
||||
return this.save(person);
|
||||
async update(person: Partial<PersonEntity> & { id: string }): Promise<PersonEntity> {
|
||||
return this.db
|
||||
.updateTable('person')
|
||||
.set(person)
|
||||
.where('person.id', '=', person.id)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow() as Promise<PersonEntity>;
|
||||
}
|
||||
|
||||
async updateAll(people: Partial<PersonEntity>[]): Promise<void> {
|
||||
await this.personRepository.save(people);
|
||||
async updateAll(people: Insertable<Person>[]): Promise<void> {
|
||||
if (people.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.db
|
||||
.insertInto('person')
|
||||
.values(people)
|
||||
.onConflict((oc) => oc.column('id').doUpdateSet(() => mapUpsertColumns('person', people[0], ['id'])))
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[{ assetId: DummyValue.UUID, personId: DummyValue.UUID }]] })
|
||||
@ChunkedArray()
|
||||
async getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]> {
|
||||
return this.assetFaceRepository.find({ where: ids, relations: { asset: true }, withDeleted: true });
|
||||
getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]> {
|
||||
const { assetIds, personIds }: { assetIds: string[]; personIds: string[] } = { assetIds: [], personIds: [] };
|
||||
|
||||
for (const { assetId, personId } of ids) {
|
||||
assetIds.push(assetId);
|
||||
personIds.push(personId);
|
||||
}
|
||||
|
||||
return this.db
|
||||
.selectFrom('asset_faces')
|
||||
.selectAll('asset_faces')
|
||||
.select(withAsset)
|
||||
.select(withPerson)
|
||||
.where('asset_faces.assetId', 'in', assetIds)
|
||||
.where('asset_faces.personId', 'in', personIds)
|
||||
.execute() as Promise<AssetFaceEntity[]>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getRandomFace(personId: string): Promise<AssetFaceEntity | null> {
|
||||
return this.assetFaceRepository.findOneBy({ personId });
|
||||
getRandomFace(personId: string): Promise<AssetFaceEntity | null> {
|
||||
return (this.db
|
||||
.selectFrom('asset_faces')
|
||||
.selectAll('asset_faces')
|
||||
.where('asset_faces.personId', '=', personId)
|
||||
.executeTakeFirst() ?? null) as Promise<AssetFaceEntity | null>;
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
async getLatestFaceDate(): Promise<string | undefined> {
|
||||
const result: { latestDate?: string } | undefined = await this.jobStatusRepository
|
||||
.createQueryBuilder('jobStatus')
|
||||
.select('MAX(jobStatus.facesRecognizedAt)::text', 'latestDate')
|
||||
.getRawOne();
|
||||
const result = (await this.db
|
||||
.selectFrom('asset_job_status')
|
||||
.select((eb) => sql`${eb.fn.max('asset_job_status.facesRecognizedAt')}::text`.as('latestDate'))
|
||||
.executeTakeFirst()) as { latestDate: string } | undefined;
|
||||
|
||||
return result?.latestDate;
|
||||
}
|
||||
|
||||
private async save(person: Partial<PersonEntity>): Promise<PersonEntity> {
|
||||
const { id } = await this.personRepository.save(person);
|
||||
return this.personRepository.findOneByOrFail({ id });
|
||||
}
|
||||
|
||||
private async vacuum({ reindexVectors }: { reindexVectors: boolean }): Promise<void> {
|
||||
await this.assetFaceRepository.query('VACUUM ANALYZE asset_faces, face_search, person');
|
||||
await this.assetFaceRepository.query('REINDEX TABLE asset_faces');
|
||||
await this.assetFaceRepository.query('REINDEX TABLE person');
|
||||
await sql`VACUUM ANALYZE asset_faces, face_search, person`.execute(this.db);
|
||||
await sql`REINDEX TABLE asset_faces`.execute(this.db);
|
||||
await sql`REINDEX TABLE person`.execute(this.db);
|
||||
if (reindexVectors) {
|
||||
await this.assetFaceRepository.query('REINDEX TABLE face_search');
|
||||
await sql`REINDEX TABLE face_search`.execute(this.db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
SearchPaginationOptions,
|
||||
SmartSearchOptions,
|
||||
} from 'src/interfaces/search.interface';
|
||||
import { anyUuid, asUuid, asVector } from 'src/utils/database';
|
||||
import { anyUuid, asUuid } from 'src/utils/database';
|
||||
import { Paginated } from 'src/utils/pagination';
|
||||
import { isValidInteger } from 'src/validation';
|
||||
|
||||
@ -82,7 +82,7 @@ export class SearchRepository implements ISearchRepository {
|
||||
{ page: 1, size: 200 },
|
||||
{
|
||||
takenAfter: DummyValue.DATE,
|
||||
embedding: Array.from({ length: 512 }, Math.random),
|
||||
embedding: DummyValue.VECTOR,
|
||||
lensModel: DummyValue.STRING,
|
||||
withStacked: true,
|
||||
isFavorite: true,
|
||||
@ -97,7 +97,7 @@ export class SearchRepository implements ISearchRepository {
|
||||
|
||||
const items = (await searchAssetBuilder(this.db, options)
|
||||
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||
.orderBy(sql`smart_search.embedding <=> ${asVector(options.embedding)}`)
|
||||
.orderBy(sql`smart_search.embedding <=> ${options.embedding}`)
|
||||
.limit(pagination.size + 1)
|
||||
.offset((pagination.page - 1) * pagination.size)
|
||||
.execute()) as any as AssetEntity[];
|
||||
@ -111,7 +111,7 @@ export class SearchRepository implements ISearchRepository {
|
||||
params: [
|
||||
{
|
||||
assetId: DummyValue.UUID,
|
||||
embedding: Array.from({ length: 512 }, Math.random),
|
||||
embedding: DummyValue.VECTOR,
|
||||
maxDistance: 0.6,
|
||||
type: AssetType.IMAGE,
|
||||
userIds: [DummyValue.UUID],
|
||||
@ -119,7 +119,6 @@ export class SearchRepository implements ISearchRepository {
|
||||
],
|
||||
})
|
||||
searchDuplicates({ assetId, embedding, maxDistance, type, userIds }: AssetDuplicateSearch) {
|
||||
const vector = asVector(embedding);
|
||||
return this.db
|
||||
.with('cte', (qb) =>
|
||||
qb
|
||||
@ -127,7 +126,7 @@ export class SearchRepository implements ISearchRepository {
|
||||
.select([
|
||||
'assets.id as assetId',
|
||||
'assets.duplicateId',
|
||||
sql<number>`smart_search.embedding <=> ${vector}`.as('distance'),
|
||||
sql<number>`smart_search.embedding <=> ${embedding}`.as('distance'),
|
||||
])
|
||||
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||
.where('assets.ownerId', '=', anyUuid(userIds))
|
||||
@ -135,7 +134,7 @@ export class SearchRepository implements ISearchRepository {
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where('assets.type', '=', type)
|
||||
.where('assets.id', '!=', asUuid(assetId))
|
||||
.orderBy(sql`smart_search.embedding <=> ${vector}`)
|
||||
.orderBy(sql`smart_search.embedding <=> ${embedding}`)
|
||||
.limit(64),
|
||||
)
|
||||
.selectFrom('cte')
|
||||
@ -148,7 +147,7 @@ export class SearchRepository implements ISearchRepository {
|
||||
params: [
|
||||
{
|
||||
userIds: [DummyValue.UUID],
|
||||
embedding: Array.from({ length: 512 }, Math.random),
|
||||
embedding: DummyValue.VECTOR,
|
||||
numResults: 10,
|
||||
maxDistance: 0.6,
|
||||
},
|
||||
@ -159,7 +158,6 @@ export class SearchRepository implements ISearchRepository {
|
||||
throw new Error(`Invalid value for 'numResults': ${numResults}`);
|
||||
}
|
||||
|
||||
const vector = asVector(embedding);
|
||||
return this.db
|
||||
.with('cte', (qb) =>
|
||||
qb
|
||||
@ -167,14 +165,14 @@ export class SearchRepository implements ISearchRepository {
|
||||
.select([
|
||||
'asset_faces.id',
|
||||
'asset_faces.personId',
|
||||
sql<number>`face_search.embedding <=> ${vector}`.as('distance'),
|
||||
sql<number>`face_search.embedding <=> ${embedding}`.as('distance'),
|
||||
])
|
||||
.innerJoin('assets', 'assets.id', 'asset_faces.assetId')
|
||||
.innerJoin('face_search', 'face_search.faceId', 'asset_faces.id')
|
||||
.where('assets.ownerId', '=', anyUuid(userIds))
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.$if(!!hasPerson, (qb) => qb.where('asset_faces.personId', 'is not', null))
|
||||
.orderBy(sql`face_search.embedding <=> ${vector}`)
|
||||
.orderBy(sql`face_search.embedding <=> ${embedding}`)
|
||||
.limit(numResults),
|
||||
)
|
||||
.selectFrom('cte')
|
||||
@ -258,12 +256,11 @@ export class SearchRepository implements ISearchRepository {
|
||||
.execute() as any as Promise<AssetEntity[]>;
|
||||
}
|
||||
|
||||
async upsert(assetId: string, embedding: number[]): Promise<void> {
|
||||
const vector = asVector(embedding);
|
||||
async upsert(assetId: string, embedding: string): Promise<void> {
|
||||
await this.db
|
||||
.insertInto('smart_search')
|
||||
.values({ assetId: asUuid(assetId), embedding: vector } as any)
|
||||
.onConflict((oc) => oc.column('assetId').doUpdateSet({ embedding: vector } as any))
|
||||
.values({ assetId: asUuid(assetId), embedding } as any)
|
||||
.onConflict((oc) => oc.column('assetId').doUpdateSet({ embedding } as any))
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,9 @@ import { exec as execCallback } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { promisify } from 'node:util';
|
||||
import sharp from 'sharp';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
|
||||
const exec = promisify(execCallback);
|
||||
const maybeFirstLine = async (command: string): Promise<string> => {
|
||||
@ -36,7 +36,7 @@ const getLockfileVersion = (name: string, lockfile?: BuildLockfile) => {
|
||||
@Injectable()
|
||||
export class ServerInfoRepository implements IServerInfoRepository {
|
||||
constructor(
|
||||
@Inject(IConfigRepository) private configRepository: IConfigRepository,
|
||||
private configRepository: ConfigRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(ServerInfoRepository.name);
|
||||
|
@ -1,84 +1,113 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
import { ExpressionBuilder, Kysely, Updateable } from 'kysely';
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { StackEntity } from 'src/entities/stack.entity';
|
||||
import { IStackRepository, StackSearch } from 'src/interfaces/stack.interface';
|
||||
import { DataSource, In, Repository } from 'typeorm';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
|
||||
const withAssets = (eb: ExpressionBuilder<DB, 'asset_stack'>, withTags = false) => {
|
||||
return jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('assets')
|
||||
.selectAll()
|
||||
.$if(withTags, (eb) =>
|
||||
eb.select((eb) =>
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('tags')
|
||||
.selectAll('tags')
|
||||
.innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId')
|
||||
.whereRef('tag_asset.assetsId', '=', 'assets.id'),
|
||||
).as('tags'),
|
||||
),
|
||||
)
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.whereRef('assets.stackId', '=', 'asset_stack.id'),
|
||||
).as('assets');
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class StackRepository implements IStackRepository {
|
||||
constructor(
|
||||
@InjectDataSource() private dataSource: DataSource,
|
||||
@InjectRepository(StackEntity) private repository: Repository<StackEntity>,
|
||||
) {}
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [{ ownerId: DummyValue.UUID }] })
|
||||
search(query: StackSearch): Promise<StackEntity[]> {
|
||||
return this.repository.find({
|
||||
where: {
|
||||
ownerId: query.ownerId,
|
||||
primaryAssetId: query.primaryAssetId,
|
||||
},
|
||||
relations: {
|
||||
assets: {
|
||||
exifInfo: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
return this.db
|
||||
.selectFrom('asset_stack')
|
||||
.selectAll('asset_stack')
|
||||
.select(withAssets)
|
||||
.where('asset_stack.ownerId', '=', query.ownerId)
|
||||
.$if(!!query.primaryAssetId, (eb) => eb.where('asset_stack.primaryAssetId', '=', query.primaryAssetId!))
|
||||
.execute() as unknown as Promise<StackEntity[]>;
|
||||
}
|
||||
|
||||
async create(entity: { ownerId: string; assetIds: string[] }): Promise<StackEntity> {
|
||||
return this.dataSource.manager.transaction(async (manager) => {
|
||||
const stackRepository = manager.getRepository(StackEntity);
|
||||
|
||||
const stacks = await stackRepository.find({
|
||||
where: {
|
||||
ownerId: entity.ownerId,
|
||||
primaryAssetId: In(entity.assetIds),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
assets: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
assets: {
|
||||
exifInfo: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
const stacks = await tx
|
||||
.selectFrom('asset_stack')
|
||||
.where('asset_stack.ownerId', '=', entity.ownerId)
|
||||
.where('asset_stack.primaryAssetId', 'in', entity.assetIds)
|
||||
.select('asset_stack.id')
|
||||
.select((eb) =>
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('assets')
|
||||
.select('assets.id')
|
||||
.whereRef('assets.stackId', '=', 'asset_stack.id')
|
||||
.where('assets.deletedAt', 'is', null),
|
||||
).as('assets'),
|
||||
)
|
||||
.execute();
|
||||
|
||||
const assetIds = new Set<string>(entity.assetIds);
|
||||
|
||||
// children
|
||||
for (const stack of stacks) {
|
||||
for (const asset of stack.assets) {
|
||||
assetIds.add(asset.id);
|
||||
if (stack.assets && stack.assets.length > 0) {
|
||||
for (const asset of stack.assets) {
|
||||
assetIds.add(asset.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stacks.length > 0) {
|
||||
await stackRepository.delete({ id: In(stacks.map((stack) => stack.id)) });
|
||||
await tx
|
||||
.deleteFrom('asset_stack')
|
||||
.where(
|
||||
'id',
|
||||
'in',
|
||||
stacks.map((stack) => stack.id),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
const { id } = await stackRepository.save({
|
||||
ownerId: entity.ownerId,
|
||||
primaryAssetId: entity.assetIds[0],
|
||||
assets: [...assetIds].map((id) => ({ id }) as AssetEntity),
|
||||
});
|
||||
const newRecord = await tx
|
||||
.insertInto('asset_stack')
|
||||
.values({
|
||||
ownerId: entity.ownerId,
|
||||
primaryAssetId: entity.assetIds[0],
|
||||
})
|
||||
.returning('id')
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return stackRepository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
relations: {
|
||||
assets: {
|
||||
exifInfo: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
await tx
|
||||
.updateTable('assets')
|
||||
.set({
|
||||
stackId: newRecord.id,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where('id', 'in', [...assetIds])
|
||||
.execute();
|
||||
|
||||
return tx
|
||||
.selectFrom('asset_stack')
|
||||
.selectAll('asset_stack')
|
||||
.select(withAssets)
|
||||
.where('id', '=', newRecord.id)
|
||||
.executeTakeFirst() as unknown as Promise<StackEntity>;
|
||||
});
|
||||
}
|
||||
|
||||
@ -91,12 +120,12 @@ export class StackRepository implements IStackRepository {
|
||||
|
||||
const assetIds = stack.assets.map(({ id }) => id);
|
||||
|
||||
await this.repository.delete(id);
|
||||
|
||||
// Update assets updatedAt
|
||||
await this.dataSource.manager.update(AssetEntity, assetIds, {
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
await this.db.deleteFrom('asset_stack').where('id', '=', asUuid(id)).execute();
|
||||
await this.db
|
||||
.updateTable('assets')
|
||||
.set({ stackId: null, updatedAt: new Date() })
|
||||
.where('id', 'in', assetIds)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteAll(ids: string[]): Promise<void> {
|
||||
@ -110,54 +139,31 @@ export class StackRepository implements IStackRepository {
|
||||
assetIds.push(...stack.assets.map(({ id }) => id));
|
||||
}
|
||||
|
||||
await this.repository.delete(ids);
|
||||
|
||||
// Update assets updatedAt
|
||||
await this.dataSource.manager.update(AssetEntity, assetIds, {
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
await this.db
|
||||
.updateTable('assets')
|
||||
.set({ updatedAt: new Date(), stackId: null })
|
||||
.where('id', 'in', assetIds)
|
||||
.where('stackId', 'in', ids)
|
||||
.execute();
|
||||
}
|
||||
|
||||
update(entity: Partial<StackEntity>) {
|
||||
return this.save(entity);
|
||||
update(id: string, entity: Updateable<StackEntity>): Promise<StackEntity> {
|
||||
return this.db
|
||||
.updateTable('asset_stack')
|
||||
.set(entity)
|
||||
.where('id', '=', asUuid(id))
|
||||
.returningAll('asset_stack')
|
||||
.returning((eb) => withAssets(eb, true))
|
||||
.executeTakeFirstOrThrow() as unknown as Promise<StackEntity>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getById(id: string): Promise<StackEntity | null> {
|
||||
return this.repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
relations: {
|
||||
assets: {
|
||||
exifInfo: true,
|
||||
tags: true,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
assets: {
|
||||
fileCreatedAt: 'ASC',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async save(entity: Partial<StackEntity>) {
|
||||
const { id } = await this.repository.save(entity);
|
||||
return this.repository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
relations: {
|
||||
assets: {
|
||||
exifInfo: true,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
assets: {
|
||||
fileCreatedAt: 'ASC',
|
||||
},
|
||||
},
|
||||
});
|
||||
getById(id: string): Promise<StackEntity | undefined> {
|
||||
return this.db
|
||||
.selectFrom('asset_stack')
|
||||
.selectAll()
|
||||
.select((eb) => withAssets(eb, true))
|
||||
.where('id', '=', asUuid(id))
|
||||
.executeTakeFirst() as Promise<StackEntity | undefined>;
|
||||
}
|
||||
}
|
||||
|
@ -15,9 +15,9 @@ import { MetricService } from 'nestjs-otel';
|
||||
import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { ImmichTelemetry, MetadataKey } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMetricGroupRepository, ITelemetryRepository, MetricGroupOptions } from 'src/interfaces/telemetry.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
|
||||
class MetricGroupRepository implements IMetricGroupRepository {
|
||||
private enabled = false;
|
||||
@ -95,7 +95,7 @@ export class TelemetryRepository implements ITelemetryRepository {
|
||||
constructor(
|
||||
private metricService: MetricService,
|
||||
private reflect: Reflector,
|
||||
@Inject(IConfigRepository) private configRepository: IConfigRepository,
|
||||
private configRepository: ConfigRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
const { telemetry } = this.configRepository.getEnv();
|
||||
|
@ -2,15 +2,14 @@ import { Kysely } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetEntity, withExif } from 'src/entities/asset.entity';
|
||||
import { IViewRepository } from 'src/interfaces/view.interface';
|
||||
import { withExif } from 'src/entities/asset.entity';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
|
||||
export class ViewRepository implements IViewRepository {
|
||||
export class ViewRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getUniqueOriginalPaths(userId: string): Promise<string[]> {
|
||||
async getUniqueOriginalPaths(userId: string) {
|
||||
const results = await this.db
|
||||
.selectFrom('assets')
|
||||
.select((eb) => eb.fn<string>('substring', ['assets.originalPath', eb.val('^(.*/)[^/]*$')]).as('directoryPath'))
|
||||
@ -25,7 +24,7 @@ export class ViewRepository implements IViewRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
||||
async getAssetsByOriginalPath(userId: string, partialPath: string): Promise<AssetEntity[]> {
|
||||
async getAssetsByOriginalPath(userId: string, partialPath: string) {
|
||||
const normalizedPath = partialPath.replaceAll(/^\/|\/$/g, '');
|
||||
|
||||
return this.db
|
||||
@ -42,6 +41,6 @@ export class ViewRepository implements IViewRepository {
|
||||
(eb) => eb.fn('regexp_replace', ['assets.originalPath', eb.val('.*/(.+)'), eb.val(String.raw`\1`)]),
|
||||
'asc',
|
||||
)
|
||||
.execute() as any as Promise<AssetEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
@ -135,14 +135,17 @@ describe(AlbumService.name, () => {
|
||||
assetIds: ['123'],
|
||||
});
|
||||
|
||||
expect(albumMock.create).toHaveBeenCalledWith({
|
||||
ownerId: authStub.admin.user.id,
|
||||
albumName: albumStub.empty.albumName,
|
||||
description: albumStub.empty.description,
|
||||
albumUsers: [{ userId: 'user-id', role: AlbumUserRole.EDITOR }],
|
||||
assets: [{ id: '123' }],
|
||||
albumThumbnailAssetId: '123',
|
||||
});
|
||||
expect(albumMock.create).toHaveBeenCalledWith(
|
||||
{
|
||||
ownerId: authStub.admin.user.id,
|
||||
albumName: albumStub.empty.albumName,
|
||||
description: albumStub.empty.description,
|
||||
|
||||
albumThumbnailAssetId: '123',
|
||||
},
|
||||
['123'],
|
||||
[{ userId: 'user-id', role: AlbumUserRole.EDITOR }],
|
||||
);
|
||||
|
||||
expect(userMock.get).toHaveBeenCalledWith('user-id', {});
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123']));
|
||||
@ -175,14 +178,17 @@ describe(AlbumService.name, () => {
|
||||
assetIds: ['asset-1', 'asset-2'],
|
||||
});
|
||||
|
||||
expect(albumMock.create).toHaveBeenCalledWith({
|
||||
ownerId: authStub.admin.user.id,
|
||||
albumName: 'Test album',
|
||||
description: '',
|
||||
albumUsers: [],
|
||||
assets: [{ id: 'asset-1' }],
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
});
|
||||
expect(albumMock.create).toHaveBeenCalledWith(
|
||||
{
|
||||
ownerId: authStub.admin.user.id,
|
||||
albumName: 'Test album',
|
||||
description: '',
|
||||
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
},
|
||||
['asset-1'],
|
||||
[],
|
||||
);
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set(['asset-1', 'asset-2']),
|
||||
@ -192,7 +198,7 @@ describe(AlbumService.name, () => {
|
||||
|
||||
describe('update', () => {
|
||||
it('should prevent updating an album that does not exist', async () => {
|
||||
albumMock.getById.mockResolvedValue(null);
|
||||
albumMock.getById.mockResolvedValue(void 0);
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.user1, 'invalid-id', {
|
||||
@ -238,7 +244,7 @@ describe(AlbumService.name, () => {
|
||||
});
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledTimes(1);
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-4', {
|
||||
id: 'album-4',
|
||||
albumName: 'new album name',
|
||||
});
|
||||
@ -344,7 +350,7 @@ describe(AlbumService.name, () => {
|
||||
describe('removeUser', () => {
|
||||
it('should require a valid album id', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1']));
|
||||
albumMock.getById.mockResolvedValue(null);
|
||||
albumMock.getById.mockResolvedValue(void 0);
|
||||
await expect(sut.removeUser(authStub.admin, 'album-1', 'user-1')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
});
|
||||
@ -529,7 +535,7 @@ describe(AlbumService.name, () => {
|
||||
{ success: true, id: 'asset-3' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-123', {
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
@ -547,7 +553,7 @@ describe(AlbumService.name, () => {
|
||||
{ success: true, id: 'asset-1' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-123', {
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: 'asset-id',
|
||||
@ -569,7 +575,7 @@ describe(AlbumService.name, () => {
|
||||
{ success: true, id: 'asset-3' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-123', {
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
@ -606,7 +612,7 @@ describe(AlbumService.name, () => {
|
||||
{ success: true, id: 'asset-3' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-123', {
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
@ -629,7 +635,7 @@ describe(AlbumService.name, () => {
|
||||
{ success: true, id: 'asset-1' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-123', {
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
@ -696,7 +702,6 @@ describe(AlbumService.name, () => {
|
||||
{ success: true, id: 'asset-id' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date) });
|
||||
expect(albumMock.removeAssetIds).toHaveBeenCalledWith('album-123', ['asset-id']);
|
||||
});
|
||||
|
||||
@ -720,8 +725,6 @@ describe(AlbumService.name, () => {
|
||||
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||
{ success: true, id: 'asset-id' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date) });
|
||||
});
|
||||
|
||||
it('should reset the thumbnail if it is removed', async () => {
|
||||
@ -734,10 +737,6 @@ describe(AlbumService.name, () => {
|
||||
{ success: true, id: 'asset-id' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
});
|
||||
expect(albumMock.updateThumbnails).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -15,7 +15,6 @@ import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AlbumUserEntity } from 'src/entities/album-user.entity';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { Permission } from 'src/enum';
|
||||
import { AlbumAssetCount, AlbumInfoOptions } from 'src/interfaces/album.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
@ -112,16 +111,18 @@ export class AlbumService extends BaseService {
|
||||
permission: Permission.ASSET_SHARE,
|
||||
ids: dto.assetIds || [],
|
||||
});
|
||||
const assets = [...allowedAssetIdsSet].map((id) => ({ id }) as AssetEntity);
|
||||
const assetIds = [...allowedAssetIdsSet].map((id) => id);
|
||||
|
||||
const album = await this.albumRepository.create({
|
||||
ownerId: auth.user.id,
|
||||
albumName: dto.albumName,
|
||||
description: dto.description,
|
||||
albumUsers: albumUsers.map((albumUser) => albumUser as AlbumUserEntity) ?? [],
|
||||
assets,
|
||||
albumThumbnailAssetId: assets[0]?.id || null,
|
||||
});
|
||||
const album = await this.albumRepository.create(
|
||||
{
|
||||
ownerId: auth.user.id,
|
||||
albumName: dto.albumName,
|
||||
description: dto.description,
|
||||
albumThumbnailAssetId: assetIds[0] || null,
|
||||
},
|
||||
assetIds,
|
||||
albumUsers,
|
||||
);
|
||||
|
||||
for (const { userId } of albumUsers) {
|
||||
await this.eventRepository.emit('album.invite', { id: album.id, userId });
|
||||
@ -141,7 +142,7 @@ export class AlbumService extends BaseService {
|
||||
throw new BadRequestException('Invalid album thumbnail');
|
||||
}
|
||||
}
|
||||
const updatedAlbum = await this.albumRepository.update({
|
||||
const updatedAlbum = await this.albumRepository.update(album.id, {
|
||||
id: album.id,
|
||||
albumName: dto.albumName,
|
||||
description: dto.description,
|
||||
@ -170,7 +171,7 @@ export class AlbumService extends BaseService {
|
||||
|
||||
const { id: firstNewAssetId } = results.find(({ success }) => success) || {};
|
||||
if (firstNewAssetId) {
|
||||
await this.albumRepository.update({
|
||||
await this.albumRepository.update(id, {
|
||||
id,
|
||||
updatedAt: new Date(),
|
||||
albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId,
|
||||
@ -199,11 +200,8 @@ export class AlbumService extends BaseService {
|
||||
);
|
||||
|
||||
const removedIds = results.filter(({ success }) => success).map(({ id }) => id);
|
||||
if (removedIds.length > 0) {
|
||||
await this.albumRepository.update({ id, updatedAt: new Date() });
|
||||
if (album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) {
|
||||
await this.albumRepository.updateThumbnails();
|
||||
}
|
||||
if (removedIds.length > 0 && album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) {
|
||||
await this.albumRepository.updateThumbnails();
|
||||
}
|
||||
|
||||
return results;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Permission } from 'src/enum';
|
||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { APIKeyService } from 'src/services/api-key.service';
|
||||
import { IApiKeyRepository } from 'src/types';
|
||||
import { keyStub } from 'test/fixtures/api-key.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
@ -12,7 +12,7 @@ describe(APIKeyService.name, () => {
|
||||
let sut: APIKeyService;
|
||||
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let keyMock: Mocked<IKeyRepository>;
|
||||
let keyMock: Mocked<IApiKeyRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, cryptoMock, keyMock } = newTestService(APIKeyService));
|
||||
@ -56,8 +56,6 @@ describe(APIKeyService.name, () => {
|
||||
|
||||
describe('update', () => {
|
||||
it('should throw an error if the key is not found', async () => {
|
||||
keyMock.getById.mockResolvedValue(null);
|
||||
|
||||
await expect(sut.update(authStub.admin, 'random-guid', { name: 'New Name' })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
@ -77,8 +75,6 @@ describe(APIKeyService.name, () => {
|
||||
|
||||
describe('delete', () => {
|
||||
it('should throw an error if the key is not found', async () => {
|
||||
keyMock.getById.mockResolvedValue(null);
|
||||
|
||||
await expect(sut.delete(authStub.admin, 'random-guid')).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(keyMock.delete).not.toHaveBeenCalledWith('random-guid');
|
||||
@ -95,8 +91,6 @@ describe(APIKeyService.name, () => {
|
||||
|
||||
describe('getById', () => {
|
||||
it('should throw an error if the key is not found', async () => {
|
||||
keyMock.getById.mockResolvedValue(null);
|
||||
|
||||
await expect(sut.getById(authStub.admin, 'random-guid')).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(keyMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid');
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { APIKeyEntity } from 'src/entities/api-key.entity';
|
||||
import { Permission } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { ApiKeyItem } from 'src/types';
|
||||
import { isGranted } from 'src/utils/access';
|
||||
|
||||
@Injectable()
|
||||
@ -57,13 +58,13 @@ export class APIKeyService extends BaseService {
|
||||
return keys.map((key) => this.map(key));
|
||||
}
|
||||
|
||||
private map(entity: APIKeyEntity): APIKeyResponseDto {
|
||||
private map(entity: ApiKeyItem): APIKeyResponseDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
createdAt: entity.createdAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
permissions: entity.permissions,
|
||||
permissions: entity.permissions as Permission[],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ import { Cron, CronExpression, Interval } from '@nestjs/schedule';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { ONE_HOUR } from 'src/constants';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||
@ -38,7 +38,7 @@ export class ApiService {
|
||||
private jobService: JobService,
|
||||
private sharedLinkService: SharedLinkService,
|
||||
private versionService: VersionService,
|
||||
@Inject(IConfigRepository) private configRepository: IConfigRepository,
|
||||
private configRepository: ConfigRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(ApiService.name);
|
||||
|
@ -520,7 +520,7 @@ describe(AssetService.name, () => {
|
||||
|
||||
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
|
||||
|
||||
expect(stackMock.update).toHaveBeenCalledWith({
|
||||
expect(stackMock.update).toHaveBeenCalledWith('stack-1', {
|
||||
id: 'stack-1',
|
||||
primaryAssetId: 'stack-child-asset-1',
|
||||
});
|
||||
|
@ -192,7 +192,7 @@ export class AssetService extends BaseService {
|
||||
const stackAssetIds = asset.stack.assets.map((a) => a.id);
|
||||
if (stackAssetIds.length > 2) {
|
||||
const newPrimaryAssetId = stackAssetIds.find((a) => a !== id)!;
|
||||
await this.stackRepository.update({
|
||||
await this.stackRepository.update(asset.stack.id, {
|
||||
id: asset.stack.id,
|
||||
primaryAssetId: newPrimaryAssetId,
|
||||
});
|
||||
|
@ -2,12 +2,12 @@ import { BadRequestException } from '@nestjs/common';
|
||||
import { FileReportItemDto } from 'src/dtos/audit.dto';
|
||||
import { AssetFileType, AssetPathType, DatabaseAction, EntityType, PersonPathType, UserPathType } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IAuditRepository } from 'src/interfaces/audit.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { JobStatus } from 'src/interfaces/job.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AuditService } from 'src/services/audit.service';
|
||||
import { IAuditRepository } from 'src/types';
|
||||
import { auditStub } from 'test/fixtures/audit.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
|
@ -201,21 +201,22 @@ export class AuditService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
const personPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
||||
this.personRepository.getAll(pagination),
|
||||
);
|
||||
for await (const people of personPagination) {
|
||||
for (const { id, thumbnailPath } of people) {
|
||||
track(thumbnailPath);
|
||||
const entity = { entityId: id, entityType: PathEntityType.PERSON };
|
||||
if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) {
|
||||
orphans.push({ ...entity, pathType: PersonPathType.FACE, pathValue: thumbnailPath });
|
||||
}
|
||||
let peopleCount = 0;
|
||||
for await (const { id, thumbnailPath } of this.personRepository.getAll()) {
|
||||
track(thumbnailPath);
|
||||
const entity = { entityId: id, entityType: PathEntityType.PERSON };
|
||||
if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) {
|
||||
orphans.push({ ...entity, pathType: PersonPathType.FACE, pathValue: thumbnailPath });
|
||||
}
|
||||
|
||||
this.logger.log(`Found ${assetCount} assets, ${users.length} users, ${people.length} people`);
|
||||
if (peopleCount === JOBS_ASSET_PAGINATION_SIZE) {
|
||||
this.logger.log(`Found ${assetCount} assets, ${users.length} users, ${peopleCount} people`);
|
||||
peopleCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`Found ${assetCount} assets, ${users.length} users, ${peopleCount} people`);
|
||||
|
||||
const extras: string[] = [];
|
||||
for (const file of allFiles) {
|
||||
extras.push(file);
|
||||
|
@ -3,7 +3,6 @@ import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
|
||||
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { AuthType, Permission } from 'src/enum';
|
||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IOAuthRepository } from 'src/interfaces/oauth.interface';
|
||||
@ -12,6 +11,7 @@ import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { IApiKeyRepository } from 'src/types';
|
||||
import { keyStub } from 'test/fixtures/api-key.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { sessionStub } from 'test/fixtures/session.stub';
|
||||
@ -62,7 +62,7 @@ describe('AuthService', () => {
|
||||
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let keyMock: Mocked<IKeyRepository>;
|
||||
let keyMock: Mocked<IApiKeyRepository>;
|
||||
let oauthMock: Mocked<IOAuthRepository>;
|
||||
let sessionMock: Mocked<ISessionRepository>;
|
||||
let sharedLinkMock: Mocked<ISharedLinkRepository>;
|
||||
|
@ -21,6 +21,7 @@ import { UserEntity } from 'src/entities/user.entity';
|
||||
import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, Permission } from 'src/enum';
|
||||
import { OAuthProfile } from 'src/interfaces/oauth.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { AuthApiKey } from 'src/types';
|
||||
import { isGranted } from 'src/utils/access';
|
||||
import { HumanReadableSize } from 'src/utils/bytes';
|
||||
|
||||
@ -309,7 +310,10 @@ export class AuthService extends BaseService {
|
||||
const hashedKey = this.cryptoRepository.hashSha256(key);
|
||||
const apiKey = await this.keyRepository.getKey(hashedKey);
|
||||
if (apiKey) {
|
||||
return { user: apiKey.user, apiKey };
|
||||
return {
|
||||
user: apiKey.user as unknown as UserEntity,
|
||||
apiKey: apiKey as unknown as AuthApiKey,
|
||||
};
|
||||
}
|
||||
|
||||
throw new UnauthorizedException('Invalid API key');
|
||||
|
@ -2,7 +2,6 @@ import { PassThrough } from 'node:stream';
|
||||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { ImmichWorker, StorageFolder } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICronRepository } from 'src/interfaces/cron.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { JobStatus } from 'src/interfaces/job.interface';
|
||||
@ -10,6 +9,7 @@ import { IProcessRepository } from 'src/interfaces/process.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { BackupService } from 'src/services/backup.service';
|
||||
import { IConfigRepository } from 'src/types';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { mockSpawn, newTestService } from 'test/utils';
|
||||
import { describe, Mocked } from 'vitest';
|
||||
|
@ -6,13 +6,9 @@ import { SALT_ROUNDS } from 'src/constants';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { Users } from 'src/db';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IAuditRepository } from 'src/interfaces/audit.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICronRepository } from 'src/interfaces/cron.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
@ -43,8 +39,12 @@ import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
|
||||
import { ITrashRepository } from 'src/interfaces/trash.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
|
||||
import { IViewRepository } from 'src/interfaces/view.interface';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
||||
import { AuditRepository } from 'src/repositories/audit.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { ViewRepository } from 'src/repositories/view-repository';
|
||||
import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access';
|
||||
import { getConfig, updateConfig } from 'src/utils/config';
|
||||
|
||||
@ -53,19 +53,19 @@ export class BaseService {
|
||||
|
||||
constructor(
|
||||
@Inject(ILoggerRepository) protected logger: ILoggerRepository,
|
||||
@Inject(IAccessRepository) protected accessRepository: IAccessRepository,
|
||||
protected accessRepository: AccessRepository,
|
||||
protected activityRepository: ActivityRepository,
|
||||
@Inject(IAuditRepository) protected auditRepository: IAuditRepository,
|
||||
protected auditRepository: AuditRepository,
|
||||
@Inject(IAlbumRepository) protected albumRepository: IAlbumRepository,
|
||||
@Inject(IAlbumUserRepository) protected albumUserRepository: IAlbumUserRepository,
|
||||
@Inject(IAssetRepository) protected assetRepository: IAssetRepository,
|
||||
@Inject(IConfigRepository) protected configRepository: IConfigRepository,
|
||||
protected configRepository: ConfigRepository,
|
||||
@Inject(ICronRepository) protected cronRepository: ICronRepository,
|
||||
@Inject(ICryptoRepository) protected cryptoRepository: ICryptoRepository,
|
||||
@Inject(IDatabaseRepository) protected databaseRepository: IDatabaseRepository,
|
||||
@Inject(IEventRepository) protected eventRepository: IEventRepository,
|
||||
@Inject(IJobRepository) protected jobRepository: IJobRepository,
|
||||
@Inject(IKeyRepository) protected keyRepository: IKeyRepository,
|
||||
protected keyRepository: ApiKeyRepository,
|
||||
@Inject(ILibraryRepository) protected libraryRepository: ILibraryRepository,
|
||||
@Inject(IMachineLearningRepository) protected machineLearningRepository: IMachineLearningRepository,
|
||||
@Inject(IMapRepository) protected mapRepository: IMapRepository,
|
||||
@ -90,7 +90,7 @@ export class BaseService {
|
||||
@Inject(ITrashRepository) protected trashRepository: ITrashRepository,
|
||||
@Inject(IUserRepository) protected userRepository: IUserRepository,
|
||||
@Inject(IVersionHistoryRepository) protected versionRepository: IVersionHistoryRepository,
|
||||
@Inject(IViewRepository) protected viewRepository: IViewRepository,
|
||||
protected viewRepository: ViewRepository,
|
||||
) {
|
||||
this.logger.setContext(this.constructor.name);
|
||||
this.storageCore = StorageCore.create(
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import {
|
||||
DatabaseExtension,
|
||||
EXTENSION_NAMES,
|
||||
@ -8,6 +7,7 @@ import {
|
||||
} from 'src/interfaces/database.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { DatabaseService } from 'src/services/database.service';
|
||||
import { IConfigRepository } from 'src/types';
|
||||
import { mockEnvData } from 'test/repositories/config.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
|
@ -2,11 +2,11 @@ import { BadRequestException } from '@nestjs/common';
|
||||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { ImmichWorker } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { IJobRepository, JobCommand, JobItem, JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
import { IConfigRepository } from 'src/types';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
|
@ -5,7 +5,6 @@ import { mapLibrary } from 'src/dtos/library.dto';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { AssetType, ImmichWorker } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICronRepository } from 'src/interfaces/cron.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import {
|
||||
@ -19,6 +18,7 @@ import {
|
||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { LibraryService } from 'src/services/library.service';
|
||||
import { IConfigRepository } from 'src/types';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { libraryStub } from 'test/fixtures/library.stub';
|
||||
|
@ -25,7 +25,7 @@ import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { faceStub } from 'test/fixtures/face.stub';
|
||||
import { probeStub } from 'test/fixtures/media.stub';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { makeStream, newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
describe(MediaService.name, () => {
|
||||
@ -55,10 +55,8 @@ describe(MediaService.name, () => {
|
||||
items: [assetStub.image],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAll.mockResolvedValue({
|
||||
items: [personStub.newThumbnail],
|
||||
hasNextPage: false,
|
||||
});
|
||||
|
||||
personMock.getAll.mockReturnValue(makeStream([personStub.newThumbnail]));
|
||||
personMock.getFacesByIds.mockResolvedValue([faceStub.face1]);
|
||||
|
||||
await sut.handleQueueGenerateThumbnails({ force: true });
|
||||
@ -72,7 +70,7 @@ describe(MediaService.name, () => {
|
||||
},
|
||||
]);
|
||||
|
||||
expect(personMock.getAll).toHaveBeenCalledWith({ skip: 0, take: 1000 }, {});
|
||||
expect(personMock.getAll).toHaveBeenCalledWith(undefined);
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.GENERATE_PERSON_THUMBNAIL,
|
||||
@ -86,10 +84,7 @@ describe(MediaService.name, () => {
|
||||
items: [assetStub.trashed],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAll.mockResolvedValue({
|
||||
items: [],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAll.mockReturnValue(makeStream());
|
||||
|
||||
await sut.handleQueueGenerateThumbnails({ force: true });
|
||||
|
||||
@ -111,10 +106,7 @@ describe(MediaService.name, () => {
|
||||
items: [assetStub.archived],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAll.mockResolvedValue({
|
||||
items: [],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAll.mockReturnValue(makeStream());
|
||||
|
||||
await sut.handleQueueGenerateThumbnails({ force: true });
|
||||
|
||||
@ -136,10 +128,7 @@ describe(MediaService.name, () => {
|
||||
items: [assetStub.image],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAll.mockResolvedValue({
|
||||
items: [personStub.noThumbnail, personStub.noThumbnail],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAll.mockReturnValue(makeStream([personStub.noThumbnail, personStub.noThumbnail]));
|
||||
personMock.getRandomFace.mockResolvedValueOnce(faceStub.face1);
|
||||
|
||||
await sut.handleQueueGenerateThumbnails({ force: false });
|
||||
@ -147,7 +136,7 @@ describe(MediaService.name, () => {
|
||||
expect(assetMock.getAll).not.toHaveBeenCalled();
|
||||
expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL);
|
||||
|
||||
expect(personMock.getAll).toHaveBeenCalledWith({ skip: 0, take: 1000 }, { where: { thumbnailPath: '' } });
|
||||
expect(personMock.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
||||
expect(personMock.getRandomFace).toHaveBeenCalled();
|
||||
expect(personMock.update).toHaveBeenCalledTimes(1);
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||
@ -165,11 +154,7 @@ describe(MediaService.name, () => {
|
||||
items: [assetStub.noResizePath],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAll.mockResolvedValue({
|
||||
items: [],
|
||||
hasNextPage: false,
|
||||
});
|
||||
|
||||
personMock.getAll.mockReturnValue(makeStream());
|
||||
await sut.handleQueueGenerateThumbnails({ force: false });
|
||||
|
||||
expect(assetMock.getAll).not.toHaveBeenCalled();
|
||||
@ -181,7 +166,7 @@ describe(MediaService.name, () => {
|
||||
},
|
||||
]);
|
||||
|
||||
expect(personMock.getAll).toHaveBeenCalledWith({ skip: 0, take: 1000 }, { where: { thumbnailPath: '' } });
|
||||
expect(personMock.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
||||
});
|
||||
|
||||
it('should queue all assets with missing webp path', async () => {
|
||||
@ -189,11 +174,7 @@ describe(MediaService.name, () => {
|
||||
items: [assetStub.noWebpPath],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAll.mockResolvedValue({
|
||||
items: [],
|
||||
hasNextPage: false,
|
||||
});
|
||||
|
||||
personMock.getAll.mockReturnValue(makeStream());
|
||||
await sut.handleQueueGenerateThumbnails({ force: false });
|
||||
|
||||
expect(assetMock.getAll).not.toHaveBeenCalled();
|
||||
@ -205,7 +186,7 @@ describe(MediaService.name, () => {
|
||||
},
|
||||
]);
|
||||
|
||||
expect(personMock.getAll).toHaveBeenCalledWith({ skip: 0, take: 1000 }, { where: { thumbnailPath: '' } });
|
||||
expect(personMock.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
||||
});
|
||||
|
||||
it('should queue all assets with missing thumbhash', async () => {
|
||||
@ -213,11 +194,7 @@ describe(MediaService.name, () => {
|
||||
items: [assetStub.noThumbhash],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAll.mockResolvedValue({
|
||||
items: [],
|
||||
hasNextPage: false,
|
||||
});
|
||||
|
||||
personMock.getAll.mockReturnValue(makeStream());
|
||||
await sut.handleQueueGenerateThumbnails({ force: false });
|
||||
|
||||
expect(assetMock.getAll).not.toHaveBeenCalled();
|
||||
@ -229,7 +206,7 @@ describe(MediaService.name, () => {
|
||||
},
|
||||
]);
|
||||
|
||||
expect(personMock.getAll).toHaveBeenCalledWith({ skip: 0, take: 1000 }, { where: { thumbnailPath: '' } });
|
||||
expect(personMock.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
||||
});
|
||||
});
|
||||
|
||||
@ -237,7 +214,7 @@ describe(MediaService.name, () => {
|
||||
it('should remove empty directories and queue jobs', async () => {
|
||||
assetMock.getAll.mockResolvedValue({ hasNextPage: false, items: [assetStub.image] });
|
||||
jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0 } as JobCounts);
|
||||
personMock.getAll.mockResolvedValue({ hasNextPage: false, items: [personStub.withName] });
|
||||
personMock.getAll.mockReturnValue(makeStream([personStub.withName]));
|
||||
|
||||
await expect(sut.handleQueueMigration()).resolves.toBe(JobStatus.SUCCESS);
|
||||
|
||||
@ -730,10 +707,7 @@ describe(MediaService.name, () => {
|
||||
items: [assetStub.video],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAll.mockResolvedValue({
|
||||
items: [],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAll.mockReturnValue(makeStream());
|
||||
|
||||
await sut.handleQueueVideoConversion({ force: true });
|
||||
|
||||
|
@ -72,23 +72,20 @@ export class MediaService extends BaseService {
|
||||
}
|
||||
|
||||
const jobs: JobItem[] = [];
|
||||
const personPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
||||
this.personRepository.getAll(pagination, { where: force ? undefined : { thumbnailPath: '' } }),
|
||||
);
|
||||
|
||||
for await (const people of personPagination) {
|
||||
for (const person of people) {
|
||||
if (!person.faceAssetId) {
|
||||
const face = await this.personRepository.getRandomFace(person.id);
|
||||
if (!face) {
|
||||
continue;
|
||||
}
|
||||
const people = this.personRepository.getAll(force ? undefined : { thumbnailPath: '' });
|
||||
|
||||
await this.personRepository.update({ id: person.id, faceAssetId: face.id });
|
||||
for await (const person of people) {
|
||||
if (!person.faceAssetId) {
|
||||
const face = await this.personRepository.getRandomFace(person.id);
|
||||
if (!face) {
|
||||
continue;
|
||||
}
|
||||
|
||||
jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: person.id } });
|
||||
await this.personRepository.update({ id: person.id, faceAssetId: face.id });
|
||||
}
|
||||
|
||||
jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: person.id } });
|
||||
}
|
||||
|
||||
await this.jobRepository.queueAll(jobs);
|
||||
@ -114,16 +111,19 @@ export class MediaService extends BaseService {
|
||||
);
|
||||
}
|
||||
|
||||
const personPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
||||
this.personRepository.getAll(pagination),
|
||||
);
|
||||
let jobs: { name: JobName.MIGRATE_PERSON; data: { id: string } }[] = [];
|
||||
|
||||
for await (const people of personPagination) {
|
||||
await this.jobRepository.queueAll(
|
||||
people.map((person) => ({ name: JobName.MIGRATE_PERSON, data: { id: person.id } })),
|
||||
);
|
||||
for await (const person of this.personRepository.getAll()) {
|
||||
jobs.push({ name: JobName.MIGRATE_PERSON, data: { id: person.id } });
|
||||
|
||||
if (jobs.length === JOBS_ASSET_PAGINATION_SIZE) {
|
||||
await this.jobRepository.queueAll(jobs);
|
||||
jobs = [];
|
||||
}
|
||||
}
|
||||
|
||||
await this.jobRepository.queueAll(jobs);
|
||||
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { AssetType, ExifOrientation, ImmichWorker, SourceType } from 'src/enum';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
@ -20,6 +19,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
|
||||
import { ITagRepository } from 'src/interfaces/tag.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { MetadataService } from 'src/services/metadata.service';
|
||||
import { IConfigRepository } from 'src/types';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { fileStub } from 'test/fixtures/file.stub';
|
||||
import { probeStub } from 'test/fixtures/media.stub';
|
||||
@ -1086,7 +1086,9 @@ describe(MetadataService.name, () => {
|
||||
],
|
||||
[],
|
||||
);
|
||||
expect(personMock.updateAll).toHaveBeenCalledWith([{ id: 'random-uuid', faceAssetId: 'random-uuid' }]);
|
||||
expect(personMock.updateAll).toHaveBeenCalledWith([
|
||||
{ id: 'random-uuid', ownerId: 'admin-id', faceAssetId: 'random-uuid' },
|
||||
]);
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.GENERATE_PERSON_THUMBNAIL,
|
||||
|
@ -509,11 +509,11 @@ export class MetadataService extends BaseService {
|
||||
return;
|
||||
}
|
||||
|
||||
const facesToAdd: Partial<AssetFaceEntity>[] = [];
|
||||
const facesToAdd: (Partial<AssetFaceEntity> & { assetId: string })[] = [];
|
||||
const existingNames = await this.personRepository.getDistinctNames(asset.ownerId, { withHidden: true });
|
||||
const existingNameMap = new Map(existingNames.map(({ id, name }) => [name.toLowerCase(), id]));
|
||||
const missing: Partial<PersonEntity>[] = [];
|
||||
const missingWithFaceAsset: Partial<PersonEntity>[] = [];
|
||||
const missing: (Partial<PersonEntity> & { ownerId: string })[] = [];
|
||||
const missingWithFaceAsset: { id: string; ownerId: string; faceAssetId: string }[] = [];
|
||||
for (const region of tags.RegionInfo.RegionList) {
|
||||
if (!region.Name) {
|
||||
continue;
|
||||
@ -540,7 +540,7 @@ export class MetadataService extends BaseService {
|
||||
facesToAdd.push(face);
|
||||
if (!existingNameMap.has(loweredName)) {
|
||||
missing.push({ id: personId, ownerId: asset.ownerId, name: region.Name });
|
||||
missingWithFaceAsset.push({ id: personId, faceAssetId: face.id });
|
||||
missingWithFaceAsset.push({ id: personId, ownerId: asset.ownerId, faceAssetId: face.id });
|
||||
}
|
||||
}
|
||||
|
||||
@ -557,7 +557,7 @@ export class MetadataService extends BaseService {
|
||||
}
|
||||
|
||||
if (facesToAdd.length > 0) {
|
||||
this.logger.debug(`Creating ${facesToAdd} faces from metadata for asset ${asset.id}`);
|
||||
this.logger.debug(`Creating ${facesToAdd.length} faces from metadata for asset ${asset.id}`);
|
||||
}
|
||||
|
||||
if (facesToRemove.length > 0 || facesToAdd.length > 0) {
|
||||
|
@ -20,8 +20,7 @@ import { faceStub } from 'test/fixtures/face.stub';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { makeStream, newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
const responseDto: PersonResponseDto = {
|
||||
@ -46,7 +45,7 @@ const face = {
|
||||
imageHeight: 500,
|
||||
imageWidth: 400,
|
||||
};
|
||||
const faceSearch = { faceId, embedding: [1, 2, 3, 4] };
|
||||
const faceSearch = { faceId, embedding: '[1, 2, 3, 4]' };
|
||||
const detectFaceMock: DetectedFaces = {
|
||||
faces: [
|
||||
{
|
||||
@ -495,14 +494,8 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should delete existing people and faces if forced', async () => {
|
||||
personMock.getAll.mockResolvedValue({
|
||||
items: [faceStub.face1.person, personStub.randomPerson],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAllFaces.mockResolvedValue({
|
||||
items: [faceStub.face1],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAll.mockReturnValue(makeStream([faceStub.face1.person, personStub.randomPerson]));
|
||||
personMock.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
|
||||
assetMock.getAll.mockResolvedValue({
|
||||
items: [assetStub.image],
|
||||
hasNextPage: false,
|
||||
@ -544,18 +537,12 @@ describe(PersonService.name, () => {
|
||||
|
||||
it('should queue missing assets', async () => {
|
||||
jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
|
||||
personMock.getAllFaces.mockResolvedValue({
|
||||
items: [faceStub.face1],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
|
||||
personMock.getAllWithoutFaces.mockResolvedValue([]);
|
||||
|
||||
await sut.handleQueueRecognizeFaces({});
|
||||
|
||||
expect(personMock.getAllFaces).toHaveBeenCalledWith(
|
||||
{ skip: 0, take: 1000 },
|
||||
{ where: { personId: IsNull(), sourceType: SourceType.MACHINE_LEARNING } },
|
||||
);
|
||||
expect(personMock.getAllFaces).toHaveBeenCalledWith({ personId: null, sourceType: SourceType.MACHINE_LEARNING });
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.FACIAL_RECOGNITION,
|
||||
@ -569,19 +556,13 @@ describe(PersonService.name, () => {
|
||||
|
||||
it('should queue all assets', async () => {
|
||||
jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
|
||||
personMock.getAll.mockResolvedValue({
|
||||
items: [],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAllFaces.mockResolvedValue({
|
||||
items: [faceStub.face1],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAll.mockReturnValue(makeStream());
|
||||
personMock.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
|
||||
personMock.getAllWithoutFaces.mockResolvedValue([]);
|
||||
|
||||
await sut.handleQueueRecognizeFaces({ force: true });
|
||||
|
||||
expect(personMock.getAllFaces).toHaveBeenCalledWith({ skip: 0, take: 1000 }, {});
|
||||
expect(personMock.getAllFaces).toHaveBeenCalledWith(undefined);
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.FACIAL_RECOGNITION,
|
||||
@ -595,26 +576,17 @@ describe(PersonService.name, () => {
|
||||
|
||||
it('should run nightly if new face has been added since last run', async () => {
|
||||
personMock.getLatestFaceDate.mockResolvedValue(new Date().toISOString());
|
||||
personMock.getAllFaces.mockResolvedValue({
|
||||
items: [faceStub.face1],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
|
||||
jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
|
||||
personMock.getAll.mockResolvedValue({
|
||||
items: [],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAllFaces.mockResolvedValue({
|
||||
items: [faceStub.face1],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAll.mockReturnValue(makeStream());
|
||||
personMock.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
|
||||
personMock.getAllWithoutFaces.mockResolvedValue([]);
|
||||
|
||||
await sut.handleQueueRecognizeFaces({ force: true, nightly: true });
|
||||
|
||||
expect(systemMock.get).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE);
|
||||
expect(personMock.getLatestFaceDate).toHaveBeenCalledOnce();
|
||||
expect(personMock.getAllFaces).toHaveBeenCalledWith({ skip: 0, take: 1000 }, {});
|
||||
expect(personMock.getAllFaces).toHaveBeenCalledWith(undefined);
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.FACIAL_RECOGNITION,
|
||||
@ -631,10 +603,7 @@ describe(PersonService.name, () => {
|
||||
|
||||
systemMock.get.mockResolvedValue({ lastRun: lastRun.toISOString() });
|
||||
personMock.getLatestFaceDate.mockResolvedValue(new Date(lastRun.getTime() - 1).toISOString());
|
||||
personMock.getAllFaces.mockResolvedValue({
|
||||
items: [faceStub.face1],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
|
||||
personMock.getAllWithoutFaces.mockResolvedValue([]);
|
||||
|
||||
await sut.handleQueueRecognizeFaces({ force: true, nightly: true });
|
||||
@ -648,15 +617,8 @@ describe(PersonService.name, () => {
|
||||
|
||||
it('should delete existing people if forced', async () => {
|
||||
jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
|
||||
personMock.getAll.mockResolvedValue({
|
||||
items: [faceStub.face1.person, personStub.randomPerson],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getAllFaces.mockResolvedValue({
|
||||
items: [faceStub.face1],
|
||||
hasNextPage: false,
|
||||
});
|
||||
|
||||
personMock.getAll.mockReturnValue(makeStream([faceStub.face1.person, personStub.randomPerson]));
|
||||
personMock.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
|
||||
personMock.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]);
|
||||
|
||||
await sut.handleQueueRecognizeFaces({ force: true });
|
||||
|
@ -50,7 +50,6 @@ import { ImmichFileResponse } from 'src/utils/file';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
import { isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc';
|
||||
import { usePagination } from 'src/utils/pagination';
|
||||
import { IsNull } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class PersonService extends BaseService {
|
||||
@ -306,7 +305,7 @@ export class PersonService extends BaseService {
|
||||
);
|
||||
this.logger.debug(`${faces.length} faces detected in ${previewFile.path}`);
|
||||
|
||||
const facesToAdd: (Partial<AssetFaceEntity> & { id: string })[] = [];
|
||||
const facesToAdd: (Partial<AssetFaceEntity> & { id: string; assetId: string })[] = [];
|
||||
const embeddings: FaceSearchEntity[] = [];
|
||||
const mlFaceIds = new Set<string>();
|
||||
for (const face of asset.faces) {
|
||||
@ -414,18 +413,22 @@ export class PersonService extends BaseService {
|
||||
}
|
||||
|
||||
const lastRun = new Date().toISOString();
|
||||
const facePagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
||||
this.personRepository.getAllFaces(pagination, {
|
||||
where: force ? undefined : { personId: IsNull(), sourceType: SourceType.MACHINE_LEARNING },
|
||||
}),
|
||||
const facePagination = this.personRepository.getAllFaces(
|
||||
force ? undefined : { personId: null, sourceType: SourceType.MACHINE_LEARNING },
|
||||
);
|
||||
|
||||
for await (const page of facePagination) {
|
||||
await this.jobRepository.queueAll(
|
||||
page.map((face) => ({ name: JobName.FACIAL_RECOGNITION, data: { id: face.id, deferred: false } })),
|
||||
);
|
||||
let jobs: { name: JobName.FACIAL_RECOGNITION; data: { id: string; deferred: false } }[] = [];
|
||||
for await (const face of facePagination) {
|
||||
jobs.push({ name: JobName.FACIAL_RECOGNITION, data: { id: face.id, deferred: false } });
|
||||
|
||||
if (jobs.length === JOBS_ASSET_PAGINATION_SIZE) {
|
||||
await this.jobRepository.queueAll(jobs);
|
||||
jobs = [];
|
||||
}
|
||||
}
|
||||
|
||||
await this.jobRepository.queueAll(jobs);
|
||||
|
||||
await this.systemMetadataRepository.set(SystemMetadataKey.FACIAL_RECOGNITION_STATE, { lastRun });
|
||||
|
||||
return JobStatus.SUCCESS;
|
||||
@ -441,7 +444,7 @@ export class PersonService extends BaseService {
|
||||
const face = await this.personRepository.getFaceByIdWithAssets(
|
||||
id,
|
||||
{ person: true, asset: true, faceSearch: true },
|
||||
{ id: true, personId: true, sourceType: true, faceSearch: { embedding: true } },
|
||||
{ id: true, personId: true, sourceType: true, faceSearch: true },
|
||||
);
|
||||
if (!face || !face.asset) {
|
||||
this.logger.warn(`Face ${id} not found`);
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { ImmichWorker } from 'src/enum';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { SmartInfoService } from 'src/services/smart-info.service';
|
||||
import { IConfigRepository } from 'src/types';
|
||||
import { getCLIPModelInfo } from 'src/utils/misc';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
@ -284,7 +284,7 @@ describe(SmartInfoService.name, () => {
|
||||
});
|
||||
|
||||
it('should save the returned objects', async () => {
|
||||
machineLearningMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]);
|
||||
machineLearningMock.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]');
|
||||
|
||||
expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS);
|
||||
|
||||
@ -293,7 +293,7 @@ describe(SmartInfoService.name, () => {
|
||||
'/uploads/user-id/thumbs/path.jpg',
|
||||
expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
|
||||
);
|
||||
expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, [0.01, 0.02, 0.03]);
|
||||
expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, '[0.01, 0.02, 0.03]');
|
||||
});
|
||||
|
||||
it('should skip invisible assets', async () => {
|
||||
@ -315,7 +315,7 @@ describe(SmartInfoService.name, () => {
|
||||
});
|
||||
|
||||
it('should wait for database', async () => {
|
||||
machineLearningMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]);
|
||||
machineLearningMock.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]');
|
||||
databaseMock.isBusy.mockReturnValue(true);
|
||||
|
||||
expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS);
|
||||
@ -326,7 +326,7 @@ describe(SmartInfoService.name, () => {
|
||||
'/uploads/user-id/thumbs/path.jpg',
|
||||
expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
|
||||
);
|
||||
expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, [0.01, 0.02, 0.03]);
|
||||
expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, '[0.01, 0.02, 0.03]');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -141,7 +141,10 @@ describe(StackService.name, () => {
|
||||
await sut.update(authStub.admin, 'stack-id', { primaryAssetId: assetStub.image1.id });
|
||||
|
||||
expect(stackMock.getById).toHaveBeenCalledWith('stack-id');
|
||||
expect(stackMock.update).toHaveBeenCalledWith({ id: 'stack-id', primaryAssetId: assetStub.image1.id });
|
||||
expect(stackMock.update).toHaveBeenCalledWith('stack-id', {
|
||||
id: 'stack-id',
|
||||
primaryAssetId: assetStub.image1.id,
|
||||
});
|
||||
expect(eventMock.emit).toHaveBeenCalledWith('stack.update', {
|
||||
stackId: 'stack-id',
|
||||
userId: authStub.admin.user.id,
|
||||
|
@ -39,7 +39,7 @@ export class StackService extends BaseService {
|
||||
throw new BadRequestException('Primary asset must be in the stack');
|
||||
}
|
||||
|
||||
const updatedStack = await this.stackRepository.update({ id, primaryAssetId: dto.primaryAssetId });
|
||||
const updatedStack = await this.stackRepository.update(id, { id, primaryAssetId: dto.primaryAssetId });
|
||||
|
||||
await this.eventRepository.emit('stack.update', { stackId: id, userId: auth.user.id });
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { SystemMetadataKey } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { StorageService } from 'src/services/storage.service';
|
||||
import { IConfigRepository } from 'src/types';
|
||||
import { ImmichStartupError } from 'src/utils/misc';
|
||||
import { mockEnvData } from 'test/repositories/config.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user